SystemD Timers vs Code Loops

In this post, we are going to compare the differences in effectiveness, from a computation and energy consumption perspective, between SystemD Timers and in-code loops for executing infinite loops.

We’ll focus specifically on Linux, since features such as timerfd are only available on it, although similar implementations exist for BSD and OSX such as kqueue.

The inspiration of this post came from this StackOverflow thread.

The case for SystemD Timers

Pros

timerfd

SystemD makes use of the Linux Kernel’s timerfd timers, a POSIX timer alternative that is more event-loop friendly, since it notifies time expiration via file descriptors (hence the fd suffix), which allows for the use of things such as epoll.

Being event-loop based, it also facilitates multi-threading and is very resource friendly.

Also, POSIX timers are considered by some to have a terrible API.

Resource Accounting

If you add CPUAccounting and MemoryAccounting to your service’s [Service] block (or have those enabled by default), and also have CGroup accounting enabled (usually the default), you are able to known exactly how much resource your service unit consumed without having to resort to custom code or external tools.

By running systemctl status on such services, you get an output similar to the one below.

● docker.service - Docker Application Container Engine
     Loaded: loaded (/nix/store/j0y2wmaywsvf8hs7y4pqd4jhll0ncsa8-docker-19.03.12/etc/systemd/system/docker.service; enabled; vendor preset: enabled)
    Drop-In: /nix/store/ig74rh79479nq89dd20fjhsn82kf0xdh-system-units/docker.service.d
             └─overrides.conf
     Active: active (running) since Tue 2021-03-16 08:34:15 -03; 56min ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2830 (dockerd)
         IP: 0B in, 0B out
      Tasks: 29 (limit: 4915)
     Memory: 163.3M  <<<<< Memory Accounting
        CPU: 9.710s  <<<<< CPU Accounting
     CGroup: /system.slice/docker.service
             ├─2830 /nix/store/j0y2wmaywsvf8hs7y4pqd4jhll0ncsa8-docker-19.03.12/libexec/docker/dockerd --group=docker --host=fd:// --log-driver=journald --live-restore
             └─2844 containerd --config /var/run/docker/containerd/containerd.toml --log-level info

Last Run and Next Run

With SystemD, by running systemctl list-timers you are able to conveniently check when the timer was last run, when it’s scheduled to run next and how much time is left before that happens. You are also able to see when the timer suceeded for the last time.

Immediate Changes

When you run a timer, any change you make to your program or script will of course be used next time it runs.

If your service where to loop in it’s on code, you would need to restart it in order to apply the changes.

A trivial matter, but worth considering.

Cons

Startup Cost

A timer needs to pay the cost of starting up the application every time it runs.

The impact of this obviously depends on your specific use case.

In a particular case of mine where I use nix-shell, this cost in not irrelevant, although I mitigated it using cached-nix-shell.

Excessive Logging

Each time the unit starts or stops, a new log entry is created on the journal.

For very frequent tasks, this may fill your journal with useless information.

Apparently this can be mitigated by using LogLevelMax=alert on the service definition, but I haven’t tested this.

Accuracy

Not exactly a con since it can be easily configured around, but by default SystemD timers have an accuracy of 1 minute. T

his means that, much like Cron, your minimum interval by default is 1 minute.

This can be fixed by setting AccuracySec to something lower.

For scripts that I want to run every 2 seconds or so, I set AccuracySec=1s, but you can go as lower as AccuracySec=1us, although that’s likely overkill and will generate much more wake-ups than you actually need, consuming more battery.

Set it to a sane amount based on your exact need.

The case for Code Loops

This part is extremely difficult to summarize, since each and every programming language has their very own way of handling timers.

On Python, for example, you can use linuxfd to interface with the same timerfd SystemD uses, achieving a very similar result. By default, if I’m not mistaken, calling functions such as sleep makes use of the default POSIX Timer interface.

On C, you obviously have easy access to any syscall you need, allowing you to use the implementation that suits you best.

For JVM-based languages, such as Java, Clojure, Kotlin (the list goes on…), it mainly depends on the specific JVM implementation, but it most likely ends up mapping to the default POSIX Timer on Unix systems.

In Shell, since each and every instruction is a command, you basically have startup costs for every line anyway, so it doesn’t matter.

As a clear pro, you are able to have much more granular control of when and how your code loops.

Charts

TBD

Conclusion

Resource consumption wise, it’s usually the case for in-code loops to be lighter, since you are avoiding the startup costs.

But in conclusion, use whatever suits you best.

If you just need to run a simple script every few seconds or so and don’t want to worry about treating errors etc, just put it under a SystemD timer, configure Restart accordingly and forget about it.

If you otherwise need more complex scenarios, use in-code loops as you normally would, and consider implementing systemd-notify for tighter integration.

comments powered by Disqus