Gunicorn: O POST falha ao retornar> 13k de resposta no Heroku

Criado em 4 ago. 2014  ·  34Comentários  ·  Fonte: benoitc/gunicorn

Olá, encontramos esse problema na produção usando Flask + Gunicorn + Heroku e não conseguimos encontrar uma causa ou solução alternativa.

Para uma determinada solicitação POST com parâmetros POST, a solicitação falharia com um erro H18 (sock = backend) no roteador do Heroku, indicando que o servidor fechou o soquete quando não deveria.

Começamos a diminuir o tamanho da resposta desse endpoint com falha até reduzi-lo para cerca de 13k. Se enviarmos menos de 13k, a resposta sempre funcionará. Se enviarmos mais de 13k, a resposta quase sempre não funcionará.

O código para reproduzi-lo está disponível em https://github.com/erjiang/gunicorn-issue - basta implantar o repo no Heroku como está e seguir as instruções no README.

( Feedback Requested unconfirmed help wanted - Bugs -

Comentários muito úteis

Consegui reproduzir usando o caso de teste em https://github.com/erjiang/gunicorn-issue (que está usando gunicorn 19.9.0, Python 2.7.14, sync worker, --workers 4 ). É importante notar que a saída do registro de acesso do gunicorn relata que ele acredita ter retornado um HTTP 200.

Atualizar para Python 3.7.3 + gunicorn master e reduzir para --workers 1 não teve efeito sobre a reprodutibilidade, no entanto, alternar de trabalhador de sincronização para gevent fez com que o erro ocorresse com menos frequência (embora ainda acontecesse). Usar --log-level debug não revelou nada significativo (a única saída adicional durante a solicitação foi a linha [DEBUG] POST /test1 ).

Em seguida, tentei --spew , mas o problema não era mais reproduzido. Isso me levou a tentar adicionar time.sleep(1) antes de resp.close() aqui, o que evitou o problema de forma semelhante.

Assim, parece que a causa é que o buffer de envio do soquete pode não estar vazio no momento do close() , o que pode causar a perda da resposta:

Nota: close() libera o recurso associado a uma conexão, mas não necessariamente fecha a conexão imediatamente. Se você quiser fechar a conexão em tempo hábil, ligue para shutdown() antes de close() .

(Consulte https://docs.python.org/3/library/socket.html#socket.socket.close)

Adicionar sock.shutdown(socket.SHUT_RDWR) ( docs ) antes de sock.close() aqui resolveu o problema para mim. Uma solução alternativa talvez seja usar SO_LINGER , embora pelo que li isso tenha compensações.

É difícil encontrar documentos sobre esse assunto, mas descobri:
https://stackoverflow.com/questions/8874021/close-socket-directly-after-send-unsafe
https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable

Espero que ajude :-)

Todos 34 comentários

Relatório realmente útil, obrigado @erjiang.

não tenho nenhuma conta Heroku para testar. Alguém com essa conta pode testá-lo? cc @tilgovi @kennethreitz

Feliz, mas provavelmente não vou chegar lá em breve.

Como uma verificação rápida de sanidade, executei localmente e verifiquei algumas coisas com curl para comparar garçonete e gunicorn:

  • [x] Comprimento do conteúdo igual
  • [x] Mesmo conteúdo corporal
  • [x] Mesma codificação de transferência (nenhum especifica chunked, ambos usam Content-Length)

Em seguida, estou curioso para saber se há diferenças no nível do TCP. Vou despejá-los e ver se noto algo suspeito.

Eu percebi que mesmo com a mesma linha curl, o gunicorn interrompe a conexão, mas a garçonete a deixa aberta. Nenhuma pista disso ainda, mas é a única coisa que eu pude ver que era diferente.

@tilgovi Acho que o comportamento que você vê com a garçonete poderia ser reproduzido com o trabalhador encadeado. De qualquer forma, obrigado por cuidar disso :)

