Gunicorn: Clarifier ce que/comment le délai d'attente et graceful_timeout fonctionnent

CrĂ©Ă© le 3 avr. 2017  Â·  30Commentaires  Â·  Source: benoitc/gunicorn

(Désolé pour le monologue ici : les choses simples se sont compliquées et j'ai fini par fouiller dans la pile. J'espÚre que ce que j'ai documenté est utile pour le lecteur, cependant.)

Si j'ai bien compris, par défaut :

  • AprĂšs 30 secondes (configurable avec timeout ) de traitement de la requĂȘte, le processus maĂźtre gunicorn envoie SIGTERM au processus de travail, pour initier un redĂ©marrage progressif.
  • Si le travailleur ne s'arrĂȘte pas pendant encore 30 secondes (configurable avec graceful_timeout ), le processus maĂźtre envoie SIGKILL . Il semble que ce signal soit Ă©galement envoyĂ© lorsque le travailleur _does_ gracieusement arrĂȘtĂ© pendant la pĂ©riode graceful_timeout (https://github.com/benoitc/gunicorn/commit/d1a09732256fa8db900a1fe75a71466cf2645ef9).

Questions:

  • Les signaux sont-ils corrects ?
  • Que se passe-t-il rĂ©ellement lorsque le travailleur gunicorn (synchronisation) reçoit ces signaux ? Comment indique-t-il Ă  l'application WSGI que le signal a Ă©tĂ© dĂ©tectĂ© et que quelque chose doit se passer (ok, je suppose qu'il "le transmet simplement") ?
  • Comment, par exemple, Flask gĂšre-t-il le signal SIGTERM ? En pratique, que se passe-t-il pendant le traitement de la demande ? Est-ce qu'il dĂ©finit simplement un indicateur pour l'application WSGI (au niveau werkzeug) qu'il doit arrĂȘter une fois le traitement de la demande terminé ? Ou est-ce que SIGTERM affecte dĂ©jĂ  d'une maniĂšre ou d'une autre le traitement des demandes en cours - tue les connexions IO ou quelque chose pour accĂ©lĂ©rer le traitement des demandes... ?

Sur SIGKILL , je suppose que le traitement de la demande est simplement interrompu de force.

Je pourrais déposer un petit PR pour améliorer les documents à ce sujet, si je comprends comment les choses fonctionnent réellement.

Discussion Documentation

Commentaire le plus utile

@tuukkamustonen --timeout n'est pas censĂ© ĂȘtre un dĂ©lai d'expiration de la demande. Il s'agit d'un test de vivacitĂ© pour les travailleurs. Pour les agents de synchronisation, cela fonctionne comme un dĂ©lai d'attente de demande car l'agent ne peut rien faire d'autre que traiter la demande. Les travailleurs asynchrones battent mĂȘme lorsqu'ils traitent des requĂȘtes de longue durĂ©e, donc Ă  moins que le travailleur ne le bloque/le bloque, il ne sera pas tuĂ©.

Ce serait peut-ĂȘtre une bonne idĂ©e pour nous de changer le nom si d'autres personnes trouvent cela dĂ©routant.

Tous les 30 commentaires

Hmm, je pense que https://github.com/benoitc/gunicorn/issues/1236#issuecomment -254059927 confirme mes hypothĂšses sur SIGTERM en mettant simplement le travailleur Ă  l'arrĂȘt une fois le traitement de la demande terminĂ© (et en mettant le travailleur Ă  ne pas accepter toute nouvelle connexion).

On dirait que la façon dont j'ai interprété timeout et graceful_timeout est fausse. Les deux périodes se réfÚrent en fait au temps au début du traitement de la demande. Ainsi, par défaut, étant donné que les deux paramÚtres sont définis sur 30 secondes, aucun redémarrage progressif n'est activé. Si je fais quelque chose comme --graceful-timeout 15 --timeout 30 , cela devrait signifier que le redémarrage gracieux est lancé à 15 secondes et que le travailleur est tué de force à 30 secondes si la demande ne s'est pas terminée avant cela.

Cependant, il semble que si la réponse est renvoyée entre graceful_timeout et timeout , alors le travailleur n'est pas redémarré aprÚs tout ? Ne devrait-il pas?

J'ai testé par app.py :

import time
from flask import Flask

app = Flask(__name__)

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

Puis:

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

Ensuite, j'envoie curl localhost:8000/foo , qui revient aprÚs 3 secondes. Mais rien ne se passe dans le gunicorn - je ne vois aucune trace de redémarrage gracieux initié ou arrivé ?

Il semble que sur timeout , SystemExit(1,) soit lancĂ©, annulant le traitement de la requĂȘte en cours dans Flask. Quel code ou signal le gĂ©nĂšre, je ne peux pas le dire.

