Suche
Suche Menü

Container, hört die Signale!

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

Schreibe einen Kommentar

Pflichtfelder sind mit * markiert.


Agile Softwareentwicklung

Durch die weitere Nutzung der Seite stimmst du der Verwendung von Cookies zu. Weitere Informationen

Die Cookie-Einstellungen auf dieser Website sind auf "Cookies zulassen" eingestellt, um das beste Surferlebnis zu ermöglichen. Wenn du diese Website ohne Änderung der Cookie-Einstellungen verwendest oder auf "Akzeptieren" klickst, erklärst du sich damit einverstanden.

Schließen