3 Punkte von GN⁺ 2025-05-06 | 1 Kommentare | Auf WhatsApp teilen
  • Der Graceful Shutdown einer Go-Anwendung ist ein Beendigungsverfahren, das neue Anfragen blockiert, auf laufende Arbeiten wartet und anschließend Ressourcen wie Datenbankverbindungen, Dateisperren und Netzwerk-Listener bereinigt
  • Die Behandlung der Beendigung beginnt damit, Beendigungssignale wie SIGTERM und SIGINT über os/signal oder ab Go 1.16 über signal.NotifyContext abzufangen, um das standardmäßige sofortige Beenden zu ersetzen
  • In Kubernetes muss die Beendigung innerhalb der standardmäßigen Grace Period von 30 Sekunden abgeschlossen sein; über eine preStop-Verzögerung oder ein fehlschlagendes Readiness Probe sollte Zeit geschaffen werden, damit sich die Unterbrechung des Traffics bis zu externen Load Balancern fortpflanzen kann
  • http.Server.Shutdown blockiert neue Verbindungen und wartet auf den Abschluss aktiver Requests, aber wenn Handler die Context Cancellation nicht berücksichtigen, können Probleme wie partielle Schreibvorgänge, Datenverlust und offene Transaktionen entstehen
  • Wichtige Ressourcen sollten nicht direkt nach dem Beendigungssignal, sondern erst nach Abschluss der Requests oder nach Ablauf eines Zeitlimits bereinigt werden; ein Shutdown in der umgekehrten Initialisierungsreihenfolge erleichtert es, Abhängigkeiten zwischen Komponenten einzuhalten

Mindestbedingungen für Graceful Shutdown

  • Graceful Shutdown sollte in der Regel drei Bedingungen erfüllen
    • An Einstiegspunkten wie HTTP oder Pub/Sub keine neuen Anfragen oder Nachrichten mehr annehmen
    • Warten, bis bereits laufende Requests abgeschlossen sind, und bei zu langer Dauer mit einem Graceful Error antworten
    • Wichtige Ressourcen wie Datenbankverbindungen, Dateisperren und Netzwerk-Listener freigeben und letzte Aufräumarbeiten durchführen
  • Ausgehende Verbindungen zu externen Diensten wie Datenbanken oder Caches werden nicht schon in der Phase des Blockierens neuer Requests getrennt
  • Der Fokus liegt auf HTTP-Servern und Container-Anwendungen, aber die Grundprinzipien gelten auch für andere Anwendungen

Umgang mit Beendigungssignalen

  • In Unix-artigen Systemen sind Signale Software-Interrupts, die einem Prozess mitteilen, dass eine bestimmte Situation eingetreten ist
  • Ein Prozess kann für bestimmte Signale Handler registrieren; ohne Handler gilt das Standardverhalten
    • Das Standardverhalten kann Beenden, Stoppen, Fortsetzen oder Ignorieren sein
    • Manche Signale wie SIGKILL können weder abgefangen noch ignoriert werden und beenden den Prozess
  • Die Go-Runtime registriert schon vor Ausführung der main-Funktion automatisch verschiedene Signal-Handler, darunter SIGTERM, SIGQUIT, SIGILL und SIGTRAP
  • Für Graceful Shutdown sind vor allem drei Beendigungssignale wichtig
    • SIGTERM: die standardmäßige und höfliche Art, das Beenden eines Prozesses anzufordern; Kubernetes sendet dieses Signal vor einer erzwungenen Beendigung an die Anwendung
    • SIGINT: wird gesendet, wenn ein Benutzer den Prozess im Terminal mit Ctrl+C stoppen will
    • SIGHUP: wurde ursprünglich beim Trennen einer Terminalverbindung verwendet und dient heute oft auch als Signal zum Neuladen von Konfigurationen
  • Ohne gesonderte Behandlung beendet die Go-Runtime die Anwendung bei SIGTERM, SIGINT oder SIGHUP

