sd-pam und der RAM
systemd hat ein nettes Feature: PAMName=
. Damit lassen sich Services wie unser php-fpm oder supervisord über das PAM-System abwickeln, das dann diverse Einstellungen für die Sitzung vornimmt. Wir selbst nutzen PAM um den technischen Unterbau der Web Backends, Network Namespaces, sauber auf breiter Ebene umzusetzen. Zusammen mit PAMName=
haben wir so für interaktive SSH-Logins, Cronjobs, Services und vieles mehr nur einen einzigen Mechanismus, anstatt vieler kleiner.
[Unit]
Description=PHP FastCGI process manager
After=local-fs.target network.target
[Service]
Type=notify
ExecStart=/usr/bin/php-fpm --fpm-config ..../%I/php-fpm.conf
User=%I
PAMName=su-l
Alles schön! Alles sauber! … dachten wir. In der Entwicklung lief es auch prima: über einen längeren Zeitraum das Feature gebaut, Tests geschrieben, eine Hand voll ausgewählte User auf einem Test-Host spielen lassen: tut. Auf die ersten beiden produktiven Hosts ausgerollt: tut. Gut, der RAM-Bedarf ist etwas gestiegen, aber nicht um Welten. Die nächsten drei Hosts haben uns dann aber schnell das Fürchten gelehrt: Kiste tot, RAM voll. Selbst mit 6 GB mehr war da nichts mehr zu holen. Nach ein bisschen Hin und Her haben wir sie natürlich wieder hinbekommen; die schönen neuen Namespaces waren aber erstmal wieder aus.
Aber warum? Dazu ein bisschen Theorie:
PAM ist ein Session-basiertes System. Ein Prozess (z.B. der SSH-Server oder auch sudo
) macht eine PAM-Session auf, lässt den User Dinge tun und mach die Session danach wieder zu. Bei systemd-Services gibt es in der Lebenszeit des Services allerdings nicht zwangsläufig einen durchgehenden Prozess. Je nach Service-Typ stirbt der erste Prozess auch schnell mal. Es gibt also schlicht niemanden, der sich um das gute PAM kümmern kann. Daher haben sich die Jungs und Mädels rund um Meister Poettering was ausgedacht: sd-pam
.
Der sd-pam
-Prozess läuft bei jedem systemd-Service mit gesetztem PAMName=
im Hintergrund mit. Technisch gesehen ist er einfach nur Fork des PID 1 systemd Prozesses. Er kümmert sich, sobald sich die Lebenszeit des Services dem Ende neigt, darum, die vorhin geöffnete PAM-Session auch wieder ordentlich zu schließen. Bis das passiert, tut sd-pam
eigentlich so gut wie gar nichts.
Tja, wo ist denn nun das Problem mit einem Prozess der so gut wie gar nichts tut? Die Resourcen! Genauer gesagt: der RAM.
1155 user1 425844 21328 12 S.0.1:00.00 (sd-pam)
1178 user2 665920 30784 14976 S.0.1:21.29 php-fpm
1187 user3 662236 40432 30012 S.0.2:03.24 php-fpm
1197 user3 425844 21328 12 S.0.1:00.00 (sd-pam)
1313 user4 662236 40436 30016 S.0.2:03.56 php-fpm
1317 user5 15756 2388 2032 S.0.0:00.05 imap
1320 user4 425844 21328 12 S.0.1:00.00 (sd-pam)
1358 user6 662236 40196 29772 S.0.2:04.00 php-fpm
1368 user6 425844 21328 12 S.0.1:00.00 (sd-pam)
1389 user7 758240 53696 28384 S.0.2:02.87 php-fpm
1490 user8 676928 25404 2024 S.0.1:18.60 radicale
1642 user7 758420 58828 33224 S.0.2:03.08 php-fpm
1793 user9 662236 40196 29776 S.0.2:06.37 php-fpm
1804 user9 425844 21328 12 S.0.1:00.00 (sd-pam)
In obrigem, gekürztem top-Output sind schön diverse User-Prozesse und ein sd-pam
pro User-Service zu sehen. Jeder dieser Prozesse benötigt je nach Host irgendetwas von 20 bis 50 Megabyte RAM. Wenn wir nun im Schnitt 120 User pro Host haben, wovon jeder 3 Services bekommt (php-fpm, supervisord und ein interner) sind wir bei … rund 10 Gigabyte RAM. Das schaffen unsere Hosts nicht aus dem Stand; und selbst wenn: die 10 Gigabyte würden wir viel lieber euch zum Hosten cooler Apps und nicht systemd zum ideln geben. Eigentlich sollten Prozesse aus einem fork()
erstmal nicht sonderlich viel RAM brauchen. Warum das hier im Detail nicht klappt ist uns bis heute nicht ganz klar.
PAMName=
ist für uns also mitten im Rollout unbrauchbar geworden. Sollte die “fucked situation” um sd-pam
doch noch in eine bessere verwandelt, würden wir die gefixte Version frühestens in CentOS 8 sehen. Das kürzlich eingebaute NetworkNamespacePath=
in CentOS 9, wenn wir dann überhaupt noch CentOS verwenden. Also, back to the basics. PAM ist natürlich lange nicht die einzige Möglichkeit, um Prozesse in Network Namespaces zu packen. Sehr viel klassischer ist das gute, alte nsenter
. Das haben wir kurzerhand zusammen mit anderen Aufgaben in ein neues uberspace-wrap-netns.sh
-Script gehackt:
#!/usr/bin/sh
# gekürzte Version ohne Error-Handling.
usr=$1; shift
# create namespace
/usr/bin/sudo -iu "$usr" /usr/bin/true
exec /usr/bin/nsenter \
--net=/var/run/netns/usr_$usr \
--setuid=$(id -u "$usr") --setgid=$(id -g "$usr") \
"$@"
… und das Script in unsere Services verpackt …
[Unit]
Description=PHP FastCGI process manager
After=local-fs.target network.target
[Service]
Type=notify
ExecStart=
/usr/local/sbin/uberspace-wrap-netns.sh %I
/usr/bin/php-fpm --fpm-config ..../%I/php-fpm.conf
#User=%I
#PAMName=su-l
Neben dem Fakt, dass wir nun erst recht mehr als einen Mechanismus haben, um Dinge in Namespaces zu packen, ist uns vor allem die die /usr/bin/sudo -iu
-Zeile dabei ein Dorn im Auge. Auch, dass wir statt dem User=
-Attribut nun selbst den User wechseln ist eher unschön. Aber, es tut erstmal und ist unglaublich effektiv:
Ein RAM-Sprung von run 10 GB in die Tiefe kann sich sehen lassen. Mittlerweile haben wir die Namespaces mit dieser Lösung auch schon Schritt für Schritt auf alle U7-Hosts ausgerollt.
Leider sind wir durch unsere kleine Havarie mit den Namespaces und Backends lange noch nicht fertig. Auf kurz oder lang müssen wir hier nochmal zurück zum Reißbrett, um unseren Mechanismus nochmal neu zu denken. Eine Möglichkeit ist systemds PrivateNetwork=
, das im Hintergrund ebenfalls Namespacing nutzt und sich mit ein bisschen Arbeit in eine generischere Lösung umbauen lässt. Alternativ dazu reicht es uns vielleicht auch schon nur den sudo
-Aufruf loszuwerden und die Namespaces auf einem anderen Weg als PAM zu erstellen.
So oder so, es bleibt spannend in der Entwicklung.
Photo by Fancycrave.com