Cette exception est lancĂ©e via la pile Flask et tous les gestionnaires teardown_request l'attrapent. Il y a suffisamment de temps pour enregistrer quelque chose, mais si vous faites time.sleep(1) ou quelque chose d'autre qui prend du temps dans le gestionnaire, il est tuĂ© en silence. C'est comme s'il y avait 100 Ă  200 ms avant que le processus ne soit effectivement terminĂ© de force et je me demande quel est ce dĂ©lai. Ce n'est pas un dĂ©lai d'attente gracieux, ce paramĂštre n'a aucun impact sur le dĂ©lai. Je m'attendrais Ă  ce que le processus soit simplement tuĂ© de force sur place, au lieu de voir SystemExit ĂȘtre jetĂ© Ă  travers la pile, mais ensuite potentiellement tuer le processus en l'air de toute façon.

En fait, je ne vois pas graceful_timeout faire quoi que ce soit - peut-ĂȘtre qu'il n'est pas pris en charge pour les travailleurs de synchronisation, ou peut-ĂȘtre qu'il ne fonctionne pas "de maniĂšre autonome" (ou avec timeout ) - seulement lorsque vous envoyez manuellement SIGTERM ?

De plus, ce qui pourrait ĂȘtre Ă©trange, c'est que https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L392 ne vĂ©rifie pas du tout le drapeau graceful . Je suppose que https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L390 garantit que self.WORKERS est vide afin que le dĂ©lai d'attente gracieux ne soit pas attendu lors d'un arrĂȘt non gracieux.

@benoitc @tilgovi Envie de donner un coup de main ici ? J'espĂšre que mes Ă©crits ci-dessus ont du sens...

@ tuco86 Le graceful timeout n'est disponible que lorsque vous quittez l'arbitre, le mettez à niveau (USR2), envoyez un signal HUP à l'arbitre ou envoyez un signal QUIT au travailleur. C'est-à-dire qu'il n'est utilisé que lorsque l'action est normale

Le dĂ©lai d'attente est lĂ  pour empĂȘcher les travailleurs occupĂ©s de bloquer les autres demandes. S'ils n'informent pas l'arbitre dans un dĂ©lai infĂ©rieur Ă  timeout , le travailleur est simplement quittĂ© et la connexion avec le client est fermĂ©e.

Euh, d'accord. Est-ce que timeout a un effet lorsque vous :

quitter l'arbitre, le mettre Ă  niveau (USR2), envoyer un signal HUP Ă  l'arbitre ou envoyer un signal QUIT au travailleur

Je veux dire, que se passe-t-il si le travailleur ne s'arrĂȘte pas dans graceful_timeout ? timeout se dĂ©clenchera-t-il aprĂšs cela et que les travailleurs seront tuĂ©s de force, ou est-ce Ă  l'utilisateur d'appeler SIGQUIT au cas oĂč ils ne meurent pas gracieusement ?

Signal QUIT au travailleur

Je suppose que vous vouliez dire TERM ici (car QUIT est documentĂ© comme _arrĂȘt rapide_ pour le maĂźtre et les travailleurs) ?

si le travailleur ne s'arrĂȘte pas pendant le temps de grĂące, il sera tuĂ© sans autre dĂ©lai.

Bien sûr. Merci d'avoir clarifié les choses !

@benoitc Demander dans le contexte de cet ancien ticket - que signifie réellement la derniÚre phrase de la documentation timeout ?

GĂ©nĂ©ralement rĂ©glĂ© sur trente secondes. Ne rĂ©glez ce paramĂštre sensiblement plus haut que si vous ĂȘtes sĂ»r des rĂ©percussions sur les travailleurs de la synchronisation. Pour les travailleurs non synchronisĂ©s, cela signifie simplement que le processus de travail communique toujours et n'est pas liĂ© Ă  la durĂ©e nĂ©cessaire pour traiter une seule demande.

N'Ă©tant pas de langue maternelle anglaise, j'ai du mal Ă  comprendre cela. Cela signifie-t-il que timeout n'est pas pris en charge pour les travailleurs non synchronisĂ©s (parce que c'est ce dont je semble ĂȘtre tĂ©moin : j'utilise des travailleurs gthread et le dĂ©lai d'expiration n'est pas activĂ© et tue les demandes trop lentes ) ?

@tuukkamustonen --timeout n'est pas censĂ© ĂȘtre un dĂ©lai d'expiration de la demande. Il s'agit d'un test de vivacitĂ© pour les travailleurs. Pour les agents de synchronisation, cela fonctionne comme un dĂ©lai d'attente de demande car l'agent ne peut rien faire d'autre que traiter la demande. Les travailleurs asynchrones battent mĂȘme lorsqu'ils traitent des requĂȘtes de longue durĂ©e, donc Ă  moins que le travailleur ne le bloque/le bloque, il ne sera pas tuĂ©.