os/signal und NotifyContext

  • signal.Notify weist die Go-Runtime an, bestimmte Signale nicht mit dem Standardverhalten zu behandeln, sondern an einen Channel weiterzuleiten
  • Für den Signal-Channel ist ein Puffer der Größe 1 meist die stabile Wahl
    • Intern verwendet Go für das Senden an den Channel select und default
    • Ist im Puffer Platz, wird das Signal zugestellt; ist der Puffer voll, wird das Signal verworfen
    • Bei einem ungepufferten Channel kann ein Signal verloren gehen, wenn gerade keine empfangende Goroutine vorhanden ist
  • signal.Notify kann für dasselbe Signal mehrfach aufgerufen werden; Go sendet das Signal dann an alle registrierten Channels
  • Selbst wenn Ctrl+C mehrfach gedrückt wird, wird die zweite Eingabe normalerweise nicht automatisch zu SIGKILL hochgestuft
    • Die meisten bash- oder Linux-Shells führen keine automatische Hochstufung durch
    • Für ein erzwungenes Beenden muss SIGKILL direkt mit kill -9 gesendet werden
  • Wenn in der lokalen Entwicklung das zweite Ctrl+C ein erzwungenes Beenden auslösen soll, kann direkt nach dem ersten Signal mit signal.Stop der Empfang weiterer Signale beendet werden
  • Seit Go 1.16 lässt sich die Signalbehandlung mit signal.NotifyContext an Context Cancellation koppeln
    • Auch nach ctx.Done() sollte stop() aufgerufen werden, damit ein zweites Ctrl+C die Anwendung zwangsweise beenden kann

Shutdown-Zeitlimit und Kubernetes-Verhalten

  • Nachdem ein Beendigungssignal eingegangen ist, sollte zuerst geklärt werden, wie viel Zeit der Anwendung real für den Shutdown zur Verfügung steht
  • Die Standard-Grace-Period von Kubernetes beträgt 30 Sekunden, sofern terminationGracePeriodSeconds nicht separat gesetzt ist
  • Danach sendet Kubernetes SIGKILL und stoppt die Anwendung zwangsweise
    • SIGKILL kann nicht abgefangen oder verarbeitet werden
  • Die gesamte Shutdown-Logik einschließlich Abarbeitung verbleibender Requests und Freigabe von Ressourcen muss innerhalb dieses Zeitfensters abgeschlossen sein
  • Nimmt man bei den Standard-30-Sekunden etwa 20 % als Sicherheitsmarge, sollte der gesamte Shutdown idealerweise innerhalb von 25 Sekunden beendet sein

Blockieren neuer Requests und Readiness-Verhalten

  • In Gos net/http lässt sich Graceful Shutdown mit http.Server.Shutdown umsetzen
    • Neue Verbindungen werden nicht mehr angenommen
    • Auf den Abschluss aktiver Requests wird gewartet
    • Anschließend werden Idle Connections geschlossen
  • Bereits laufende Requests können noch abgeschlossen werden; danach geht die Verbindung in den Idle-Zustand über und wird geschlossen
  • Clients, die während des Shutdowns eine neue Verbindung aufbauen wollen, erhalten in der Regel einen connection refused-Fehler, weil der Listener bereits geschlossen ist
  • In Container- oder Orchestrierungsumgebungen mit externem Load Balancer ist es wichtig, die Annahme neuer Requests nicht sofort zu stoppen
    • Ein Pod kann nach der Markierung zur Beendigung noch kurz Traffic empfangen
    • Die interne Kubernetes-Komponente kube-proxy erkennt den Wechsel des Pod-Status zu Terminating schnell
    • Externe Load Balancer verwenden jedoch eigene Health Checks unabhängig von Kubernetes und benötigen Zeit für die Zustandspropagierung
  • Es gibt zwei Wege, auf die Propagierung der Traffic-Sperre zu warten
    • Im preStop-Hook kurz sleep ausführen, damit externe Load Balancer Zeit haben, den Beendigungsstatus des Pods zu erkennen
      • Die Zeit im preStop zählt zur terminationGracePeriodSeconds
    • Auf Code-Ebene das Readiness Probe fehlschlagen lassen und kurz warten
      • Das funktioniert nicht nur in Kubernetes, sondern auch in anderen Umgebungen, in denen der Load Balancer den Bereitschaftszustand kennen muss
  • Ein Readiness Probe prüft regelmäßig, ob der Container bereit ist, Traffic anzunehmen
    • Health Checks können per HTTP-Request, TCP-Verbindung oder Kommandoausführung erfolgen
    • Scheitert das Probe, entfernt Kubernetes den Pod aus den Service-Endpoints, sodass er keinen Traffic mehr erhält
  • Für die Shutdown-Vorbereitung kann etwa ein atomic.Bool wie isShuttingDown verwendet werden, damit /healthz HTTP 503 zurückgibt
  • Nachdem der Readiness-Status auf fehlschlagend gesetzt wurde, sollte einige Sekunden auf die Propagierung der Änderung gewartet werden
    • Die Beispielkonfiguration verwendet periodSeconds: 5, und im Textbeispiel wird 5 Sekunden gewartet
    • Die genaue Wartezeit hängt von der Konfiguration des Readiness Probe ab

