sd-pam und der RAM

Posted by luto on Monday, April 8, 2019

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:

ram_graph

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