Hast du in der Arbeit mit Docker schon einmal eines dieser Symptome gehabt?
- Du willst einen laufenden Container mit
docker stop
beenden, aber er lässt sich zehn Sekunden Zeit bis er sich tatsächlich beendet? - Du hast eine Applikation in Kubernetes deployt und extra ein Shutdownverhalten eingebaut, damit offen gehaltene Verbindungen beim Herunterfahren eines Pods geschlossen werden – aber irgendwie brechen immer noch Verbindungen beim Shutdown ab?
Das und mehr kann als Ursache haben, dass das Signalhandling in deinem Container nicht korrekt funktioniert. In anderen Worten: deine Applikation bekommt nicht mit, dass sie sich beenden soll. Schauen wir uns erst einmal an, was eigentlich passieren sollte. Nehmen wir an, du willst einen laufenden Container mit docker stop
anhalten. Docker sendet dann an dem im Container laufenden Prozess das Signal SIGTERM
. Dies ist quasi der freundliche Hinweis an den Prozess, sich zu beenden. Wenn der Prozess dieser Anweisung keine Folge leistet, sendet Docker schließlich das SIGKILL
an den Prozess, der diesen ohne Gnade beendet. Als User stellt sich das so dar, dass der Container sekundenlang braucht bis er schließlich stoppt.
Das Problem ist hier entweder, dass deine Applikation das SIGTERM
ignoriert (z.B. weil sie kein Signalhandling implementiert), oder dass dieses Signal irgendwo zwischen Docker und deiner Applikation verloren geht.
Wo geht das Signal verloren?
Sendet Docker ein SIGTERM
an einen Container, wird dieses Signal konkret an den Prozess weitergeleitet, der im Container die Prozess-ID (PID) 1 besitzt. Dies ist der ENTRYPOINT
, der im Dockerfile angegeben wurde. Der häufigste Grund für verlorenes SIGTERM
ist daher, dass die PID 1 nicht deine Applikation, sondern z.B. Bash ist. Dies kann explizit passieren, wenn du im Dockerfile deinen Entrypoint so angibst:
ENTRYPOINT [ "bash.sh" , "-c" , "application" , "--some-args" ] |
oder implizit, wenn du deinen Entrypoint in der “Shell”-Notation angibst:
ENTRYPOINT application --some-args |
denn in diesem Fall wird im Hintergrund der Wert des ENTRYPOINTs
als Argument an bash -c
übergeben. Und dies ist dann Ursache des Problems, denn Shellskripte besitzen zunächst einmal kein Signalhandling.
Klare Empfehlung also: Immer die JSON-Array-Notation in ENTRYPOINT
und CMD
verwenden.
Die verflixte PID 1
Leider löst dies zwar einige Probleme, aber bei weitem nicht alle. Dadurch, dass jeder Container einen eigenen Prozess-Namespace erhält, hat der ENTRYPOINT
immer die Prozess-ID (PID) 1. Der Prozess mit dieser ID hat eine Sonderrolle, die in einem regulären Linux-System vom Initsystem eingenommen wird, z.B. systemd oder SysVInit. Dieses Initsystem kümmert sich unter anderem darum, dass keine Zombieprozesse auftreten. Diese “Zombies” können auch in Containern auftreten, wenn die Prozesse im Container Kindprozesse erzeugen. Exzellent erklärt ist dies auch in diesem Blogpost.
Man könnte nun seiner Applikation beibringen, sich wie ein Initsystem zu verhalten – oder man verwendet ein vorgefertigtes, minimales Initsystem.
Ein Mini-Initsystem
Zum Glück gibt es mehrere minimale Initsysteme, die man sehr einfach als statisch gelinkte Binaries in seinen Container einbauen kann. Beispiele wären dumb-init oder tini. Deren Dokumentation erläutert bereits gut, wie man sie in seinen Build integrieren kann. Hat man eine solche Binary als seinen ENTRYPOINT
angegeben, kümmert sich diese um die Weitergabe der Signale an die ausgeführte Applikation und das Aufräumen von Zombieprozesse.
Docker selbst bringt seit Version 1.13 tini mit, ohne dass man es selbst in seinen Container stecken muss. Bei docker run
versteckt sich dieses Feature hinter der Flag --init
:
docker run --init myimage:mytag |
Auch Docker Compose bringt ab Dateiformat 3.7 den Key init
mit, der die gleiche Funktionalität exponiert.
Exkurs: Kubernetes
In Kubernetes-Pods löst der Pause-Container das PID-1-Problem – aber nur wenn im Cluster das Feature Process Namespace Sharing aktiv ist. Dann teilen sich alle Container eines Pods einen gemeinsamen PID-Namespace, und der Pause-Container übernimmt die Rolle von PID 1. Dieses Feature ist allerdings noch im Beta-Stadium und per default deaktiviert.
Fazit
Was du immer machen solltest: die JSON-Notation für ENTRYPOINT
und CMD
verwenden
Worüber du nachdenken solltest: dumb-init
oder tini
einsetzen