Umgang mit laufenden Requests

  • Passend zum Shutdown-Budget wird mit context.WithTimeout ein Zeitlimit erzeugt und an server.Shutdown(ctx) übergeben
  • server.Shutdown kehrt in zwei Fällen zurück
    • Alle aktiven Verbindungen sind geschlossen und alle Handler abgeschlossen
    • Der übergebene Context läuft vor Abschluss der Handler ab, sodass der Server das Warten aufgibt
  • In beiden Fällen kehrt Shutdown erst zurück, nachdem der Server die Request-Verarbeitung vollständig beendet hat
  • Handler sollten schnell und context-aware arbeiten
    • Andernfalls können sie beim Ablauf des Zeitlimits mitten in der Arbeit unterbrochen werden
    • Das kann zu partiellen Schreibvorgängen, Datenverlust, inkonsistentem Zustand, offenen Transaktionen oder beschädigten Daten führen
  • Es gibt zwei typische Wege, Handlern das Shutdown-Signal zu übermitteln
    • Über Middleware, die jedem Request-Context eine Cancel-Logik injiziert
    • Über BaseContext von http.Server, um allen Verbindungen einen gemeinsamen globalen Context bereitzustellen
  • Im HTTP-Server lassen sich BaseContext und ConnContext anpassen
    • Für Graceful Shutdown ist BaseContext besser geeignet, weil damit ein abbrechbarer globaler Context für den gesamten Server erstellt werden kann
  • Graceful Shutdown wirkt nur dann zuverlässig, wenn Funktionen die Context-Cancellation respektieren
    • Nutzungen wie context.Background() oder time.Sleep() sollten vermieden werden, wenn sie Cancellation ignorieren
    • time.Sleep(duration) kann durch ein select ersetzt werden, das gleichzeitig auf time.After(duration) und ctx.Done() wartet
  • In älteren Go-Versionen konnte time.After Speicher verlieren, bis der Timer ausgelöst wurde

Unterschied zwischen Shutdown und Close

  • Dieselben Prinzipien gelten nicht nur für HTTP-Server, sondern auch für Dienste von Drittanbietern
  • database/sql-DB.Close schließt Datenbankverbindungen, verhindert den Start neuer Queries und wartet auf den Abschluss laufender Queries
  • Entscheidend ist, keine neuen Requests oder Nachrichten mehr anzunehmen und bestehenden Arbeiten Zeit zu geben, innerhalb einer definierten Grace Period abzuschließen
  • server.Close() beendet sofort, ohne auf laufende Verbindungen zu warten
    • Handler, die das Netzwerk verwenden, erhalten beim Lesen oder Schreiben Fehler
    • Clients können sofort Verbindungsfehler wie ECONNRESET oder socket hang up erhalten
    • Lang laufende Handler ohne Netzwerkinteraktion können im Hintergrund weiterlaufen
  • server.Close() kann nach einem Fehler von server.Shutdown() verwendet werden, aber das hängt von der gewählten Shutdown-Strategie ab
  • Das Weiterreichen des Shutdown-Signals über einen Context ist der verlässlichere und gracefulere Ansatz