Olá a todos,
Eu estou passando pelo mesmo problema. Algum de vocês teve a chance de examinar esta questão mais detalhadamente?
@tilgovi @erjiang @benoitc

Saúde
Máxima

@maximkgn você também está usando o flask? Mais detalhes?

Estou usando o Django 1.7.
Tivemos uma determinada pós-resposta que sempre foi maior do que 13k, e com uma certa probabilidade de ~ 0,5 a resposta no cliente seria truncada para um pouco mais de 13k. Nos logs do heroku, vimos o mesmo erro h18, e depois de nos certificarmos de que nenhum erro estava acontecendo em nosso código python, tivemos que concluir que isso acontece na camada gunicorn entre o heroku e nosso python.
Quando mudamos para garçonete / uwsgi, o bug parou de acontecer.

@maximkgn o que acontecerá se você usar a configuração --threads ?

Alguém pode testar isso?

Tenho o mesmo problema com o frasco e o gunicorn (versões testadas 19.3 e 19.4.5). @benoitc Tentei 1, 2 e 4 threads (com a opção

Deixe-me saber se eu posso ajudar a testar isso de alguma forma.

@cbaines como são os pedidos?

Friendpaste é capaz de aceitar mais de 1 milhão de postagens .... então certamente não há um limite dentro do gunicorn.

nunca teve uma resposta. encerrando o problema, pois não é reproduzível. Sinta-se à vontade para reabrir um, se necessário.

Ainda se reproduz após atualizar as dependências para incluir o Flask 1.0.2 e o gunicorn 19.9.0. Pode ser bom chamar a atenção de alguém da Heroku sobre isso - ouvi dizer que eles têm algumas pessoas dedicadas ao Python.

Veja o compromisso mais recente aqui: https://github.com/erjiang/gunicorn-issue/

Também estou recebendo esse erro H18 em uma grande solicitação GET regularmente.

Mudar para garçonete corrigiu o problema. Não sei por que o gunicorn o produz, mas o mesmo código exato está sendo executado.

corpo de resposta é 21,54 KB

Ainda se reproduz após atualizar as dependências para incluir o Flask 1.0.2 e o gunicorn 19.9.0. Pode ser bom chamar a atenção de alguém da Heroku sobre isso - ouvi dizer que eles têm algumas pessoas dedicadas ao Python.

Veja o compromisso mais recente aqui: https://github.com/erjiang/gunicorn-issue/

Criei um tíquete de suporte no Heroku. Será atualizado aqui se algo útil vier disso.

@benoitc parece que @erjiang forneceu um exemplo reproduzível. Podemos abrir isso de volta?

Reaberto. Vou me auto-atribuir e dar uma olhada quando puder.

Ainda se reproduz após atualizar as dependências para incluir o Flask 1.0.2 e o gunicorn 19.9.0. Pode ser bom chamar a atenção de alguém da Heroku sobre isso - ouvi dizer que eles têm algumas pessoas dedicadas ao Python.
Veja o compromisso mais recente aqui: https://github.com/erjiang/gunicorn-issue/

Criei um tíquete de suporte no Heroku. Será atualizado aqui se algo útil vier disso.

Você recebeu uma resposta de heroku?

Consegui reproduzir usando o caso de teste em https://github.com/erjiang/gunicorn-issue (que está usando gunicorn 19.9.0, Python 2.7.14, sync worker, --workers 4 ). É importante notar que a saída do registro de acesso do gunicorn relata que ele acredita ter retornado um HTTP 200.

Atualizar para Python 3.7.3 + gunicorn master e reduzir para --workers 1 não teve efeito sobre a reprodutibilidade, no entanto, alternar de trabalhador de sincronização para gevent fez com que o erro ocorresse com menos frequência (embora ainda acontecesse). Usar --log-level debug não revelou nada significativo (a única saída adicional durante a solicitação foi a linha [DEBUG] POST /test1 ).

Em seguida, tentei --spew , mas o problema não era mais reproduzido. Isso me levou a tentar adicionar time.sleep(1) antes de resp.close() aqui, o que evitou o problema de forma semelhante.

Assim, parece que a causa é que o buffer de envio do soquete pode não estar vazio no momento do close() , o que pode causar a perda da resposta:

Nota: close() libera o recurso associado a uma conexão, mas não necessariamente fecha a conexão imediatamente. Se você quiser fechar a conexão em tempo hábil, ligue para shutdown() antes de close() .

(Consulte https://docs.python.org/3/library/socket.html#socket.socket.close)

Adicionar sock.shutdown(socket.SHUT_RDWR) ( docs ) antes de sock.close() aqui resolveu o problema para mim. Uma solução alternativa talvez seja usar SO_LINGER , embora pelo que li isso tenha compensações.

É difícil encontrar documentos sobre esse assunto, mas descobri:
https://stackoverflow.com/questions/8874021/close-socket-directly-after-send-unsafe
https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable

Espero que ajude :-)

STR completo:

  1. Crie uma conta Heroku gratuita em https://signup.heroku.com
  2. Instale o Heroku CLI (consulte https://devcenter.heroku.com/articles/heroku-cli)
  3. Faça login no CLI usando heroku login
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (isso cria um aplicativo Heroku gratuito com nome gerado aleatoriamente e configura um remoto git chamado heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (falha> 75% das vezes)
  8. Quando terminar, execute heroku destroy para excluir o aplicativo.

@tilgovi Parece que @edmorley produziu uma explicação plausível do que está errado. Você consegue dar uma olhada e ver qual é a solução certa? Eu também poderia enviar um PR para adicionar sock.shutdown() mas não sei o suficiente para dizer se é a correção certa ou se afetaria negativamente outras situações.

Olá, eu encontrei o mesmo problema com tamanho de resposta de 503 KB. Os dados de resposta são uma matriz JSON.
O comportamento observado é:

  1. Vejo o corpo da resposta truncado e o cliente http (Chrome, curl) ainda está esperando pela resposta.
  2. ~ 75% das solicitações têm tempo de resposta entre 120-130 segundos. As solicitações restantes são resolvidas em 400 ms.
  3. As solicitações com tamanho de resposta pequeno são rápidas.

É reproduzível em ambos:

  1. instalação local do Docker no Windows 10
  2. Executando o contêiner do docker no AWS ECS

Configuração do ambiente de tempo de execução
imagem meinheld-gunicorn-docker marcada como _python3.6_ com Python 3.6.7, Flask 1.0.2, flask-restplus 0.12.1, simpe Flask-caching

Configuração do Docker : 3 CPUs, RAM 1024 MB

Configuração Gunicorn :

  • trabalhadores = 2 * CPUs + 1 (recomendado por doc)
  • threads = 1 (mesmo comportamento com 2 * threads de CPUs)
  • worker_class = " ovo: meinheld # gunicorn_worker "

Em https://github.com/benoitc/gunicorn/issues/2015, outra pessoa teve problemas com o enforcamento de um trabalhador meinheld e o uso de um tipo de trabalhador diferente resolveu o problema. Eu me pergunto se há um problema geral com isso. @stapetro, você pode tentar um trabalhador diferente?

Olá @jamadden ,
Sua sugestão corrigiu o problema. Não há problema com as classes de trabalho _gevent_ e _gthread_. Eu me afastei de meinheld. Obrigado pela resposta rápida e ajuda! :)

STR completo:

  1. Crie uma conta Heroku gratuita em https://signup.heroku.com
  2. Instale o Heroku CLI (consulte https://devcenter.heroku.com/articles/heroku-cli)
  3. Faça login no CLI usando heroku login
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (isso cria um aplicativo Heroku gratuito com nome gerado aleatoriamente e configura um remoto git chamado heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (falha> 75% das vezes)
  8. Quando terminar, execute heroku destroy para excluir o aplicativo.

Tive um comportamento muito semelhante em meu aplicativo e descobri que, ao usar curl -H em vez de curl --data (já que é uma solicitação GET), ele funciona para meu aplicativo (Django, Gunicorn, Heruko). Eu não testei no aplicativo gunicorn-issue. Achei que isso pudesse ser útil para alguém.

@mikkelhn Yesss. Um aplicativo com Flask / Flask RestPlus e Gunicorn se comporta desta forma: responder à solicitação POST dá erro 503 [se carga útil> 13k], enquanto o erro não acontece se o aplicativo responde a um GET. Exatamente o mesmo código!
Alguém pode explicar esse comportamento tão irritante? Mudar para garçonete é a única solução alternativa para corrigir esse problema? Acho que modificar Gunicorn "manualmente" não é uma solução viável ...

Fui em frente e abri um PR para chamar shutdown () antes de fechar (). Francamente, é um pouco estranho que o Heroku continue recomendando o Gunicorn quando ele é quebrado por padrão no Heroku.

Se, como @erijang afirma corretamente, Heroku recomenda Gunicorn quando Gunicorn não é o caminho a seguir: quais são as alternativas simples e viáveis ​​para Gunicorn (e como configurá-las da melhor forma no Heroku)?
AFAIK, muitos clientes escolhem o Heroku apenas porque ele não deve exigir um conhecimento profundo em arquiteturas de servidor e detalhes de configuração ...: |

@RinaldoNani o que você quer dizer? Também de qual trabalhador estamos falando? .

@benoitc Este problema afeta vários tipos de trabalhadores, conforme mencionado em:
https://github.com/benoitc/gunicorn/issues/840#issuecomment -482491267

Olá @benoitc. Como mencionei em um post anterior, implantamos um aplicativo Flask / FlaskRestPlus bem simples no Heroku, seguindo com cuidado as diretrizes do Heroku para implantação de aplicativos do lado do servidor Python / Flask (que, pelo que entendi, sugerem o uso da sincronização "web" do Gunicorn trabalhadores ).

O comportamento de nosso aplicativo reflete o título deste tópico.

Testado localmente, tudo funciona bem, o aplicativo oferece 20k + JSON sem problemas; mas quando o aplicativo é implantado no Heroku, o problema de erro 503 se torna sistemático: mesmo sem literalmente nenhum tráfego, a saída não é entregue.
Como outros apontaram, os logs mostram que no nível HTTP tudo parece ok (200 códigos de resposta são registrados).
Se a carga útil for inferior a 13k, o Heroku / Gunicorn responderá aos POSTs conforme o esperado.
Seguimos a ideia de

Não somos especialistas em Gunicorn e, francamente, esperávamos que nosso caso de uso simples pudesse funcionar "fora da caixa".
Se você tiver alguma sugestão para nos ajudar, seremos eternamente gratos :)

@RinaldoNani Filmado no escuro ... em algum lugar em seu gerenciador de solicitações, tente ler request.data . Por exemplo:

@route('/whatever', methods=['POST'])
def whatever_handler():
    str(request.data)
    return flask.jsonify(...)

Isso tem algum efeito sobre seus erros?

Estou escrevendo isso à 1:00 da manhã, depois de lidar com o problema H18 por mais de 2 semanas (mal podia esperar para compartilhar).

Estou trabalhando com um enorme conjunto de dados e respondendo de 18 mil a 20 mil registros para traçar. H18 veio como um erro muito aleatório. Funcionaria bem às vezes, mas exibiria "O comprimento do cabeçalho do conteúdo não corresponde" em todos os navegadores. Tentei quase todas as soluções discutidas sobre esse problema, mas não tive sorte. Tentei duas coisas que finalmente funcionaram:

  1. Alterou a solicitação POST para GET.
  2. Meus dados tinham valores NaN / Null, então mudei meu modelo e forneci um valor padrão. (Acho que isso resolveu o problema)
    Depois disso, parei de receber esse erro.
    Espero que isso possa ajudar alguém!
Esta página foi útil?
0 / 5 - 0 avaliações