BLUF — nehmen Sie keine neue Arbeit mehr an, setzen Sie Readiness auf false, arbeiten Sie bereits angenommene Anfragen innerhalb eines ausdrücklichen Zeitbudgets ab und dokumentieren Sie offene Ergebnisse. Der Abbruch einer wartenden asyncio-Coroutine beweist nicht, dass ihr Thread, die Inferenz-Engine oder eine entfernte Operation beendet wurde. Ein Shutdown besteht erst dann, wenn Anfrageergebnisse und Backend-Arbeit getrennt nachvollziehbar sind – nicht schon dann, wenn der Prozess aus dem Dashboard verschwindet.
Der Beitrag enthält einen ausgeführten lokalen ASGI-Test mit kurzer Arbeit, einem unvollständigen Stream und einem blockierenden Thread. Er prüft eine Prozessgrenze, keine Kubernetes-, GPU- oder Load-Balancer-Abnahme. Das Betriebsziel ergänzt die SLOs für Inferenz: Eine durch das Deployment unterbrochene Anfrage bleibt Teil des Zuverlässigkeitsnenners.
Definieren Sie zwei Register vor dem Timeout
Verfolgen Sie jede angenommene Anfrage bis zu abgeschlossen, fehlgeschlagen, abgebrochen oder zum Beobachtungsende weiterhin offen. Halten Sie vor der Annahme abgelehnte Anfragen separat. Eine abgeschlossene Antwort muss den Anwendungsvertrag erfüllen; ein HTTP-200-Header am Anfang eines Streams reicht nicht. Verwenden Sie eine Abschlussmarkierung oder eine gleichwertige validierte Protokollgrenze, nicht allein das Schließen der Verbindung.
Verfolgen Sie die Backend-Ausführung unabhängig davon: nicht gestartet, läuft, bestätigt gestoppt, abgeschlossen oder unbekannt. Zu einer abgebrochenen Anfrage kann weiterhin laufende Backend-Arbeit gehören. Addieren Sie diese beiden Register nicht wie gegenseitig ausschließende Kategorien. Metriken benötigen begrenzte Anfrageklassen und eine Release-Kennung; Korrelationskennungen gehören in zugriffsgeschützte Traces statt in hochkardinale Metriklabels. Protokollieren Sie keine Prompts, nur um offene Arbeit zu zählen.
Die Uvicorn-Dokumentation zum Serververhalten erklärt, dass Status und Header beim Beginn der ASGI-Antwort gesendet werden. Eine spätere Exception kann den Status nicht nachträglich durch einen Fehler ersetzen. Deshalb kann eine unauffällige HTTP-Statusauswertung abgeschnittene Generierungen verbergen.
Eine Zustandsmaschine mit klarer Verantwortung
BETRIEB → ANNAHMESTOPP → ABARBEITUNG → DOKUMENTATION → ENDE. Im Betrieb gelten die normalen Annahmelimits. Beim Annahmestopp schließt die Anwendung ihre Zugangsschranke und meldet Readiness false. Während der Abarbeitung darf die bereits angenommene Kohorte ihre verbleibenden Fristen nutzen. Die Dokumentation hält abschließende Anfrageergebnisse und ungeklärte Backend-Zustände innerhalb eines begrenzten Aufräumbudgets fest. Zum Ende werden Ressourcen freigegeben oder offene dauerhafte Aufgaben ihrem festgelegten Verantwortlichen übergeben.
Das ist ein vorgeschlagener Anwendungsvertrag, kein Kubernetes-Enum. Führen Sie Annahmeentscheidung und Registrierung laufender Arbeit atomar im zuständigen Event Loop oder einem geschützten Register aus. Sonst kann eine Anfrage unmittelbar vor dem Annahmestopp passieren, ohne in der Drain-Zählung zu erscheinen. Bei mehreren Worker-Prozessen benötigt jeder Worker die Sperre; ein prozesslokales Boolean ist keine Sperre für die gesamte Flotte.
Liveness und Readiness beantworten unterschiedliche Fragen. Ein abarbeitender Prozess kann leben und Aufräumarbeiten durchführen, ohne für neue Anfragen bereit zu sein. Verwenden Sie absichtlich fehlschlagende Liveness-Prüfungen nicht als Drain-Mechanismus. Umgekehrt ist Readiness false keine sofortige Annahmesperre: Bestehende Verbindungen und verzögerte Routing-Updates können weitere Anfragen zustellen. Lehnen Sie diese gemäß dem Schnittstellenvertrag ab, bevor neue Inferenz beginnt. Verwenden Sie das bestehende Retry-Budget, statt eine unkontrollierte Wiederholungswelle auszulösen.
Zeitablauf: Die äußere Frist umfasst den Hook
Die Kubernetes-Dokumentation zum Pod-Lebenszyklus beschreibt Termination-Hooks, Signale und die spätere erzwungene Beendigung. Die Termination Grace Period umfasst die preStop-Ausführung. Sie beginnt nicht nach dem Hook erneut. Endpoint-Updates laufen neben dem lokalen Shutdown; sie bilden keine garantierte synchrone Barriere vor der nächsten Anfrage.
Zeitskizze — Pod-Beendigung beginnt → preStop schließt die Annahme und leitet Readiness-Rücknahme ein → TERM erreicht den Server → begrenzte Abarbeitung → begrenztes Aufräumen und Ergebnisdokumentation → Prozessende. Parallel verbreiten sich EndpointSlice- und Proxy-Routing-Updates. Die äußere Frist kann die gesamte Folge abschneiden. Die Skizze zeigt die Ordnung, keinen gemessenen Cluster-Trace und keine Zusage, dass jeder Proxy das Update vor TERM sieht.
Kopieren Sie keine feste Wartezeit in preStop und behandeln Sie sie als Propagationsnachweis. Messen Sie nach der Rücknahme die letzte eintreffende Anfrage über Ihren tatsächlichen Ingress einschließlich Verbindungswiederverwendung. Falls eine begrenzte Propagationsreserve erforderlich ist, rechnen Sie sie in dasselbe äußere Budget ein und behalten Sie die anwendungsseitige Ablehnung bei. Schützen Sie Drain-Endpunkte durch eine lokale oder authentifizierte Verwaltungsgrenze; die nur auf localhost erreichbare Testroute ist keine öffentliche Administrations-API.
| Zeitlimit | Verantwortung und Grenze | Keine Garantie für |
|---|---|---|
| Pod-Beendigungsfrist | Kubelet/Runtime; umfasst preStop | Erfolgreichen Abschluss vor Zwangsende |
| preStop-Dauer | Verwaltungs-Hook; verbraucht äußeres Budget | Sofortige Propagation zu jedem Proxy |
| Server Graceful Timeout | ASGI-Server; Warten auf Verbindungen/Tasks | Begrenztes Lifespan-Cleanup oder Thread-Ende |
| Anfragefrist | Aufrufer/Anwendung; verbleibender Nutzen | Backend-Abbruch nach Fristablauf |
| Worker-Lebensdauer | Worker-Verwaltung oder entfernter Dienst | Kopplung an die wartende Coroutine |
Planen Sie die äußere Frist für gemessene Hook-Dauer, Server-Drain, begrenztes Anwendungs-Cleanup und eine ausdrückliche Sicherheitsreserve. Das ist eine Planungsbedingung, keine exakte Gleichung: Laufzeit und Scheduling erzeugen Zusatzaufwand, parallele Phasen dürfen nicht doppelt gezählt werden. Die Anfragefrist gilt auch beim Shutdown. Sie nur für einen erfolgreichen Rollout zu verlängern, kann den Vertrag gegenüber dem Aufrufer verletzen.
In der versionsgebundenen Uvicorn-Implementierung begrenzt der Graceful Timeout das Warten auf Verbindungen und Tasks. Danach folgt der Lifespan-Shutdown. Dieser Timeout allein begrenzt daher weder jede Aufräumoperation noch zwingend die gesamte Prozesslebensdauer. Begrenzen Sie das Anwendungs-Cleanup ausdrücklich und behalten Sie die äußere Container-Terminierungsgrenze bei.
Ergebnisse des ausgeführten lokalen Tests
Der Lauf verwendete Python 3.14.9, Uvicorn 0.41.0, asyncio und HTTP/1.1 über h11. Das sind Reproduktionsversionen, keine Empfehlung der jeweils neuesten Version. Der Server band einen dynamischen localhost-Port. Er nahm drei Anfragen an: eine Coroutine mit 0,2 Sekunden Laufzeit, einen Stream mit einem Chunk alle 0,1 Sekunden für höchstens 100 Durchläufe und synthetische blockierende Arbeit mit geplanter Dauer von vier Sekunden über asyncio.to_thread.
Der Test wartete auf den Abschluss der kurzen Anfrage und den Start des Threads und rief anschließend die lokale Drain-Route auf. Er prüfte die Ablehnung einer späteren Anfrage, einen HTTP-Fehler bei Readiness und weiterhin HTTP 200 bei Liveness. Danach sendete er echtes SIGTERM an den eigenen Kindprozess; der Graceful Timeout betrug eine Sekunde. Das Lifespan-Cleanup wartete absichtlich 0,2 Sekunden, damit Thread-Aktivität nach dem Coroutine-Abbruch beobachtet werden konnte. Diese Zeiten sind Testreize, keine Produktionseinstellungen.
| Beobachtung | Anzahl | Bedeutung |
|---|---|---|
| Angenommene Anfragen | 3 | Kurze Arbeit, Stream und Thread |
| Abgeschlossene Anfragen | 1 | Kurze Antwort vor Drain abgeschlossen |
| Abgebrochene Anfrage-Handler | 2 | Stream und wartende Thread-Coroutine |
| Vor Annahme abgelehnt | 1 | Späte Anfrage startete keine Arbeit |
| Ausgeführte finally-Blöcke | 3 | Cleanup-Eintritt, kein Abschlussbeweis |
| Offene Backend-Arbeit beim Lifespan-Shutdown | 1 | Thread-Heartbeat nach Coroutine-Abbruch |
Der kurze Client erhielt eine vollständige 200-Antwort. Der Streaming-Client meldete IncompleteRead; die Thread-Anfrage erhielt einen HTTP-Fehler. Alle drei angenommenen Handler führten ihre finally-Blöcke aus. Das belegt den Eintritt in das instrumentierte Cleanup, nicht erfolgreiche dauerhafte Speicherung oder Backend-Abbruch.
Der Thread schrieb noch 0,196 Sekunden nach dem Abbruch seiner wartenden Coroutine ein Heartbeat-Ereignis. Ein Worker-fertig-Ereignis wurde nicht aufgezeichnet. Beim Lifespan-Shutdown war eine Backend-Operation weiterhin offen. Der Kindprozess endete anschließend mit Rückgabecode -15: Der Signalhandler der geprüften Uvicorn-Version stellt den vorherigen Handler wieder her und löst SIGTERM nach dem Shutdown erneut aus. Deuten Sie diesen Befund weder als erfolgreichen Thread-Abschluss noch als allgemeine Aussage über andere Prozessmanager.
Das JSONL-Register enthält monotone Ereigniszeit, Ereignistyp und synthetische Route. Seine Bilanzprüfung verlangt drei angenommene Anfragen, davon eine abgeschlossen und zwei abgebrochen, sowie eine separat abgelehnte späte Anfrage. Das Backend-Register bleibt getrennt. Der Test enthält keine externen Schreiboperationen, keinen Produktionsverkehr und keine native GPU-Arbeit. Ein entfernter Anbieter könnte auch nach dem Ende dieses lokalen Prozesses weiterarbeiten.
Abbruch verlangt Kooperation
Die asyncio-Dokumentation von Python beschreibt die Zustellung von CancelledError bei einer Gelegenheit zur Fortsetzung des Tasks. Verwenden Sie try/finally zum Aufräumen und reichen Sie den Abbruch danach normalerweise weiter. Verschluckte Abbrüche können Timeout- und Structured-Concurrency-Verhalten stören. Die Verlagerung blockierender Arbeit in einen Thread hält den Event Loop reaktionsfähig, erzeugt aber kein Protokoll zum Beenden dieses Threads.
Implementieren Sie für eigene Worker ein kooperatives Stoppsignal, das zwischen begrenzten Arbeitseinheiten geprüft und bestätigt wird. Testen Sie bei nativen Bibliotheken die tatsächliche Abbruch-API und sichere Unterbrechungspunkte. Trennen Sie bei entfernten Aufrufen Verbindungsabbruch, angeforderten Abbruch und vom empfangenden System bestätigten Abbruch. Ein überwachter Worker-Prozess bietet bei notwendiger harter Isolation eine Beendigungsgrenze, verursacht aber IPC-, Start- und Wiederherstellungsaufwand. Auch sein erzwungenes Ende nimmt keine entfernte Nebenwirkung zurück.
| Beobachtete Grenze | Entscheidung | Erforderlicher Nachweis |
|---|---|---|
| Neue Anfrage nach Annahmestopp | Vor Arbeitsbeginn ablehnen | Sperrergebnis und kein Backend-Start |
| Kurze angenommene Arbeit innerhalb Frist | Begrenzten Abschluss zulassen | Vollständige validierte Antwort |
| Stream nach 200-Header abgeschnitten | Unvollständig statt erfolgreich | Client-Protokollergebnis und Abschlussmarker |
| Coroutine abgebrochen; Worker läuft | Backend-Zustand offen halten | Stoppbestätigung oder Abschlussbeleg |
| Ergebnis entfernter Schreiboperation unklar | Abgleichen; nicht blind wiederholen | Verbindliches Ergebnis des Zielsystems |
Abnahmecheckliste für das tatsächliche Deployment
1. Wiederholen Sie den lokalen Test mit Ihrem wirklichen Server-Entrypoint und Prozessmanager. Prüfen Sie normalen Abschluss, Fristablauf, Client-Verbindungsabbruch und wiederholte Terminationssignale. Bewahren Sie Client-Ergebnisse und serverseitige Ergebnisbelege auf; ein Exit-Code null reicht nicht.
2. Wiederholen Sie den Test über Staging-Ingress und Kubernetes-Beendigung. Beziehen Sie persistente Verbindungen, lange Streams, mehrere Worker-Prozesse und Routing-Updates ein. Jede neue Anfrage muss entweder vor der Grenze angenommen oder ohne Backend-Start abgelehnt werden. Messen Sie die Endpoint-Propagation statt einer universellen Verzögerung zu vertrauen.
3. Testen Sie einen Worker, der kooperativen Abbruch ignoriert. Halten Sie seinen Zustand offen, bis ein Verantwortlicher Beendigung oder Abschluss bestätigt. Reservieren Sie Kapazität für Ersatzinstanzen und weiterlaufende Arbeit. Längeres Abarbeiten kann Unterbrechungen reduzieren, belegt aber länger Ressourcen und kann Rollouts verzögern.
4. Injizieren Sie einen Ausfall des Ergebnisspeichers. Begrenzen Sie Wiederholungen der Speicherung und melden Sie fehlende Bilanznachweise ausdrücklich. Kann der Prozess vor dem letzten Ereignis getötet werden, benötigen Sie ein externes Annahmeregister und einen Lease-/Abgleichmechanismus. Restore-Tests behandeln Wiederherstellung nach Zustandsverlust; sie ersetzen diesen Test der laufenden Beendigung nicht.
5. Stoppen Sie den Rollout, wenn unvollständige Streams, das Alter der ältesten offenen Anfrage oder ungeklärte Operationen die vorab definierte Release-Regel verletzen. Entfernen Sie diese Anfragen nicht durch ein verkürztes Beobachtungsfenster. Leiten Sie Schwellen aus Dienstverträgen und Geschäftsrisiko ab, nicht aus der kleinen synthetischen Stichprobe dieses Beitrags.
Der nächste sinnvolle Liefergegenstand
Bringen Sie für ein Shutdown-Review Ihres KI-Dienstes den tatsächlichen Entrypoint, die Termination-Einstellungen, einen unterbrochenen Stream und eine Operation mit unklarem Abbruchergebnis mit. Das Ergebnis sollte eine Zuständigkeitsmatrix für Zeitlimits, ein reproduzierbarer Drain-Test und ein Ergebnisregister sein, das beendetes Warten von beendeter Arbeit trennt. Das ist ein belastbareres Freigabekriterium als ein pauschales Versprechen unterbrechungsfreien Betriebs.