Reihenfolge beim Freigeben wichtiger Ressourcen

  • Ein häufiger Fehler ist es, wichtige Ressourcen sofort nach Eingang des Beendigungssignals freizugeben
  • Zu diesem Zeitpunkt können Handler und In-Flight-Requests diese Ressourcen noch verwenden; die Bereinigung sollte daher auf nach Ablauf des Shutdown-Timeouts oder nach Abschluss aller Requests verschoben werden
  • In vielen Fällen räumt das Betriebssystem Ressourcen bereits beim Prozessende auf
    • Von Go belegter Speicher wird beim Beenden des Prozesses freigegeben
    • File Descriptors werden vom Betriebssystem geschlossen
    • Auch OS-Ressourcen wie Process Handles werden freigegeben
  • In manchen Fällen ist explizites Aufräumen dennoch nötig
    • Datenbankverbindungen sollten sauber geschlossen werden, und offene Transaktionen benötigen ein Commit oder Rollback
    • Message Queues und Broker können ein Flush von Nachrichten, ein Commit von Offsets oder eine Abmeldung des Clients erfordern
    • Externe Dienste erkennen einen Verbindungsabbruch möglicherweise nicht sofort; ein manuelles Schließen der Verbindung kann schneller aufräumen, als auf ein TCP-Timeout zu warten
  • Eine gute Regel ist, Komponenten in der umgekehrten Reihenfolge ihrer Initialisierung zu beenden
    • Gos defer passt gut zu diesem Muster, weil die zuletzt registrierte Funktion zuerst ausgeführt wird
  • Für manche Komponenten, etwa wenn Daten aus einem In-Memory-Cache auf die Festplatte geschrieben werden müssen, sollte eine eigene Shutdown-Routine entworfen werden

Ablauf des Gesamtbeispiels

  • Das Gesamtbeispiel erstellt mit signal.NotifyContext einen Root-Context, der SIGINT und SIGTERM empfängt
  • Der Endpunkt /healthz gibt bei isShuttingDown == true HTTP 503 und Shutting down zurück, andernfalls OK
  • Der Beispiel-Request-Handler liefert nach 2 Sekunden Hello, world! zurück oder antwortet mit HTTP Request Timeout, wenn der Request-Context abgebrochen wird
  • Über BaseContext wird ongoingCtx verbunden, damit In-Flight-Requests nicht direkt nach SIGTERM abgebrochen werden
  • Nach Eingang des Beendigungssignals läuft die folgende Reihenfolge ab
    • Aufruf von stop(), um weitere Standardbehandlung zuzulassen
    • isShuttingDown.Store(true), um den Readiness-Status auf fehlschlagend zu setzen
    • Warten auf die Propagierung des Readiness Checks für 5 Sekunden über _readinessDrainDelay
    • Aufruf von server.Shutdown mit einem Zeitlimit von 15 Sekunden über _shutdownPeriod
    • Abbruch des laufenden Contexts über stopOngoingGracefully()
    • Falls Shutdown fehlschlägt, eine Wartezeit für erzwungenes Beenden von 3 Sekunden über _shutdownHardPeriod

