From 872bcf3f09ce30cc710899108b9326b08f4f2ea6 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Tue, 3 Feb 2026 03:37:21 +0700 Subject: journal/2026: new journal "Reducing Idle Services with systemd" In this journal, we will take a look on how to minimize idle services running in the background using systemd-socket-proxyd(8) for services that we cannot control their code and using systemd.socket(5) for services that we can changes their code. In the _project submodules, we update awwan, gorankusu, pakakeh.go/cmd/httpdfs to run with socket based activation. This site program also has been modified to run with socket based activation too. --- _content/index.adoc | 3 +- _content/journal/2026/index.adoc | 11 + .../2026/reducing_idle_services/host_processes.png | Bin 0 -> 201651 bytes .../journal/2026/reducing_idle_services/index.adoc | 277 +++++++++++++++++++++ _project/src/awwan | 2 +- _project/src/gorankusu | 2 +- _project/src/jarink | 2 +- _project/src/lilin | 2 +- _project/src/pakakeh.go | 2 +- _project/src/rescached | 2 +- cmd/www-kilabit/main.go | 16 ++ 11 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 _content/journal/2026/reducing_idle_services/host_processes.png create mode 100644 _content/journal/2026/reducing_idle_services/index.adoc diff --git a/_content/index.adoc b/_content/index.adoc index 98e0d6a..e1e593c 100644 --- a/_content/index.adoc +++ b/_content/index.adoc @@ -3,8 +3,7 @@ = kilabit.info :description: Personal website of Muhammad Shulhan, contains list of open source projects, articles on software engineering, talks, and journals. -:revnumber: v20260202-1 -:revdate: 2 February 2026 +:revnumber: v20260203-1 :sectanchors: :stylesheet: default, index.css :toc: diff --git a/_content/journal/2026/index.adoc b/_content/journal/2026/index.adoc index ff5d0fd..531ac52 100644 --- a/_content/journal/2026/index.adoc +++ b/_content/journal/2026/index.adoc @@ -3,6 +3,17 @@ === 2026 +link:/journal/2026/reducing_idle_services/[Reducing Idle Services with +SystemD^]. +In this journal, we will take a look on how to minimize idle services +running in the background using +https://man.archlinux.org/man/systemd-socket-proxyd.8[systemd-socket-proxyd(8)^] +for services that we cannot control their code +and using +https://man.archlinux.org/man/systemd.socket.5[systemd.socket(5)^] +for services that we can changes their code. + + link:/journal/2026/systemd_socket_with_go/[systemd Socket Activation with Go^]. In this journal, we will take a look on how to implement socket-based activation on Go program that listen and serve HTTP. diff --git a/_content/journal/2026/reducing_idle_services/host_processes.png b/_content/journal/2026/reducing_idle_services/host_processes.png new file mode 100644 index 0000000..6fcde60 Binary files /dev/null and b/_content/journal/2026/reducing_idle_services/host_processes.png differ diff --git a/_content/journal/2026/reducing_idle_services/index.adoc b/_content/journal/2026/reducing_idle_services/index.adoc new file mode 100644 index 0000000..bbf4775 --- /dev/null +++ b/_content/journal/2026/reducing_idle_services/index.adoc @@ -0,0 +1,277 @@ += Reducing Idle Services with systemd +1 February 2026 +:description: Minimize running services in Linux with systemd-socket-proxyd +:sectanchors: +:toc: + +In this journal, we will take a look on how to minimize idle services +running in the background using +https://man.archlinux.org/man/systemd-socket-proxyd.8[systemd-socket-proxyd(8)^] +for services that we cannot control their code +and using +https://man.archlinux.org/man/systemd.socket.5[systemd.socket(5)^] +for services that we can changes their code. + +== Background + +Lets take a look running processes on my host machine +(click on the image to enlarge): + +.Host processes with top sorted by VIRT column +image:host_processes.png[Host process,width=100%] + +At the top, we have a process with 261.3 gigabytes virtual memory address. +This service rarely used, only once a day or less. + +Below that, we have HTTP services that I manage for local development using +systemd user services. +For example, this website has their own local domain `kilabit.local` that +run on specific port. + +My goal is to reduce the running processes and activate it only when I +opened the port or domain in the browser or when some other services connect +to the port directly. + +As for measurement, we count number of tasks using `ps`, memory using +`free`, and boot time using `systemd-analyze` after booting and login. +Here is the initial value, + +---- +$ ps -e | wc -l +198 + +$ free -m + total used free shared buff/cache available +Mem: 15620 3562 10370 142 2132 12058 +Swap: 4095 0 4095 + +$ systemd-analyze +Startup finished in 6.868s (firmware) + 2.737s (loader) + 934ms (kernel) + \ + 1.693s (initrd) + 6.198s (userspace) = 18.432s +graphical.target reached after 6.193s in userspace. + +$ +---- + +Lets start with external services first. + +== Using systemd-socket-proxyd + +For external services where I cannot changes their code, I use +systemd-socket-proxyd. + +[quote, systemd-socket-proxyd, https://man.archlinux.org/man/systemd-socket-proxyd.8] +systemd-socket-proxyd is a generic socket-activated network socket +forwarder proxy daemon for IPv4, IPv6 and UNIX stream sockets. +It may be used to bi-directionally forward traffic from a local listening +socket to a local or remote destination socket. + +For example, let say I have service `X` that listen on port 8080. +We will let the systemd.socket listen to that port and run the actual +service on port 18080. +Any other services still connect to port 8080 to communicate to service `X` +through systemd-socket-proxyd. + +First, we create the `/etc/systemd/system/proxy-X.socket` that will listen +on port 8080, + +---- +[Socket] +ListenStream=127.0.0.1:8080 + +[Install] +WantedBy=sockets.target +---- + +Next, we create the `/etc/systemd/system/proxy-X.service` that will run +`systemd-socket-proxyd` and channel the traffic to port 18080, + +---- +[Unit] +Requires=X.service +Requires=proxy-X.socket +After=X.service +After=proxy-X.socket + +[Service] +Type=notify +ExecStartPre=/bin/sleep 3 +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=5m \ + 127.0.0.1:18080 +ExecStopPost=systemctl stop X.service +---- + +Note that, in this case, we add the `ExecStartPre=` to wait for `X.service` +run successfully, and the `ExecStopPost=` to stop the service. +Both of these may or may not needed, depends on the service. + +In the `X.service`, we override the service unit file to run on port 18080 +instead of default port, + +---- +# /etc/systemd/system/X.service.d/override.conf +[Unit] +StopWhenUnneeded=true + +[Service] +ExecStart= +ExecStart=/usr/bin/X --Port 18080 +---- + +With `--exit-idle-time=5m` and `StopWhenUnneeded=true`, we tell the systemd +to stop the service when no traffic received after 5 minutes. + +Now stop the actual service and start the `proxy-X.socket` service, + +---- +$ sudo systemctl disable --now X.service +$ sudo systemctl enable --now proxy-X.socket +$ +---- + +We test it by opening the service port `:8080` in the browser. +Here is sample log in the journal, + +---- +Feb 01 18:20:11 systemd[1]: Started X Daemon. +Feb 01 18:20:11 systemd[1]: Starting proxy-X.service... + +Feb 01 18:20:14 X[8144]: Now listening on: http://127.0.0.1:8080 +Feb 01 18:20:14 systemd[1]: Started proxy-X.service. +---- + +After 5 minutes we will the service stopped automatically, + +---- +Feb 01 18:25:34 systemd[1]: proxy-X.service: Deactivated successfully. +Feb 01 18:25:34 systemd[1]: Stopping X Daemon... +Feb 01 18:25:34 X[8144]: Application is shutting down... +Feb 01 18:25:34 X[8144]: 02-01 18:25:34 Info X stopped +Feb 01 18:25:34 systemd[1]: X.service: Deactivated successfully. +Feb 01 18:25:34 systemd[1]: Stopped X Daemon. +Feb 01 18:25:34 systemd[1]: X.service: Consumed 4.010s CPU time over 5min 23.145s wall clock time, 163.1M memory peak. +---- + +Great. +We can repeat the same configurations to other services. + + +== Using systemd.socket + +Now, the hard part is to rewrite all of the programs so it can be activated +using +https://man.archlinux.org/man/systemd.socket.5[systemd.socket(5)^]. + +All of the program that we want to convert are built with Go. +In the +link:/journal/2026/systemd_socket_with_go/[previous journal], +I have describe how to modify the Go program so it can be activate using +systemd.socket(5). +If you have a program that need to be converted too, but not written using +Go, you can read the following blog first to understand the basic, + +- https://0pointer.de/blog/projects/socket-activation.html[systemd for Developers I^] +- https://0pointer.de/blog/projects/socket-activation2.html[systemd for Developers II^] + +We will modify each of the main function in the program. +There are at least 11 of them. +Fortunately, most of them are using the same library/package. + +- local.awwan.service => link:/project/ciigo/[ciigo] based. + This serve the local version of https://awwan.org . + +- local.golang-id.service => link:/project/ciigo/[ciigo] based. This serve + the local version of https://golang-id.org . + +- local.golang-id-tour.service => net/http based. + This serve the local version of https://tour.golang-id.org . + +- local.home.bahasa-go.service => link:/project/ciigo/[ciigo] based. + +- local.home.gitdoc.service => + https://git.sr.ht/~shulhan/pakakeh.go/tree/main/item/cmd/httpdfs[httpdfs] + => lib/http based. This serve the + https://git.kernel.org/pub/scm/git/git.git/tree/Documentation[Git documentation] + directory + +- local.home.go.service => net/http based. + This run golang.org/x/website for offline documentation (the + same content as https://go.dev ). + You can view the patch + https://git.sr.ht/~shulhan/go-x-website/commit/1490b977eefea3c9cb1b35aaf211cd6934299383[here^] + (subject to changes in the future). + +- local.home.godoc.service => net/http based. + This run golang.org/x/pkgsite with local modules. + You can see the patch + https://git.sr.ht/~shulhan/go-x-pkgsite/commit/a2088f34de993bb4a4bd24dd6d722605373e5d08[here^] + (subject to changes in the future). + +- local.home.gorankusu.service => lib/http based. + +- local.home.ops.service => lib/http based + +- local.kilabit.service => link:/project/ciigo/[ciigo] based. + +- local.kilabit.umum.service => + https://git.sr.ht/~shulhan/pakakeh.go/tree/main/item/cmd/httpdfs[httpdfs] + => lib/http based. + +Most of the modification are adding the following identical code, + +---- +... + listeners, err := systemd.Listeners(true) + if err != nil { + log.Fatal(err) + } + if len(listeners) > 1 { + log.Fatal(`too many listeners received for activation`) + } + var listener net.Listener + if len(listeners) == 1 { + listener = listeners[0] + gotAddr := listener.Addr().String() + if gotAddr != serveOpts.Address { + log.Fatalf(`invalid Listener address, got %s, want %s`, + gotAddr, serveOpts.Address) + } + } + // Pass the listener to HTTP Server. +... +---- + + +== Results + +Let's reboot the machine and see the results. + +---- +$ diff -u before after +--- before 2026-02-02 23:52:11.677244743 +0700 ++++ after 2026-02-02 23:52:25.315331792 +0700 +@@ -1,12 +1,12 @@ + $ ps -e | wc -l +-198 ++182 + + $ free -m + total used free shared buff/cache available +-Mem: 15620 3562 10370 142 2132 12058 ++Mem: 15620 2898 12491 102 601 12722 + Swap: 4095 0 4095 + + $ systemd-analyze +-Startup finished in 6.868s (firmware) + 2.737s (loader) + 934ms (kernel) + \ +- 1.693s (initrd) + 6.198s (userspace) = 18.432s +-graphical.target reached after 6.193s in userspace. ++Startup finished in 6.804s (firmware) + 1.438s (loader) + 985ms (kernel) + \ ++ 1.508s (initrd) + 5.985s (userspace) = 16.722s ++graphical.target reached after 5.983s in userspace. +---- + +The number of processes started on boot reduced by 16, +the total memory used reduced by 664 MB (-18%), and +the boot time reduced by 1.71 second (-9%). +Well, it is not so bad. The big win is in the reduced memory usage and +learning how to use `systemd.socket` and `systemd-socket-proxyd`. diff --git a/_project/src/awwan b/_project/src/awwan index 663fdaf..bbbb67b 160000 --- a/_project/src/awwan +++ b/_project/src/awwan @@ -1 +1 @@ -Subproject commit 663fdaf45162ac0eb1b45672f90fa58b73ab6f04 +Subproject commit bbbb67bc6cfff3a42db6b7f0feec6c6a54875be4 diff --git a/_project/src/gorankusu b/_project/src/gorankusu index 34cb54f..a6947ee 160000 --- a/_project/src/gorankusu +++ b/_project/src/gorankusu @@ -1 +1 @@ -Subproject commit 34cb54fc8fc68dededfe57e42ba8ae50d0d91fda +Subproject commit a6947eef2c0efbd30fc2d93127c44537e4358ac0 diff --git a/_project/src/jarink b/_project/src/jarink index 2a4376d..a33efc3 160000 --- a/_project/src/jarink +++ b/_project/src/jarink @@ -1 +1 @@ -Subproject commit 2a4376d5ddeee82d4ef38f4953453abb43e85220 +Subproject commit a33efc3992f58355eb98d7a5574df955952924b8 diff --git a/_project/src/lilin b/_project/src/lilin index bf87c6a..fbad5db 160000 --- a/_project/src/lilin +++ b/_project/src/lilin @@ -1 +1 @@ -Subproject commit bf87c6ad4824c7ed1c990aeff2d2883936c1f20a +Subproject commit fbad5db9b998ca64feaffee78ba680300d3a52a6 diff --git a/_project/src/pakakeh.go b/_project/src/pakakeh.go index 1e3bb9b..ca3002f 160000 --- a/_project/src/pakakeh.go +++ b/_project/src/pakakeh.go @@ -1 +1 @@ -Subproject commit 1e3bb9be8444082dc1ada7f76a727b9a9f764d47 +Subproject commit ca3002fb7041a863600042f013e12faf73caf227 diff --git a/_project/src/rescached b/_project/src/rescached index 3bbfcd5..d30b17c 160000 --- a/_project/src/rescached +++ b/_project/src/rescached @@ -1 +1 @@ -Subproject commit 3bbfcd5d598f0aa0f4d1f840f3ba7df4a74dbe5c +Subproject commit d30b17c4db1392c19b11c3af7a47051f8dd6568a diff --git a/cmd/www-kilabit/main.go b/cmd/www-kilabit/main.go index d1bbaba..cfbcb41 100644 --- a/cmd/www-kilabit/main.go +++ b/cmd/www-kilabit/main.go @@ -10,6 +10,7 @@ import ( "git.sr.ht/~shulhan/ciigo" "git.sr.ht/~shulhan/pakakeh.go/lib/memfs" + "git.sr.ht/~shulhan/pakakeh.go/lib/systemd" ) const ( @@ -55,6 +56,21 @@ func main() { } default: + listeners, err := systemd.Listeners(true) + if err != nil { + log.Fatal(err) + } + if len(listeners) > 1 { + log.Fatal(`too many listeners received for activation`) + } + if len(listeners) == 1 { + serveOpts.Listener = listeners[0] + gotAddr := serveOpts.Listener.Addr().String() + if gotAddr != serveOpts.Address { + log.Fatalf(`invalid Listener address, got %s, want %s`, + gotAddr, serveOpts.Address) + } + } err = ciigo.Serve(serveOpts) if err != nil { log.Fatal(err) -- cgit v1.3