Gunicorn: Aclarar qué/cómo funcionan timeout y graceful_timeout

Creado en 3 abr. 2017  ·  30Comentarios  ·  Fuente: benoitc/gunicorn

(Perdón por el monólogo aquí: las cosas simples se complicaron y terminé buscando en la pila. Sin embargo, espero que lo que he documentado sea útil para el lector).

Según tengo entendido, por defecto:

  • Después 30 segundos (configurable con timeout ) del procesamiento de la solicitud, el proceso maestro de gunicorn envía SIGTERM al proceso de trabajo para iniciar un reinicio correcto.
  • Si el trabajador no se apaga durante otros 30 segundos (configurable con graceful_timeout ), el proceso maestro envía SIGKILL . Parece que esta señal también se envía cuando el trabajador _sí_ se apaga correctamente durante el período de graceful_timeout (https://github.com/benoitc/gunicorn/commit/d1a09732256fa8db900a1fe75a71466cf2645ef9).

Las preguntas:

  • ¿Son correctas las señales?
  • ¿Qué sucede realmente cuando el trabajador gunicorn (sync) recibe estas señales? ¿Cómo le dice a la aplicación WSGI que la señal fue captada y que algo debería suceder (bueno, supongo que simplemente "la pasa")?
  • ¿Cómo, por ejemplo, Flask maneja la señal SIGTERM ? En la práctica, ¿qué sucede durante el procesamiento de la solicitud? ¿Simplemente establece un indicador para la aplicación WSGI (en el nivel werkzeug) que debe cerrarse después de que se complete el procesamiento de la solicitud? ¿O SIGTERM ya afecta de alguna manera el procesamiento de solicitudes en curso: elimina las conexiones de E/S o algo para acelerar el procesamiento de solicitudes...?

En SIGKILL , supongo que el procesamiento de la solicitud se abortó por la fuerza.

Podría presentar un pequeño PR para mejorar los documentos sobre esto, si entiendo cómo funcionan realmente las cosas.

Discussion Documentation

Comentario más útil

@tuukkamustonen --timeout no está pensado como un tiempo de espera de solicitud. Está pensado como un control de vida para los trabajadores. Para los trabajadores de sincronización, esto funciona como un tiempo de espera de solicitud porque el trabajador no puede hacer nada más que procesar la solicitud. El corazón de los trabajadores asincrónicos late incluso mientras manejan solicitudes de ejecución prolongada, por lo que, a menos que el trabajador bloquee/congele, no se eliminará.

Tal vez sería una buena idea para nosotros cambiar el nombre si otras personas lo encuentran confuso.

Todos 30 comentarios

Hmm, creo que https://github.com/benoitc/gunicorn/issues/1236#issuecomment -254059927 confirma mis suposiciones sobre SIGTERM simplemente configurando el trabajador para que se apague después de que se complete el procesamiento de la solicitud (y configurando el trabajador para que no acepte cualquier conexión nueva).

Parece que la forma en que interpreté timeout y graceful_timeout es incorrecta. Ambos períodos en realidad se refieren al tiempo al comienzo del procesamiento de la solicitud. Entonces, de forma predeterminada, debido a que ambas configuraciones están configuradas en 30 segundos, no hay un reinicio correcto habilitado. Si hago algo como --graceful-timeout 15 --timeout 30 eso debería significar que el reinicio correcto se inicia a los 15 segundos y el trabajador se mata a la fuerza a los 30 segundos si la solicitud no se completó antes de eso.

Sin embargo, parece que si la respuesta se devuelve entre graceful_timeout y timeout , ¿entonces el trabajador no se reinicia después de todo? ¿No debería?

Probé por app.py :

import time
from flask import Flask

app = Flask(__name__)

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

Entonces:

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

Luego envío curl localhost:8000/foo , que regresa después de 3 segundos. Pero no sucede nada en gunicorn: ¿no veo ningún rastro de que se inicie o suceda un reinicio elegante?

Parece que en timeout , se lanza SystemExit(1,) , abortando el procesamiento de solicitud actual en Flask. Qué código o señal lo genera, no puedo decirlo.

Esta excepción se lanza a través de la pila Flask y cualquier controlador teardown_request la detecta. Hay suficiente tiempo para registrar algo, pero si haces time.sleep(1) o algo más que consume mucho tiempo en el controlador, se elimina silenciosamente. Es como si hubiera un tiempo de 100 a 200 ms antes de que el proceso se terminara por la fuerza y ​​me pregunto qué es este retraso. No es un tiempo de espera elegante, esa configuración no tiene impacto en el retraso. Esperaría que el proceso se elimine a la fuerza en su lugar, en lugar de ver SystemExit arrojados a través de la pila, pero luego potencialmente matar el proceso en el aire de todos modos.

De hecho, no veo que graceful_timeout haga nada; tal vez no sea compatible con los trabajadores de sincronización, o tal vez no funcione de forma "independiente" (o junto con timeout ), solo cuando envía manualmente SIGTERM ?

Además, lo que podría ser extraño es que https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L392 no marca el indicador graceful en absoluto. Supongo que https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L390 asegura que self.WORKERS está vacío, por lo que no se espera el tiempo de espera correcto cuando se realiza una parada no elegante.

@benoitc @tilgovi ¿Te importa echar una mano aquí? Espero que mis escritos anteriores tengan sentido...

@tuco86 El graceful timeout solo está disponible cuando sale del árbitro, lo actualiza (USR2), envía una señal HUP al árbitro o envía una señal de SALIDA al trabajador. Es decir, solo se usa cuando la acción es normal.

El tiempo de espera está aquí para evitar que los trabajadores ocupados bloqueen otras solicitudes. Si no notifican al árbitro en un tiempo menor al timeout el trabajador simplemente sale y la conexión con el cliente se cierra.

Está bien. ¿ timeout tiene algún efecto cuando usted:

salir del árbitro, actualizarlo (USR2), enviar una señal HUP al árbitro o enviar una señal QUIT al trabajador

Quiero decir, ¿qué pasa si el trabajador no se apaga en graceful_timeout ? Se activará timeout después de eso y los trabajadores serán asesinados a la fuerza, o queda en manos del usuario pedir SIGQUIT en caso de que no mueran con gracia?

Señal de SALIDA al trabajador

Supongo que quiso decir TERM aquí (ya que QUIT está documentado como _cierre rápido_ tanto para el maestro como para los trabajadores)?

si el trabajador no se apaga durante el tiempo de gracia, se matará sin ningún otro retraso.

Por supuesto. ¡Gracias por aclarar las cosas!

@benoitc Preguntando en el contexto de este ticket anterior: ¿qué significa realmente la última oración en la documentación de timeout ?

Generalmente se establece en treinta segundos. Establezca esto notablemente más alto solo si está seguro de las repercusiones para los trabajadores de sincronización. Para los trabajadores que no están sincronizados, solo significa que el proceso de trabajo aún se está comunicando y no está vinculado al tiempo requerido para manejar una sola solicitud.

Al no ser un hablante nativo de inglés, me cuesta mucho entender esto. ¿Significa que timeout no es compatible con los trabajadores que no están sincronizados (porque eso es lo que parece estar presenciando: estoy usando gthread trabajadores y el tiempo de espera no se activa y elimina las solicitudes demasiado lentas) )?

@tuukkamustonen --timeout no está pensado como un tiempo de espera de solicitud. Está pensado como un control de vida para los trabajadores. Para los trabajadores de sincronización, esto funciona como un tiempo de espera de solicitud porque el trabajador no puede hacer nada más que procesar la solicitud. El corazón de los trabajadores asincrónicos late incluso mientras manejan solicitudes de ejecución prolongada, por lo que, a menos que el trabajador bloquee/congele, no se eliminará.

Tal vez sería una buena idea para nosotros cambiar el nombre si otras personas lo encuentran confuso.

@tilgovi timeout está bien, aunque algo como worker_timeout podría ser más descriptivo. Inicialmente me confundí porque timeout y graceful_timeout se declaran uno al lado del otro en la documentación, por lo que mi cerebro asumió que están estrechamente conectados, mientras que en realidad no lo están.

Para los trabajadores de sincronización, esto funciona como un tiempo de espera de solicitud porque el trabajador no puede hacer nada más que procesar la solicitud. El corazón de los trabajadores asincrónicos late incluso mientras manejan solicitudes de ejecución prolongada, por lo que, a menos que el trabajador bloquee/congele, no se eliminará.

¿Tendría un ejemplo de cuándo se timeout con trabajadores no sincronizados? ¿Es algo que nunca debería suceder, en realidad, tal vez solo si hay un error que hace que el trabajador se bloquee/congele?

Eso es correcto. Un trabajador asíncrono que se basa en un núcleo de bucle de eventos podría realizar un procedimiento intensivo de CPU que no rinda dentro del tiempo de espera.

No solo un error, en otras palabras. Aunque, a veces, puede indicar un error, como una llamada a una función de E/S de bloqueo cuando un protocolo asyncio sería más apropiado.

Quedarse atascado en una tarea intensiva de CPU es un buen ejemplo, gracias.

Llamar a E/S de bloqueo en código asíncrono también es una, pero no estoy seguro de cómo se aplica a este contexto: estoy ejecutando una aplicación Flask tradicional con código de bloqueo pero ejecutándola con un trabajador asíncrono ( gthread ) sin ningún tipo de parche de mono. Y funciona bien. Sé que esto ya no está realmente en el contexto de este ticket, pero ¿no causa problemas mezclar y combinar código asíncrono/sincrónico como este?

Además, ¿cuál es el intervalo de latidos del corazón? ¿Cuál sería un valor sensato para usar para timeout con trabajadores no sincronizados?

El trabajador gthread no es asíncrono, pero tiene un subproceso principal para el latido del corazón, por lo que tampoco se agotará el tiempo de espera. En el caso de ese trabajador, probablemente no verá un tiempo de espera a menos que el trabajador esté muy sobrecargado o, más probablemente, llame a un módulo de extensión C que no libera el GIL.

Probablemente no tenga que cambiar el tiempo de espera a menos que comience a ver los tiempos de espera de los trabajadores.

Bien. Solo una cosa más:

El trabajador gthread no es asíncrono

Puede ser un poco confuso que el trabajador gthread no sea asíncrono, pero que aparezca como trabajadores "AsyncIO" en http://docs.gunicorn.org/en/stable/design.html#asyncio -workers. Aparte de eso, el uso de "hilos" no necesita asyncio, por lo que también genera preguntas en el lector. Solo diciendo esto desde la perspectiva de un usuario ingenuo, estoy seguro de que todo está bien fundamentado técnicamente.

En pocas palabras, el trabajador gthread se implementa con asyncio lib pero genera subprocesos para manejar el código de sincronización. Corrígeme si me equivoco.

¡Me alegra que hayas preguntado!

El trabajador subproceso no usa asyncio y no hereda de la clase de trabajador asíncrono base.

Deberíamos aclarar la documentación. Creo que puede haber sido catalogado como asíncrono porque el tiempo de espera del trabajador se maneja simultáneamente, lo que hace que se comporte más como los trabajadores asíncronos que como el trabajador sincronizado con respecto a la capacidad de manejar solicitudes largas y solicitudes simultáneas.

Sería genial aclarar la documentación y hacer que describa con mayor precisión a todos los trabajadores.

sí, el trabajador gthreads no debería aparecer en el trabajador asyncio. ¿Tal vez sea mejor tener una sección que describa el diseño de cada trabajador?

Reabriendo esto para que podamos rastrearlo como trabajo para aclarar la sección sobre tipos de trabajadores y tiempos de espera.

@tilgovi

--timeout no se entiende como un tiempo de espera de solicitud. Está pensado como un control de vida para los trabajadores. Para los trabajadores de sincronización, esto funciona como un tiempo de espera de solicitud porque el trabajador no puede hacer nada más que procesar la solicitud. El corazón de los trabajadores asincrónicos late incluso mientras manejan solicitudes de ejecución prolongada, por lo que, a menos que el trabajador bloquee/congele, no se eliminará.

¿Hay una opción de tiempo de espera de solicitud disponible para los trabajadores asincrónicos? En otras palabras, ¿cómo hacer que el árbitro mate a un trabajador que no procesó una solicitud dentro de un tiempo específico?

@aschatten no hay, desafortunadamente. Ver también #1658.

matar a un trabajador que no procesó una solicitud dentro de un tiempo específico

Como un trabajador puede estar procesando varias solicitudes al mismo tiempo, matar a todo el trabajador porque una solicitud se agota suena bastante extremo. ¿Eso no daría como resultado que todas las demás solicitudes fueran eliminadas en vano?

Recuerdo que uWSGI estaba planeando introducir la eliminación basada en subprocesos en 2.1 más o menos, aunque probablemente incluso eso se aplica solo a los trabajadores sincronizados/subprocesos (y mi recuerdo sobre esto es vago).

Como un trabajador puede estar procesando varias solicitudes al mismo tiempo, matar a todo el trabajador porque una solicitud se agota suena bastante extremo. ¿Eso no daría como resultado que todas las demás solicitudes fueran eliminadas en vano?

El enfoque puede ser el mismo que para max_request , donde hay una implementación separada para cada tipo de trabajador.

Estamos trabajando en un lanzamiento esta semana, momento en el cual _puede_ ser el momento de pasar a R20, donde planeamos abordar algunas cosas importantes. Ese podría ser el momento adecuado para convertir el tiempo de espera actual en un tiempo de espera de solicitud adecuado para cada tipo de trabajador.

Comentar aquí en lugar de presentar un problema por separado, ya que estoy tratando de entender cómo se supone que funciona el tiempo de espera y no estoy seguro de si se trata de un error o no.

El comportamiento inesperado de la OMI que estoy viendo es este:

Cada solicitud max-requests'th (aquella después de la cual se reiniciará el trabajador) se agota, mientras que las demás solicitudes se completan correctamente. En el siguiente ejemplo, se realizan 4 solicitudes, las solicitudes 1, 2 y 4 tienen éxito, mientras que la solicitud 3 falla.

Configuración relevante:

  • trabajador de subprocesos
  • la solicitud de servicio tarda más que el tiempo de espera
  • max-requests no es cero
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"]

gunicornio:

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

Cliente:

[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

¿Cuál debería ser el plan allí? Tengo en mente lo siguiente:

  • [ ] actualizar la descripción del trabajador (si aún es necesario)
  • [ ] documentar el protocolo para detectar trabajadores muertos o bloqueados

¿Debería ser 20.0 o podríamos posponerlo?

posponiendo

Oye, ¿así que esto no será parte de 20.0?

Ese podría ser el momento adecuado para convertir el tiempo de espera actual en un tiempo de espera de solicitud adecuado para cada tipo de trabajador.

aclarado @ lucas03 no está claro qué tiempo de espera de solicitud hay. por favor abra un ticket si necesita algo especifico?.

¿Fue útil esta página
0 / 5 - 0 calificaciones