Ce serait peut-ĂȘtre une bonne idĂ©e pour nous de changer le nom si d'autres personnes trouvent cela dĂ©routant.

@tilgovi timeout est trĂšs bien, mĂȘme si quelque chose comme worker_timeout pourrait ĂȘtre plus descriptif. J'ai juste Ă©tĂ© d'abord confus parce que timeout et graceful_timeout sont dĂ©clarĂ©s l'un Ă  cĂŽtĂ© de l'autre dans la documentation, donc mon cerveau a supposĂ© qu'ils Ă©taient Ă©troitement connectĂ©s, alors qu'ils ne le sont pas.

Pour les agents de synchronisation, cela fonctionne comme un dĂ©lai d'attente de demande car l'agent ne peut rien faire d'autre que traiter la demande. Les travailleurs asynchrones battent mĂȘme lorsqu'ils traitent des requĂȘtes de longue durĂ©e, donc Ă  moins que le travailleur ne le bloque/le bloque, il ne sera pas tuĂ©.

Auriez-vous un exemple oĂč timeout en jeu avec des travailleurs non synchronisĂ©s ? Est-ce quelque chose qui ne devrait jamais arriver, vraiment - peut-ĂȘtre seulement s'il y a un bogue qui provoque le blocage/gel du travailleur ?

C'est correct. Un travailleur asynchrone qui s'appuie sur un noyau de boucle d'événements peut exécuter une procédure gourmande en CPU qui ne produit pas dans le délai d'expiration.

Pas seulement un bug, en d'autres termes. Cependant, cela peut parfois indiquer un bogue, tel qu'un appel à une fonction d'E/S bloquante alors qu'un protocole asynchrone serait plus approprié.

Être bloquĂ© dans une tĂąche intensive du processeur est un bon exemple, merci.

Appeler le blocage des E/S dans le code asynchrone en est un également, mais je ne sais pas comment cela s'applique à ce contexte - j'exécute une application Flask traditionnelle avec du code de blocage mais je l'exécute avec un travailleur asynchrone ( gthread ) sans aucune sorte de correctif de singe. Et ça marche bien. Je sais que ce n'est plus vraiment dans le contexte de ce ticket, mais le fait de mélanger et de faire correspondre le code asynchrone/synchronisé comme celui-ci ne pose-t-il pas de problÚmes ?

De plus, quel est l'intervalle de battement de coeur ? Quelle serait une valeur sensée à utiliser pour timeout avec des travailleurs non synchronisés ?

Le travailleur gthread n'est pas asynchrone, mais il a un thread principal pour le battement de cƓur afin qu'il n'expire pas non plus. Dans le cas de ce travailleur, vous ne verrez probablement pas de dĂ©lai d'attente Ă  moins que le travailleur ne soit trĂšs surchargĂ© ou, plus probablement, que vous appeliez un module d'extension C qui ne libĂšre pas le GIL.

Vous n'avez probablement pas besoin de modifier le délai d'expiration à moins que vous ne commenciez à voir des délais d'expiration de travailleur.

Bien. Encore une chose:

Le travailleur gthread n'est pas asynchrone

Il peut ĂȘtre un peu dĂ©routant que le travailleur gthread ne soit pas asynchrone mais soit rĂ©pertoriĂ© en tant que travailleurs "AsyncIO" sur http://docs.gunicorn.org/en/stable/design.html#asyncio -workers. En dehors de cela, l'utilisation de "threads" n'a pas besoin d'asyncio, ce qui soulĂšve Ă©galement des questions chez le lecteur. En disant cela du point de vue d'un utilisateur naĂŻf, je suis sĂ»r que tout cela est techniquement fondĂ©.

En un mot, le travailleur gthread est implémenté avec asyncio lib mais il génÚre des threads pour gérer le code de synchronisation. Corrigez-moi si je me trompe.

Heureux que vous ayez demandé !

Le travailleur threadé n'utilise pas asyncio et n'hérite pas de la classe de travail asynchrone de base.

Nous devrions clarifier la documentation. Je pense qu'il a peut-ĂȘtre Ă©tĂ© rĂ©pertoriĂ© comme asynchrone car le dĂ©lai d'expiration du travailleur est gĂ©rĂ© simultanĂ©ment, ce qui le fait se comporter davantage comme les travailleurs asynchrones que le travailleur de synchronisation en ce qui concerne la capacitĂ© Ă  gĂ©rer les demandes longues et les demandes simultanĂ©es.

