Gunicorn: Klären Sie, was/wie Timeout und Graceful_timeout funktionieren

Erstellt am 3. Apr. 2017  ·  30Kommentare  ·  Quelle: benoitc/gunicorn

(Entschuldigung für den Monolog hier: Einfache Dinge wurden kompliziert und ich habe am Ende den Stapel durchwühlt. Hoffentlich ist das, was ich dokumentiert habe, jedoch für den Leser hilfreich.)

Wie ich verstanden habe, standardmäßig:

  • Nach 30 Sekunden (konfigurierbar mit timeout ) der Anforderungsverarbeitung sendet der Gunicorn-Masterprozess SIGTERM an den Arbeitsprozess, um einen ordnungsgemäßen Neustart einzuleiten.
  • Wenn der Worker während weiterer 30 Sekunden (konfigurierbar mit graceful_timeout ) nicht herunterfährt, sendet der Master-Prozess SIGKILL . Scheint, als würde dieses Signal auch gesendet, wenn der Worker während des Zeitraums graceful_timeout ordnungsgemäß heruntergefahren wird (https://github.com/benoitc/gunicorn/commit/d1a09732256fa8db900a1fe75a71466cf2645ef9).

Die Fragen:

  • Sind die Signale korrekt?
  • Was passiert eigentlich, wenn Gunicorn (Sync) Worker diese Signale empfängt? Wie teilt es der WSGI-App mit, dass das Signal abgefangen wurde und etwas passieren sollte (ok, ich nehme an, es "leitet es nur weiter")?
  • Wie verarbeitet Flask beispielsweise das Signal SIGTERM - was passiert in der Praxis während der Anfrageverarbeitung? Setzt es nur ein Flag für die WSGI-Anwendung (auf Werkzeugebene), dass es heruntergefahren werden soll, nachdem die Anforderungsverarbeitung abgeschlossen ist? Oder wirkt sich SIGTERM bereits irgendwie auf die laufende Anforderungsverarbeitung aus - IO-Verbindungen beenden oder etwas, um die Anforderungsverarbeitung zu beschleunigen ...?

Bei SIGKILL wird die Anfrageverarbeitung wohl einfach zwangsweise abgebrochen.

Ich könnte eine kleine PR einreichen, um die Dokumentation darüber zu verbessern, wenn ich verstehe, wie die Dinge tatsächlich funktionieren.

Discussion Documentation

Hilfreichster Kommentar

@tuukkamustonen --timeout ist nicht als Anforderungs-Timeout gedacht. Es ist als Liveness-Check für Arbeiter gedacht. Für Sync-Worker fungiert dies als Anforderungs-Timeout, da der Worker nichts anderes tun kann, als die Anforderung zu verarbeiten. Die asynchronen Worker schlagen sogar während sie lang andauernde Anfragen bearbeiten, sodass sie nicht getötet werden, es sei denn, der Worker blockiert/friert ein.

Vielleicht wäre es eine gute Idee für uns, den Namen zu ändern, wenn andere Leute das verwirrend finden.

Alle 30 Kommentare

Hmm, ich denke, https://github.com/benoitc/gunicorn/issues/1236#issuecomment -254059927 bestätigt meine Annahmen darüber, dass SIGTERM den Worker einfach nach Abschluss der Anfrageverarbeitung herunterfahren lässt (und den Worker so einstellt, dass er nicht akzeptiert alle neuen Verbindungen).

Scheint so, als ob ich timeout und graceful_timeout falsch interpretiert habe. Beide Zeiträume beziehen sich tatsächlich auf die Zeit zu Beginn der Anfragebearbeitung. Da beide Einstellungen auf 30 Sekunden gesetzt sind, ist standardmäßig kein ordnungsgemäßer Neustart aktiviert. Wenn ich so etwas wie --graceful-timeout 15 --timeout 30 mache, sollte das bedeuten, dass nach 15 Sekunden ein ordnungsgemäßer Neustart eingeleitet wird und der Worker nach 30 Sekunden zwangsweise getötet wird, wenn die Anforderung vorher nicht abgeschlossen wurde.

Es scheint jedoch so, als ob die Antwort zwischen graceful_timeout und timeout zurückgegeben wird, dann wird der Worker doch nicht neu gestartet? Sollte es nicht?

Ich habe von app.py getestet:

import time
from flask import Flask

app = Flask(__name__)

@app.route('/foo')
def foo():
    time.sleep(3)
    return 'ok'

Dann:

12:51 $ gunicorn app:app --timeout 5 --graceful-timeout 1
[2017-04-03 12:51:37 +0300] [356] [INFO] Starting gunicorn 19.6.0
[2017-04-03 12:51:37 +0300] [356] [INFO] Listening at: http://127.0.0.1:8000 (356)
[2017-04-03 12:51:37 +0300] [356] [INFO] Using worker: sync
[2017-04-03 12:51:37 +0300] [359] [INFO] Booting worker with pid: 359

Dann sende ich curl localhost:8000/foo , was nach 3 Sekunden zurückkehrt. Aber in Gunicorn passiert nichts - ich sehe keine Spur von einem ordnungsgemäßen Neustart, der eingeleitet wurde oder passiert ist?

Es scheint, dass auf timeout SystemExit(1,) geworfen wird, wodurch die aktuelle Anfrageverarbeitung in Flask abgebrochen wird. Welcher Code oder welches Signal es erzeugt, kann ich nicht sagen.

Diese Ausnahme wird durch den Flask-Stack geworfen, und alle teardown_request -Handler fangen sie ab. Es ist genug Zeit, um etwas zu loggen, aber wenn Sie time.sleep(1) oder etwas anderes zeitraubendes im Handler tun, wird es stillschweigend beendet. Es ist, als ob 100-200 ms Zeit vergingen, bevor der Prozess tatsächlich zwangsweise beendet wird, und ich frage mich, was diese Verzögerung ist. Es ist kein elegantes Timeout, diese Einstellung hat keinen Einfluss auf die Verzögerung. Ich würde erwarten, dass der Prozess nur gewaltsam an Ort und Stelle beendet wird, anstatt zu sehen, wie SystemExit durch den Stapel geworfen werden, aber den Prozess dann möglicherweise trotzdem in der Luft beenden.

Tatsächlich sehe ich nicht, dass graceful_timeout irgendetwas tut - vielleicht wird es für Sync-Worker nicht unterstützt, oder es funktioniert nicht "stand-alone" (oder zusammen mit timeout ) - nur wenn Sie manuell SIGTERM senden?

Was auch seltsam sein könnte, ist, dass https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L392 das Flag graceful überhaupt nicht überprüft. Ich denke, https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L390 stellt sicher, dass self.WORKERS leer ist, sodass auf ein ordnungsgemäßes Timeout nicht gewartet wird, wenn ein nicht ordnungsgemäßer Stopp durchgeführt wird.

@benoitc @tilgovi Möchtest du hier helfen? Hoffentlich machen meine obigen Ausführungen Sinn...

@tuco86 Das graceful timeout ist nur verfügbar, wenn Sie den Arbiter beenden, aktualisieren (USR2), ein HUP-Signal an den Arbiter senden oder ein QUIT-Signal an den Worker senden. Das heißt, es wird nur verwendet, wenn die Aktion normal ist

Das Timeout soll verhindern, dass beschäftigte Mitarbeiter andere Anfragen blockieren. Wenn sie den Arbiter nicht innerhalb timeout benachrichtigen, wird der Worker einfach verlassen und die Verbindung mit dem Client geschlossen.

Äh, in Ordnung. Hat timeout Auswirkungen, wenn Sie:

Beenden Sie den Arbiter, aktualisieren Sie ihn (USR2), senden Sie ein HUP-Signal an den Arbiter oder senden Sie ein QUIT-Signal an den Worker

Ich meine, was ist, wenn der Arbeiter in graceful_timeout nicht herunterfährt - wird danach timeout einspringen und Arbeiter werden gewaltsam getötet, oder bleibt es dem Benutzer überlassen, SIGQUIT zu verlangen

QUIT-Signal an den Arbeiter

Ich nehme an, Sie meinten hier TERM (da QUIT sowohl für Master als auch für Worker als _schnelles Herunterfahren_ dokumentiert ist)?

Wenn der Worker während der Graceful Time nicht heruntergefahren wird, wird er ohne weitere Verzögerung getötet.

Na sicher. Danke für die Aufklärung!

@benoitc Frage im Zusammenhang mit diesem alten Ticket - was bedeutet der letzte Satz in der timeout -Dokumentation eigentlich?

Im Allgemeinen auf 30 Sekunden eingestellt. Stellen Sie dies nur merklich höher ein, wenn Sie sich der Auswirkungen auf Sync-Worker sicher sind. Für die nicht synchronisierten Worker bedeutet dies lediglich, dass der Worker-Prozess immer noch kommuniziert und nicht an die Zeitdauer gebunden ist, die für die Bearbeitung einer einzelnen Anfrage erforderlich ist.

Da ich kein englischer Muttersprachler bin, fällt es mir schwer, dies zu verstehen. Bedeutet dies, dass timeout nicht für nicht synchronisierte Worker unterstützt wird (weil ich das anscheinend beobachte: Ich verwende gthread Worker und das Timeout tritt nicht ein und beendet zu langsame Anfragen )?

@tuukkamustonen --timeout ist nicht als Anforderungs-Timeout gedacht. Es ist als Liveness-Check für Arbeiter gedacht. Für Sync-Worker fungiert dies als Anforderungs-Timeout, da der Worker nichts anderes tun kann, als die Anforderung zu verarbeiten. Die asynchronen Worker schlagen sogar während sie lang andauernde Anfragen bearbeiten, sodass sie nicht getötet werden, es sei denn, der Worker blockiert/friert ein.

Vielleicht wäre es eine gute Idee für uns, den Namen zu ändern, wenn andere Leute das verwirrend finden.

@tilgovi timeout ist in Ordnung, obwohl etwas wie worker_timeout aussagekräftiger sein könnte. Ich war anfangs nur verwirrt, weil timeout und graceful_timeout in der Dokumentation nebeneinander deklariert sind, also nahm mein Gehirn an, dass sie eng miteinander verbunden sind, obwohl sie es eigentlich nicht sind.

Für Sync-Worker fungiert dies als Anforderungs-Timeout, da der Worker nichts anderes tun kann, als die Anforderung zu verarbeiten. Die asynchronen Worker schlagen sogar während sie lang andauernde Anfragen bearbeiten, sodass sie nicht getötet werden, es sei denn, der Worker blockiert/friert ein.

Hätten Sie ein Beispiel dafür, wann timeout bei nicht synchronisierten Workern ansetzt? Ist es etwas, das eigentlich nie passieren sollte – vielleicht nur, wenn es einen Fehler gibt, der dazu führt, dass der Worker blockiert/einfriert?

Das ist richtig. Ein asynchroner Worker, der auf einen Ereignisschleifenkern angewiesen ist, führt möglicherweise eine CPU-intensive Prozedur aus, die innerhalb des Timeouts nicht nachgibt.

Mit anderen Worten, nicht nur ein Fehler. Manchmal kann es jedoch auf einen Fehler hinweisen, z. B. einen Aufruf einer blockierenden E/A-Funktion, wenn ein Asyncio-Protokoll angemessener wäre.

In einer CPU-intensiven Aufgabe stecken zu bleiben, ist ein gutes Beispiel, danke.

Das Aufrufen von blockierender E/A in asynchronem Code ist auch eine, aber ich bin mir nicht sicher, wie es auf diesen Kontext zutrifft - ich führe eine traditionelle Flask-App mit blockierendem Code aus, aber führe sie mit einem asynchronen Worker aus ( gthread ) ohne Affen-Patching. Und es funktioniert ganz gut. Ich weiß, dass dies nicht mehr wirklich im Zusammenhang mit diesem Ticket steht, aber verursacht das Mischen und Abgleichen von Async/Sync-Code wie diesem nicht Probleme?

Und was ist das Heartbeat-Intervall? Was wäre ein sinnvoller Wert für timeout mit nicht synchronisierten Workern?

Der Gthread-Worker ist nicht asynchron, aber er hat einen Haupt-Thread für den Heartbeat, sodass es auch nicht zu einem Timeout kommt. Im Fall dieses Workers werden Sie wahrscheinlich keine Zeitüberschreitung sehen, es sei denn, der Worker ist sehr überlastet, oder, was wahrscheinlicher ist, Sie rufen ein C-Erweiterungsmodul auf, das die GIL nicht freigibt.

Sie müssen das Timeout wahrscheinlich nicht ändern, es sei denn, Sie sehen Worker-Timeouts.

In Ordung. Nur noch eine Sache:

Der gthread-Worker ist nicht asynchron

Es mag etwas verwirrend sein, dass der Worker gthread nicht async ist, sondern unter http://docs.gunicorn.org/en/stable/design.html#asyncio -workers als „AsyncIO“-Worker aufgeführt wird. Abgesehen davon ist für die Verwendung von "Threads" kein Asyncio erforderlich, was auch beim Leser Fragen aufwirft. Ich sage das nur aus der Perspektive eines naiven Benutzers, ich bin mir sicher, dass alles technisch gut begründet ist.

Kurz gesagt, der Worker gthread wird mit der asyncio implementiert, erzeugt aber Threads, um den Synchronisierungscode zu verarbeiten. Korrigieren Sie mich, wenn falsch.

Schön, dass du gefragt hast!

Der Thread-Worker verwendet kein asyncio und erbt nicht von der asynchronen Basis-Worker-Klasse.

Wir sollten die Dokumentation klären. Ich denke, es wurde möglicherweise als asynchron aufgeführt, da das Worker-Timeout gleichzeitig behandelt wird, sodass es sich in Bezug auf die Fähigkeit, lange Anforderungen und gleichzeitige Anforderungen zu verarbeiten, eher wie die asynchronen Worker als wie der Sync-Worker verhält.

Es wäre großartig, die Dokumentation zu klären und alle Arbeiter genauer zu beschreiben.

Ja, der Gthreads-Worker sollte nicht im Asyncio-Worker aufgeführt sein. Vielleicht ist es besser, einen Abschnitt zu haben, der das Design der einzelnen Arbeiter beschreibt?

Öffnen Sie dies erneut, damit wir es als Arbeit verfolgen können, um den Abschnitt zu Arbeitertypen und Zeitüberschreitungen zu klären.

@tilgovi

--timeout ist nicht als Anforderungs-Timeout gedacht. Es ist als Liveness-Check für Arbeiter gedacht. Für Sync-Worker fungiert dies als Anforderungs-Timeout, da der Worker nichts anderes tun kann, als die Anforderung zu verarbeiten. Die asynchronen Worker schlagen sogar während sie lang andauernde Anfragen bearbeiten, sodass sie nicht getötet werden, es sei denn, der Worker blockiert/friert ein.

Gibt es eine Option für das Anforderungszeitlimit für asynchrone Worker? Mit anderen Worten, wie kann man einen Arbiter dazu bringen, einen Arbeiter zu töten, der eine Anfrage nicht innerhalb einer bestimmten Zeit bearbeitet hat?

@aschatten gibt es leider nicht. Siehe auch #1658.

Töte einen Worker, der eine Anfrage nicht innerhalb einer bestimmten Zeit verarbeitet hat

Da ein Worker möglicherweise mehrere Anfragen gleichzeitig verarbeitet, klingt das Töten eines ganzen Workers, weil bei einer Anfrage das Zeitlimit überschritten wird, ziemlich extrem. Würde das nicht dazu führen, dass alle anderen Anfragen vergeblich getötet werden?

Ich erinnere mich, dass uWSGI plante, Thread-basiertes Töten in 2.1 oder so einzuführen, obwohl wahrscheinlich sogar das nur für Sync/Threaded-Worker gilt (und meine Erinnerung daran ist vage).

Da ein Worker möglicherweise mehrere Anfragen gleichzeitig verarbeitet, klingt das Töten eines ganzen Workers, weil bei einer Anfrage das Zeitlimit überschritten wird, ziemlich extrem. Würde das nicht dazu führen, dass alle anderen Anfragen vergeblich getötet werden?

Der Ansatz kann derselbe sein wie für max_request , wobei es für jeden Worker-Typ eine separate Implementierung gibt.

Wir arbeiten diese Woche an einer Veröffentlichung, zu diesem Zeitpunkt _kann_ es an der Zeit sein, für R20 zu verzweigen, wo wir planen, einige wichtige Dinge in Angriff zu nehmen. Das könnte der richtige Zeitpunkt sein, um das aktuelle Timeout in ein angemessenes Request-Timeout für jeden Worker-Typ umzuwandeln.

Kommentieren Sie hier, anstatt ein separates Problem einzureichen, da ich versuche zu verstehen, wie Timeout funktionieren soll, und ich bin mir nicht sicher, ob dies ein Fehler ist oder nicht.

Das IMO unerwartete Verhalten, das ich sehe, ist folgendes:

Jede Anfrage von max-requests (diejenige, nach der der Worker neu gestartet wird) hat ein Timeout, während die anderen Anfragen erfolgreich abgeschlossen werden. Im folgenden Beispiel werden 4 Anfragen ausgeführt, die Anfragen 1, 2 und 4 sind erfolgreich, während Anfrage 3 fehlschlägt.

Relevante Konfiguration:

  • gthread-Arbeiter
  • Die Bereitstellungsanforderung dauert länger als das Zeitlimit
  • max-requests ist ungleich Null
import time

def app(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    time.sleep(5)
    return [b"Hello World\n"]

Gunicorn:

gunicorn --log-level debug -k gthread -t 4 --max-requests 3 "app:app"
...
[2018-02-08 10:11:59 +0200] [28592] [INFO] Starting gunicorn 19.7.1
[2018-02-08 10:11:59 +0200] [28592] [DEBUG] Arbiter booted
[2018-02-08 10:11:59 +0200] [28592] [INFO] Listening at: http://127.0.0.1:8000 (28592)
[2018-02-08 10:11:59 +0200] [28592] [INFO] Using worker: gthread
[2018-02-08 10:11:59 +0200] [28595] [INFO] Booting worker with pid: 28595
[2018-02-08 10:11:59 +0200] [28592] [DEBUG] 1 workers
[2018-02-08 10:12:06 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:11 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:15 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:20 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:23 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:23 +0200] [28595] [INFO] Autorestarting worker after current request.
[2018-02-08 10:12:27 +0200] [28592] [CRITICAL] WORKER TIMEOUT (pid:28595)
[2018-02-08 10:12:27 +0200] [28595] [INFO] Worker exiting (pid: 28595)
[2018-02-08 10:12:28 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:28 +0200] [28599] [INFO] Booting worker with pid: 28599
[2018-02-08 10:12:32 +0200] [28599] [DEBUG] GET /
[2018-02-08 10:12:37 +0200] [28599] [DEBUG] Closing connection.
^C[2018-02-08 10:12:39 +0200] [28592] [INFO] Handling signal: int

Klient:

[salonen<strong i="19">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World
[salonen<strong i="20">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World
[salonen<strong i="21">@mac</strong> ~]$ curl http://127.0.0.1:8000
curl: (52) Empty reply from server
[salonen<strong i="22">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World

was soll da geplant sein? Ich habe folgendes im Sinn:

  • [ ] Worker-Beschreibung aktualisieren (falls noch benötigt)
  • [ ] Dokumentieren Sie das Protokoll, um tote oder blockierte Arbeiter zu erkennen

Soll es 20.0 sein oder könnten wir es verschieben?

verschieben.

Hey, das wird also nicht Teil von 20.0 sein?

Das könnte der richtige Zeitpunkt sein, um das aktuelle Timeout in ein angemessenes Request-Timeout für jeden Worker-Typ umzuwandeln.

geklärt. @ lucas03 Es ist unklar, was für ein Anforderungs-Timeout dort ist. Bitte öffnen Sie ein Ticket, wenn Sie etwas Bestimmtes benötigen?.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen