Gunicorn: Esclareça o que/como o timeout e o graceful_timeout funcionam

Criado em 3 abr. 2017  ·  30Comentários  ·  Fonte: benoitc/gunicorn

(Desculpe pelo monólogo aqui: coisas simples ficaram complicadas e acabei vasculhando a pilha. Espero que o que eu documentei seja útil para o leitor, no entanto.)

Pelo que entendi, por padrão:

  • Após 30 segundos (configurável com timeout ) de processamento de solicitação, o processo mestre do gunicorn envia SIGTERM para o processo de trabalho, para iniciar uma reinicialização normal.
  • Se o trabalhador não for encerrado durante outros 30 segundos (configurável com graceful_timeout ), o processo mestre enviará SIGKILL . Parece que esse sinal também é enviado quando o trabalhador _does_ desliga normalmente durante o período graceful_timeout (https://github.com/benoitc/gunicorn/commit/d1a09732256fa8db900a1fe75a71466cf2645ef9).

As questões:

  • Os sinais estão corretos?
  • O que realmente acontece quando o trabalhador gunicorn (sincronização) recebe esses sinais? Como ele informa ao aplicativo WSGI que o sinal foi capturado e algo deve acontecer (ok, suponho que apenas "passa adiante")?
  • Como, por exemplo, o Flask lida com o sinal SIGTERM - na prática, o que acontece durante o processamento da solicitação? Ele apenas define um sinalizador para o aplicativo WSGI (no nível werkzeug) que deve ser encerrado após a conclusão do processamento da solicitação? Ou SIGTERM já afeta de alguma forma o processamento de solicitações em andamento - mata conexões de E/S ou algo para acelerar o processamento de solicitações...?

Em SIGKILL , acho que o processamento da solicitação é abortado com força.

Eu poderia registrar um pequeno PR para melhorar os documentos sobre isso, se eu entender como as coisas realmente funcionam.

Discussion Documentation

Comentários muito úteis

@tuukkamustonen --timeout não significa um tempo limite de solicitação. Destina-se como uma verificação de vitalidade para os trabalhadores. Para trabalhadores de sincronização, isso funciona como um tempo limite de solicitação porque o trabalhador não pode fazer nada além de processar a solicitação. A pulsação dos trabalhadores assíncronos mesmo enquanto eles estão lidando com solicitações de longa execução, portanto, a menos que o trabalhador bloqueie/congele, ele não será eliminado.

Talvez seja uma boa ideia mudarmos o nome se outras pessoas acharem isso confuso.

Todos 30 comentários

Hmm, acho que https://github.com/benoitc/gunicorn/issues/1236#issuecomment -254059927 confirma minhas suposições sobre SIGTERM simplesmente configurar o trabalhador para desligar após o processamento da solicitação ser concluído (e configurar o trabalhador para não aceitar quaisquer novas conexões).

Parece que como eu interpretei timeout e graceful_timeout está errado. Ambos os períodos referem-se, na verdade, ao tempo no início do processamento da solicitação. Portanto, por padrão, como ambas as configurações são definidas para 30 segundos, não há reinicialização normal habilitada. Se eu fizer algo como --graceful-timeout 15 --timeout 30 isso deve significar que a reinicialização graciosa é iniciada em 15 segundos e o trabalhador é morto à força em 30 segundos se a solicitação não for concluída antes disso.

No entanto, parece que se a resposta for retornada entre graceful_timeout e timeout , o trabalhador não será reiniciado afinal? Não deveria?

Eu testei por app.py :

import time
from flask import Flask

app = Flask(__name__)

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

Então:

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

Então eu envio curl localhost:8000/foo , que retorna após 3 segundos. Mas nada acontece no gunicorn - não vejo nenhum traço de reinício gracioso sendo iniciado ou acontecido?

Parece que em timeout , SystemExit(1,) é lançado, abortando o processamento da solicitação atual no Flask. Que código ou sinal o gera, não posso dizer.

Essa exceção é lançada através da pilha do Flask e qualquer manipulador teardown_request a captura. Há tempo suficiente para registrar algo, mas se você fizer time.sleep(1) ou algo mais demorado no manipulador, ele será morto silenciosamente. É como se houvesse um tempo de 100-200ms antes que o processo fosse realmente encerrado com força e estou me perguntando qual é esse atraso. Não é um tempo limite normal, essa configuração não tem impacto no atraso. Eu esperaria que o processo fosse morto com força no lugar, em vez de ver SystemExit sendo jogado pela pilha, mas potencialmente matando o processo no ar de qualquer maneira.

Na verdade, não vejo graceful_timeout fazendo nada - talvez não seja compatível com trabalhadores de sincronização ou talvez não funcione "independente" (ou junto com timeout ) - apenas quando você envia manualmente SIGTERM ?

Além disso, o que pode ser estranho é que https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L392 não verifica o sinalizador graceful . Eu acho que https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L390 garante que self.WORKERS esteja vazio, então o tempo limite gracioso não é esperado ao fazer uma parada não graciosa.

@benoitc @tilgovi Quer dar uma mãozinha aqui? Espero que meus escritos acima façam sentido ...

@tuco86 O graceful timeout só está disponível quando você sai do árbitro, atualiza-o (USR2), envia um sinal HUP para o árbitro ou envia um sinal QUIT para o trabalhador. Ou seja, só é usado quando a ação é normal

O tempo limite está aqui para evitar que trabalhadores ocupados bloqueiem outras solicitações. Se eles não notificarem o árbitro em um tempo menor que timeout o trabalhador é simplesmente encerrado e a conexão com o cliente encerrada.

Certo. timeout tem algum efeito quando você:

saia do árbitro, atualize-o (USR2), envie um sinal HUP para o árbitro ou envie um sinal QUIT para o trabalhador

Quero dizer, e se o trabalhador não desligar em graceful_timeout - timeout entrará em ação depois disso e os trabalhadores forem mortos à força, ou é deixado para o usuário pedir SIGQUIT caso eles não morram graciosamente?

QUIT sinal para o trabalhador

Eu suponho que você quis dizer TERM aqui (como QUIT está documentado como _desligamento rápido_ para mestre e trabalhadores)?

se o trabalhador não desligar durante o tempo normal, ele será morto sem qualquer outro atraso.

Claro. Obrigado por esclarecer as coisas!

@benoitc Perguntando no contexto deste ticket antigo - o que a última frase na documentação timeout realmente significa?

Geralmente definido para trinta segundos. Apenas defina isso visivelmente mais alto se tiver certeza das repercussões para os trabalhadores de sincronização. Para os trabalhadores não sincronizados, isso significa apenas que o processo de trabalho ainda está se comunicando e não está vinculado ao período de tempo necessário para lidar com uma única solicitação.

Não sendo um falante nativo de inglês, tenho dificuldade em entender isso. Isso significa que timeout não é suportado para trabalhadores não sincronizados (porque é isso que pareço estar testemunhando: estou usando gthread trabalhadores e o tempo limite não está entrando e matando solicitações muito lentas )?

@tuukkamustonen --timeout não significa um tempo limite de solicitação. Destina-se como uma verificação de vitalidade para os trabalhadores. Para trabalhadores de sincronização, isso funciona como um tempo limite de solicitação porque o trabalhador não pode fazer nada além de processar a solicitação. A pulsação dos trabalhadores assíncronos mesmo enquanto eles estão lidando com solicitações de longa execução, portanto, a menos que o trabalhador bloqueie/congele, ele não será eliminado.

Talvez seja uma boa ideia mudarmos o nome se outras pessoas acharem isso confuso.

@tilgovi timeout é bom, embora algo como worker_timeout possa ser mais descritivo. Fiquei inicialmente confuso porque timeout e graceful_timeout são declarados um ao lado do outro na documentação, então meu cérebro assumiu que eles estão firmemente conectados, enquanto na verdade não estão.

Para trabalhadores de sincronização, isso funciona como um tempo limite de solicitação porque o trabalhador não pode fazer nada além de processar a solicitação. A pulsação dos trabalhadores assíncronos mesmo enquanto eles estão lidando com solicitações de longa execução, portanto, a menos que o trabalhador bloqueie/congele, ele não será eliminado.

Você teria um exemplo de quando timeout entra em ação com trabalhadores não sincronizados? É algo que nunca deveria acontecer, realmente - talvez apenas se houver um bug que faça com que o trabalhador bloqueie/congele?

Está correto. Um trabalhador assíncrono que depende de um núcleo de loop de eventos pode executar um procedimento intensivo de CPU que não produz dentro do tempo limite.

Não apenas um bug, em outras palavras. Embora, às vezes, possa indicar um bug, como uma chamada para uma função de bloqueio de E/S quando um protocolo assíncrono seria mais apropriado.

Ficar preso na tarefa intensiva da CPU é um bom exemplo, obrigado.

Chamar o bloqueio de E/S em código assíncrono também é um, mas não tenho certeza de como isso se aplica a esse contexto - estou executando um aplicativo Flask tradicional com código de bloqueio, mas executando-o com um trabalhador assíncrono ( gthread ) sem nenhum tipo de correção de macaco. E funciona apenas ok. Eu sei que isso não está mais no contexto deste ticket, mas misturar e combinar código assíncrono/sincronizado como esse não causa problemas?

Além disso, qual é o intervalo de batimentos cardíacos? Qual seria um valor sensato para usar timeout com trabalhadores não sincronizados?

O trabalhador gthread não é assíncrono, mas possui um thread principal para a pulsação, portanto, também não atingirá o tempo limite. No caso desse trabalhador, você provavelmente não verá um tempo limite a menos que o trabalhador esteja muito sobrecarregado ou, mais provavelmente, você chame um módulo de extensão C que não libera o GIL.

Você provavelmente não precisa alterar o tempo limite, a menos que comece a ver os tempos limite do trabalhador.

Tudo bem. Só mais uma coisa:

O trabalhador gthread não é assíncrono

Pode ser um pouco confuso que gthread worker não seja assíncrono, mas esteja listado como workers "AsyncIO" em http://docs.gunicorn.org/en/stable/design.html#asyncio -workers. Fora isso, usar "threads" não precisa de assíncrono, então isso também levanta dúvidas no leitor. Apenas dizendo isso da perspectiva de um usuário ingênuo, tenho certeza de que tudo é tecnicamente bem fundamentado.

Em poucas palavras, o trabalhador gthread é implementado com asyncio lib, mas gera threads para manipular o código de sincronização. Corrija-me se estiver errado.

Que bom que você perguntou!

O trabalhador encadeado não usa assíncrono e não herda da classe trabalhadora assíncrona base.

Devemos esclarecer a documentação. Eu acho que pode ter sido listado como assíncrono porque o tempo limite do trabalhador é tratado simultaneamente, fazendo com que ele se comporte mais como os trabalhadores assíncronos do que o trabalhador de sincronização com relação à capacidade de lidar com solicitações longas e solicitações simultâneas.

Seria ótimo esclarecer a documentação e torná-la mais precisa para descrever todos os trabalhadores.

sim, o trabalhador gthreads não deve ser listado no trabalhador assíncrono. talvez ter uma seção que descreva o design de cada trabalhador seja melhor?

Reabrindo isso para que possamos rastreá-lo como trabalho para esclarecer a seção sobre tipos de trabalhadores e tempos limite.

@tilgovi

--timeout não significa um tempo limite de solicitação. Destina-se como uma verificação de vitalidade para os trabalhadores. Para trabalhadores de sincronização, isso funciona como um tempo limite de solicitação porque o trabalhador não pode fazer nada além de processar a solicitação. A pulsação dos trabalhadores assíncronos mesmo enquanto eles estão lidando com solicitações de longa execução, portanto, a menos que o trabalhador bloqueie/congele, ele não será eliminado.

Existe uma opção de tempo limite de solicitação disponível para trabalhadores assíncronos? Em outras palavras, como fazer o árbitro matar um trabalhador que não processou uma solicitação dentro de um tempo especificado?

@aschatten não existe, infelizmente. Veja também #1658.

matar um trabalhador que não processou uma solicitação dentro de um tempo especificado

Como um trabalhador pode estar processando várias solicitações simultaneamente, matar o trabalhador inteiro porque uma solicitação atinge o tempo limite parece bastante extremo. Isso não resultaria em todos os outros pedidos sendo mortos em vão?

Lembro-me de que o uWSGI estava planejando introduzir a eliminação baseada em thread em 2.1 ou algo assim, embora provavelmente até isso se aplique apenas a trabalhadores de sincronização / thread (e minha lembrança sobre isso é vaga).

Como um trabalhador pode estar processando várias solicitações simultaneamente, matar o trabalhador inteiro porque uma solicitação atinge o tempo limite parece bastante extremo. Isso não resultaria em todos os outros pedidos sendo mortos em vão?

A abordagem pode ser a mesma de max_request , onde há uma implementação separada para cada tipo de trabalhador.

Estamos trabalhando em um lançamento esta semana, quando _pode_ ser hora de ramificar para o R20, onde planejamos abordar algumas coisas importantes. Esse pode ser o momento certo para transformar o tempo limite atual em um tempo limite de solicitação adequado para cada tipo de trabalhador.

Comentando aqui em vez de registrar um problema separado, pois estou tentando entender como o tempo limite deve funcionar e não tenho certeza se isso é um bug ou não.

O comportamento inesperado da IMO que estou vendo é este:

Cada solicitação max-requests'th (aquela após a qual o trabalhador será reiniciado) é expirada, enquanto as outras solicitações são concluídas com sucesso. No exemplo abaixo, 4 solicitações são executadas, as solicitações 1, 2 e 4 são bem-sucedidas, enquanto a solicitação 3 falha.

Configuração relevante:

  • trabalhador gthread
  • a solicitação de atendimento demora mais do que o tempo limite
  • max-requests é diferente de zero
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"]

gunicórnio:

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

qual deve ser o plano lá? Tenho em mente o seguinte:

  • [ ] atualizar a descrição do trabalhador (se ainda for necessário)
  • [ ] documentar o protocolo para detectar trabalhadores mortos ou bloqueados

Deveria ser 20.0 ou poderíamos adiá-lo?

adiando.

Ei, então isso não fará parte do 20.0?

Esse pode ser o momento certo para transformar o tempo limite atual em um tempo limite de solicitação adequado para cada tipo de trabalhador.

esclarecido. @lucas03 não está claro qual é o tempo limite da solicitação. por favor abra um ticket se precisar de algo específico?.

Esta página foi útil?
0 / 5 - 0 avaliações