Ce serait formidable de clarifier la documentation et de la faire décrire plus précisément tous les travailleurs.

oui, le travailleur gthreads ne devrait pas ĂȘtre rĂ©pertoriĂ© dans le travailleur asyncio. peut-ĂȘtre qu'avoir une section dĂ©crivant la conception de chaque travailleur est prĂ©fĂ©rable ?

Rouvrir ceci afin que nous puissions le suivre comme un travail pour clarifier la section sur les types de travailleurs et les délais d'attente.

@tilgovi

--timeout n'est pas conçu comme un dĂ©lai d'attente de requĂȘte. Il s'agit d'un test de vivacitĂ© pour les travailleurs. Pour les agents de synchronisation, cela fonctionne comme un dĂ©lai d'attente de demande car l'agent ne peut rien faire d'autre que traiter la demande. Les travailleurs asynchrones battent mĂȘme lorsqu'ils traitent des requĂȘtes de longue durĂ©e, donc Ă  moins que le travailleur ne le bloque/le bloque, il ne sera pas tuĂ©.

Existe-t-il une option de délai d'attente de demande disponible pour les travailleurs asynchrones ? En d'autres termes, comment faire en sorte qu'un arbitre tue un travailleur qui n'a pas traité une demande dans un délai spécifié ?

@aschatten il n'y en a pas, malheureusement. Voir aussi #1658.

tuer un travailleur qui n'a pas traité une demande dans un délai spécifié

Comme un travailleur peut traiter plusieurs demandes simultanĂ©ment, tuer un travailleur entier parce qu'une demande expire semble assez extrĂȘme. Cela n'entraĂźnerait-il pas que toutes les autres requĂȘtes seraient tuĂ©es en vain ?

Je me souviens que uWSGI prévoyait d'introduire la mise à mort basée sur les threads dans la version 2.1 environ, bien que cela ne s'applique probablement qu'aux travailleurs synchronisés/threadés (et mes souvenirs à ce sujet sont vagues).

Comme un travailleur peut traiter plusieurs demandes simultanĂ©ment, tuer un travailleur entier parce qu'une demande expire semble assez extrĂȘme. Cela n'entraĂźnerait-il pas la mort de toutes les autres requĂȘtes en vain ?

L'approche peut ĂȘtre la mĂȘme que pour max_request , oĂč il existe une implĂ©mentation distincte pour chaque type de travailleur.

Nous travaillons sur une version cette semaine, Ă  quel point il est _peut-ĂȘtre_ temps de passer Ă  R20, oĂč nous prĂ©voyons de nous attaquer Ă  quelques problĂšmes majeurs. C'est peut-ĂȘtre le bon moment pour transformer le dĂ©lai d'expiration actuel en un dĂ©lai d'expiration de demande appropriĂ© pour chaque type de travailleur.

Commentant ici au lieu de déposer un problÚme séparé car j'essaie de comprendre comment le délai d'attente est censé fonctionner et je ne sais pas s'il s'agit d'un bogue ou non.

Le comportement inattendu de l'OMI que je vois est le suivant :

Chaque requĂȘte max-requests (celle aprĂšs laquelle le travailleur sera redĂ©marrĂ©) est dĂ©passĂ©e, tandis que les autres requĂȘtes sont terminĂ©es avec succĂšs. Dans l'exemple ci-dessous, 4 requĂȘtes sont exĂ©cutĂ©es, les requĂȘtes 1, 2 et 4 rĂ©ussissent, tandis que la requĂȘte 3 Ă©choue.

Paramétrage concerné :

  • travailleur gthread
  • la demande de traitement prend plus de temps que le dĂ©lai d'attente
  • max-requests est diffĂ©rent de zĂ©ro
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"]

gunicorne :

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

Client:

[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

quel devrait ĂȘtre le plan lĂ -bas? J'ai en tĂȘte ce qui suit :

  • [ ] mettre Ă  jour la description du collaborateur (si nĂ©cessaire)
  • [ ] documenter le protocole pour dĂ©tecter les travailleurs morts ou bloquĂ©s

Devrait-il ĂȘtre 20,0 ou pourrions-nous le reporter?

report.

Hé, donc ça ne fera pas partie de la 20.0 ?

C'est peut-ĂȘtre le bon moment pour transformer le dĂ©lai d'expiration actuel en un dĂ©lai d'expiration de demande appropriĂ© pour chaque type de travailleur.

clarifié. @ lucas03 on ne sait pas quel est le délai d'expiration de la demande. veuillez ouvrir un ticket si vous avez besoin de quelque chose de spécifique ?.

Cette page vous a été utile?
0 / 5 - 0 notes