1 Kommentare

 
GN⁺ 2025-05-06
Hacker-News-Kommentare
  • Ich bin schon einmal darauf hereingefallen, dass Kubernetes in manchen Konfigurationen länger als gedacht braucht, um die Ziel-IPs des Load Balancers zu aktualisieren. In meinem Fall bestanden 90 % des Graceful Shutdowns darin sicherzustellen, dass der Traffic vor dem Beenden des Pods tatsächlich gedraint wird.
    Ein 15-Sekunden-Sleep im globalen preStop-Hook hat die HTTP-503-Rate deutlich gesenkt und Zeit gewonnen, bis nach Beginn der Abmeldung vom Load Balancer SIGTERM an die Anwendung zugestellt wird. Dadurch wurde die Verarbeitung auf Anwendungsseite viel einfacher.

    • Stimmt. Ein preStop-Sleep ist eine Art magische Lösung, um bei hochwertigen Rolling Deployments SLOs einzuhalten.
      Ich sehe zwei Dinge, die Kubernetes verbessern könnte. Pods sollten zuerst aus den Endpoints entfernt werden, bevor die Beendigungssequenz startet, und es sollte eine Option für einen Termination Delay geben, ähnlich der Termination Grace. Außerdem sollte PDB eine Option haben, vor der Räumung eine Neuerstellung zu erlauben.
  • Wenn man einen üblichen Prometheus-/metrics-Endpoint alle N Sekunden scrapt, entsteht ein Zeitfenster, in dem Metriken, die zwischen dem letzten Scrape und dem tatsächlichen Prozessende aufgezeichnet wurden, nicht weitergegeben werden. Dadurch kann man einen falschen Eindruck davon bekommen, ob es während der Beendigungssequenz Fehler gab.
    Wenn man nicht aufpasst, kann man auch die Logs der letzten Sekunden vor dem Dienstende verlieren. Wenn zum Beispiel ein Sidecar wie Promtail oder Vector eine Logdatei überwacht und der Dienst beim Start denselben Pfad truncatet und danach wieder schreibt, entsteht eine Race Condition, bei der Logs während des Shutdowns verschwinden.

    • Observability-Stacks wirken etwas absurd. Logs, Metriken und Traces haben jeweils ihre eigene Datenbank, ihre eigenen Sidecars und ihren eigenen Visualisierungs-Stack, die Integrationsbibliotheken unterscheiden sich je nach Sprache, und die Cloud-Kosten sind ebenfalls enorm.
      Trotz dieses ganzen Aufwands wird der größte Teil der Daten völlig ignoriert, und Business-Insights sind selten deutlich besser als die Slum-Version, sich per ssh auf einen Server einzuloggen und Logdateien mit grep zu durchsuchen. Ich bin nicht sicher, ob der Aufwand, der in dieses Ökosystem geflossen ist, Uptime, Performance und Usability signifikant verbessert hat.
    • Die Probleme, die ich in den letzten über acht Jahren mit hoch belasteten Go-Anwendungen erlebt habe, behandeln wir in unserer Plattformbibliothek genau auf diese Weise. Es war bei jedem Unternehmen so etwas wie mein Hobby, Plattformen und Rolling Deployments zu entwickeln und zu verbessern.
      Dinge wie „Log-Synchronisierung“ und „warten, bis der Ingress den Liveness-Handler eingeholt hat“ werden wir behandeln.
      https://github.com/utrack/caisson-go/blob/main/caiapp/caiapp...
      https://github.com/utrack/caisson-go/tree/main/closer
      Die Dokumentation ist noch dürftig und es fehlt noch einiges, aber nach dem Urlaub plane ich den ersten Release. Am Ende soll es eine Meta-Plattform und eine Referenz-Plattformbibliothek für typische k8s/otel/grpc+http-Infrastruktur werden.
    • Ich habe nie verstanden, warum Prometheus und verwandte Tools ein Pull-Modell verwenden. Die meisten anderen nutzen doch ein Push-Modell.
    • Ich frage mich, ob jemand dafür schon eine bequeme Lösung gesehen hat. Wenn das Scrape-Intervall 15 Sekunden beträgt, kann man nicht 30 Sekunden warten, nur um Metriken zweimal aufzeichnen zu lassen.
      Wegen dieses Verhaltens verwenden unsere Dienste immer noch statsd, denn ein Push-basiertes Modell hat dieses Problem nicht.
  • Eine kleine Falle, die ich häufig sehe: Manche glauben, dass bei einem Aufruf von log.Fatal auch defer ausgeführt wird. Tatsächlich passiert das nicht.
    log.Fatal("fatal") ruft intern os.Exit auf, beendet also sofort, sodass defer nicht läuft. panic("fatal") hingegen zeigt sowohl fatal als auch in defer an.

  • Wenn ein verteiltes System darauf angewiesen ist, dass Clients graceful herunterfahren, damit es korrekt funktioniert, wird es früher oder später zwangsläufig heftig scheitern.

    • Dieser Überzeugung bin ich so stark, dass ich Graceful Shutdown beim Design gar nicht erst berücksichtige. Komponenten sollten sicher und sogar häufig hart abstürzen dürfen, und wenn ein relevanter Anteil des Systems wie vorgesehen läuft, sollte das keine nennenswerten Auswirkungen auf das Gesamtsystem haben.
      Die einzige Möglichkeit zu prüfen, ob ein System harte Abstürze von Komponenten aushält, besteht darin, harte Abstürze zu einem normalen, ständig auftretenden Ereignis zu machen. Ruhm dem Chaos Monkey.
    • Es gibt einen großen Unterschied zwischen Graceful Shutdown, um nett zu Clients oder Workflows zu sein, und der Tatsache, dass Clients darauf angewiesen sein müssen, damit das System funktioniert.
    • Früher, in Zeiten physischer Server, nutzte man dafür STONITH: https://smcleod.net/2015/07/delayed-serial-stonith/
    • Selbst bei wiederherstellbaren Situationen gibt es gute Gründe dafür, dass ein normaler Shutdown nicht wie ein katastrophaler Ausfall aussieht.
      Es ist ein großer Unterschied, ob eine Anwendung per sig int heruntergefahren oder per kill getötet wurde. Für eine Blue-Green-Migration braucht man zum Beispiel ein graceful Shutdown-Verhalten.
    • Stimmt. Trotzdem muss man beim Herunterfahren nicht wirklich den Stecker ziehen, nur weil die Software so entworfen wurde, dass sie es aushält, wenn man den Stecker zieht.
      Wenn ich noch einmal darüber nachdenke, vielleicht muss man es doch. Es könnte die einzige Möglichkeit sein, sicherzustellen, dass diese Annahme stimmt. So ähnlich wie Netflix’ Chaos Monkey vor ein paar Jahren.
  • Ich dachte, es würde darum gehen, wie eine neue Service-Instanz den Listening Socket von der bestehenden Instanz übernimmt und die Anwendung neu startet, ohne eine einzige eingehende Verbindung zu kappen.
    Mit systemd lässt sich das relativ einfach umsetzen, und nginx unterstützt es seit über 20 Jahren. Leider unterstützen Kubernetes und Docker das nicht, weil sie davon ausgehen, dass das im Load Balancer oder Reverse Proxy gehandhabt wird.

  • Mein Kollege sagte immer, ein Programm sei ein schlecht geschriebenes Programm, wenn es ctrl c und einige Beendigungsbefehle nicht sauber behandeln kann.

    • Ctrl-C ist fürs Kopieren in die Zwischenablage reserviert. Es zum Anhalten eines Programms zu verwenden, ist sehr kontraintuitiv und wird Nutzer verärgern.
  • Ich finde, Elixir handhabt solche Dinge wirklich clever. Ich habe zwar nicht besonders viel Erfahrung damit, aber weil kleine VM-Prozesse so entworfen sind, dass sie abstürzen, beendet und wieder erstellt werden, scheint es seltener nötig zu sein, absichtlich eine Graceful-Shutdown-Routine zu bauen.
    Diese Eigenschaft ist bereits in die Anwendungsarchitektur eingebaut.

    • Ich frage mich, wie genau das den Bedarf an graceful shutdown beseitigt, den der Autor behandelt.
  • Für mein Projekt habe ich eine kleine Bibliothek gebaut, um graceful shutdown zu handhaben: https://github.com/eberkund/graceful
    Meist gibt es ein paar Services, die gestartet werden müssen, und jeder hat oft eine andere Art, gestartet und beendet zu werden. Manchmal muss man zuerst ein Objekt instanziieren, manchmal gibt es einen Context, den man abbrechen möchte, und manchmal eine Stop-Methode, die aufgerufen werden muss. Ich habe sie so entworfen, dass all das an einer Stelle über eine einheitliche API zusammengeführt wird.

  • Ein Pod, der beendet wird, ist per Definition nicht bereit. Auch der Service markiert den Endpoint als terminating und not ready. Das passiert beim Wechsel in den Terminating-Zustand, daher muss man den Readiness Check nicht extra fehlschlagen lassen.
    Die genaue Reihenfolge von SIGTERM und Aktualisierungen von Objekten wie Pod.status oder Endpoint Slices kenne ich nicht. Es kann ein kleines Zeitfenster geben, in dem auch nach SIGTERM noch Verbindungen hereinkommen, aber kein großer Zeitraum „bis der Readiness Check fehlschlägt“, wie der Text nahelegt. Aus Sicht des Cluster-Betriebs halte ich dieses winzige Fenster für nicht besonders wichtig. Man sollte keine neuen Verbindungen annehmen, bestehende Verbindungen sauber schließen und in angemessen kurzer Zeit beenden. Allerdings gehört die Hälfte der Apps, mit denen ich zu tun habe, zu denen, die SIGTERM zwar behandeln, aber lange zum Beenden brauchen, oder zu denen, die SIGTERM gar nicht behandeln und trotzdem lange zum Beenden brauchen.

  • In einigen Projekten bei JustWatch haben wir Google Wire eingeführt, und das hat die Spielregeln verändert. Es ist überraschend wenig bekannt, hilft aber dabei, unordentliche Shutdown-Logik in Kubernetes loszuwerden.
    Wire erzwingt saubere Dependency Injection, sodass jetzt alles in einer festgelegten Reihenfolge herunterfährt, statt in einer unbekannten Reihenfolge.
    https://go.dev/blog/wire
    https://github.com/google/wire