Gunicorn: POST falla al devolver> 13k respuesta en Heroku

Creado en 4 ago. 2014  ·  34Comentarios  ·  Fuente: benoitc/gunicorn

Hola, resolvimos este problema en producción usando Flask + Gunicorn + Heroku y no pudimos encontrar una causa o una solución.

Para una solicitud POST en particular con parámetros POST, la solicitud fallaría con un error H18 (sock = backend) en el enrutador de Heroku que indica que el servidor cerró el socket cuando no debería haberlo hecho.

Comenzamos a disminuir el tamaño de respuesta de ese extremo fallido hasta que lo redujimos a alrededor de la marca 13k. Si enviamos menos de 13k, la respuesta siempre funcionará. Si enviáramos más de 13k, la respuesta casi siempre no funcionaría.

El código para reproducir esto está disponible en https://github.com/erjiang/gunicorn-issue ; simplemente implemente el repositorio en Heroku tal como está y siga las instrucciones en el archivo README.

( Feedback Requested unconfirmed help wanted - Bugs -

Comentario más útil

Pude reproducir usando el caso de prueba en https://github.com/erjiang/gunicorn-issue (que usa gunicorn 19.9.0, Python 2.7.14, sync worker, --workers 4 ). Es de destacar que la salida del registro de acceso de gunicorn informa que cree que ha devuelto un HTTP 200.

Actualizar a Python 3.7.3 + gunicorn master , y reducir a --workers 1 no tuvo ningún efecto en la reproducibilidad, sin embargo, cambiar de sync worker a gevent hizo que el error ocurriera con menos frecuencia (aunque aún lo hizo). El uso de --log-level debug no reveló nada significativo (la única salida adicional durante la solicitud fue la línea [DEBUG] POST /test1 ).

Luego probé --spew , sin embargo, el problema ya no se reproducía. Esto me llevó a intentar agregar un time.sleep(1) antes del resp.close() aquí, lo que evitó el problema de manera similar.

Como tal, parece que la causa es que el búfer de envío del socket podría no estar vacío en el momento de close() , lo que puede hacer que se pierda la respuesta:

Nota: close() libera el recurso asociado con una conexión pero no necesariamente cierra la conexión inmediatamente. Si desea cerrar la conexión de manera oportuna, llame al shutdown() antes del close() .

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

Agregar sock.shutdown(socket.SHUT_RDWR) ( docs ) antes de sock.close() aquí resolvió el problema. Una solución alternativa tal vez podría ser usar SO_LINGER , aunque por lo que he leído tiene ventajas y desventajas.

Los documentos sobre este tema son difíciles de conseguir, pero encontré:
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 ayude :-)

Todos 34 comentarios

Informe realmente útil, gracias @erjiang.

No tengo una cuenta de Heroku para probar. ¿Alguien con esa cuenta puede probarlo? cc @tilgovi @kennethreitz

Feliz de hacerlo, pero probablemente no lo haré pronto.

Como una verificación rápida de cordura, lo ejecuté localmente y verifiqué algunas cosas con curl para comparar camarera y gunicorn:

  • [x] Contenido-Longitud igual
  • [x] Mismo contenido corporal
  • [x] Misma codificación de transferencia (ninguno especifica fragmentado, ambos usan Content-Length)

A continuación, tengo curiosidad por saber si quizás existen diferencias a nivel de TCP. Los sacaré y veré si noto algo sospechoso.

Me di cuenta de que incluso con la misma línea de rizo, gunicorn deja de conectar, pero la camarera la deja abierta. Aún no hay pistas de eso, pero es la única cosa que pude ver que era diferente.

@tilgovi Supongo que el comportamiento que ves con la camarera podría reproducirse con el trabajador enhebrado. De todos modos gracias por encargarme de esto :)

Hola todos,
Estoy experimentando el mismo problema. ¿Alguno de ustedes tuvo la oportunidad de examinar este tema más a fondo?
@tilgovi @erjiang @benoitc

Salud
Máxima

@maximkgn ¿también estás usando frasco? ¿Más detalles?

Estoy usando django 1.7.
Tuvimos una determinada respuesta posterior que siempre fue más larga que 13k, y con una cierta probabilidad ~ 0.5 la respuesta en el cliente se truncaría a un poco más de 13k. En los registros de heroku vimos el mismo error h18, y después de asegurarnos de que no ocurriera ningún error en nuestro código de Python, tuvimos que concluir que ocurre en la capa gunicorn entre heroku y nuestra Python.
Cuando cambiamos a waitress / uwsgi, el error dejó de ocurrir.

@maximkgn ¿qué sucede si usa la configuración --threads ?

¿Alguien puede probar esto?

Tengo el mismo problema con flask y gunicorn (versiones probadas 19.3 y 19.4.5). @benoitc Probé 1, 2 y 4 subprocesos (con la opción --threads), y no hace ninguna diferencia.

Avíseme si puedo ayudar a probar esto de alguna manera.

@cbaines ¿cómo se

Friendpaste puede aceptar más de 1 millón de publicaciones ... por lo que ciertamente no hay un límite dentro de gunicorn.

nunca tuve una respuesta. cerrando el problema ya que no es reproducible. No dude en volver a abrir uno si es necesario.

Aún se reproduce después de actualizar las dependencias para incluir Flask 1.0.2 y gunicorn 19.9.0. Sin embargo, podría ser bueno llamar la atención de alguien en Heroku sobre esto; escuché que tienen algunas personas dedicadas a Python.

Vea la última confirmación aquí: https://github.com/erjiang/gunicorn-issue/

También recibo este error H18 en una solicitud GET grande con regularidad.

Cambiar a camarera solucionó el problema. No estoy seguro de por qué gunicorn lo produce, pero se está ejecutando exactamente el mismo código.

El cuerpo de respuesta es de 21,54 KB.

Aún se reproduce después de actualizar las dependencias para incluir Flask 1.0.2 y gunicorn 19.9.0. Sin embargo, podría ser bueno llamar la atención de alguien en Heroku sobre esto; escuché que tienen algunas personas dedicadas a Python.

Vea la última confirmación aquí: https://github.com/erjiang/gunicorn-issue/

Creé un ticket de soporte en Heroku. Se actualizará aquí si algo útil proviene de él.

@benoitc parece que @erjiang proporcionó un ejemplo reproducible. ¿Podríamos abrir esta copia de seguridad?

Reabierto. Me autoasignaré y echaré un vistazo cuando pueda.

Aún se reproduce después de actualizar las dependencias para incluir Flask 1.0.2 y gunicorn 19.9.0. Sin embargo, podría ser bueno llamar la atención de alguien en Heroku sobre esto; escuché que tienen algunas personas dedicadas a Python.
Vea la última confirmación aquí: https://github.com/erjiang/gunicorn-issue/

Creé un ticket de soporte en Heroku. Se actualizará aquí si algo útil proviene de él.

¿Recibiste una respuesta de heroku?

Pude reproducir usando el caso de prueba en https://github.com/erjiang/gunicorn-issue (que usa gunicorn 19.9.0, Python 2.7.14, sync worker, --workers 4 ). Es de destacar que la salida del registro de acceso de gunicorn informa que cree que ha devuelto un HTTP 200.

Actualizar a Python 3.7.3 + gunicorn master , y reducir a --workers 1 no tuvo ningún efecto en la reproducibilidad, sin embargo, cambiar de sync worker a gevent hizo que el error ocurriera con menos frecuencia (aunque aún lo hizo). El uso de --log-level debug no reveló nada significativo (la única salida adicional durante la solicitud fue la línea [DEBUG] POST /test1 ).

Luego probé --spew , sin embargo, el problema ya no se reproducía. Esto me llevó a intentar agregar un time.sleep(1) antes del resp.close() aquí, lo que evitó el problema de manera similar.

Como tal, parece que la causa es que el búfer de envío del socket podría no estar vacío en el momento de close() , lo que puede hacer que se pierda la respuesta:

Nota: close() libera el recurso asociado con una conexión pero no necesariamente cierra la conexión inmediatamente. Si desea cerrar la conexión de manera oportuna, llame al shutdown() antes del close() .

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

Agregar sock.shutdown(socket.SHUT_RDWR) ( docs ) antes de sock.close() aquí resolvió el problema. Una solución alternativa tal vez podría ser usar SO_LINGER , aunque por lo que he leído tiene ventajas y desventajas.

Los documentos sobre este tema son difíciles de conseguir, pero encontré:
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 ayude :-)

STR completo:

  1. Cree una cuenta gratuita de Heroku en https://signup.heroku.com
  2. Instale la CLI de Heroku (consulte https://devcenter.heroku.com/articles/heroku-cli)
  3. Inicie sesión en la CLI usando heroku login
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (esto crea una aplicación Heroku gratuita con un nombre generado aleatoriamente y configura un control remoto git llamado heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (falla> 75% del tiempo)
  8. Cuando termine, ejecute heroku destroy para eliminar la aplicación.

@tilgovi Parece que @edmorley produjo una explicación plausible de lo que está mal. ¿Puede echar un vistazo y ver cuál es la solución correcta? También podría enviar un PR para agregar sock.shutdown() pero no sé lo suficiente para decir si es la solución correcta o si afectaría negativamente otras situaciones.

Hola, tuve el mismo problema con el tamaño de respuesta de 503 KB. Los datos de respuesta son una matriz JSON.
El comportamiento observado es:

  1. Veo el cuerpo de respuesta truncado y el cliente http (Chrome, curl) todavía está esperando la respuesta.
  2. ~ 75% de las solicitudes experimentan un tiempo de respuesta de entre 120 y 130 segundos. Las solicitudes restantes se resuelven en menos de 400 ms.
  3. Las solicitudes con un tamaño de respuesta pequeño son rápidas.

Es reproducible en ambos:

  1. instalación local de Docker en Windows 10
  2. Ejecución de un contenedor de Docker en AWS ECS

Configuración del entorno de ejecución
imagen de meinheld-gunicorn-docker etiquetada como _python3.6_ con Python 3.6.7, Flask 1.0.2, flask-restplus 0.12.1, simpe Flask-caching

Configuración de Docker : 3 CPU, RAM 1024 MB

Configuración de Gunicorn :

  • trabajadores = 2 * CPU + 1 (recomendado por doc)
  • subprocesos = 1 (mismo comportamiento con 2 * subprocesos de CPU)
  • worker_class = " huevo: meinheld # gunicorn_worker "

En https://github.com/benoitc/gunicorn/issues/2015, alguien más tuvo problemas con un trabajador de meinheld colgando, y el uso de un tipo de trabajador diferente resolvió el problema. Me pregunto si hay un problema general con eso. @stapetro ¿puedes probar con un trabajador diferente?

Hola @jamadden ,
Tu sugerencia solucionó el problema. No hay ningún problema con las clases de trabajo _gevent_ y _gthread_. Me alejé de mí. ¡Gracias por la rápida respuesta y ayuda! :)

STR completo:

  1. Cree una cuenta gratuita de Heroku en https://signup.heroku.com
  2. Instale la CLI de Heroku (consulte https://devcenter.heroku.com/articles/heroku-cli)
  3. Inicie sesión en la CLI usando heroku login
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (esto crea una aplicación Heroku gratuita con un nombre generado aleatoriamente y configura un control remoto git llamado heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (falla> 75% del tiempo)
  8. Cuando termine, ejecute heroku destroy para eliminar la aplicación.

Tuve un comportamiento muy similar en mi aplicación y descubrí que cuando uso curl -H en lugar de curl --data (ya que es una solicitud GET) funciona para mi aplicación (Django, Gunicorn, Heruko). No he probado en la aplicación gunicorn-issue. Pensé que esto podría ser útil para alguien.

@mikkelhn Yesss. Una aplicación con Flask / Flask RestPlus y Gunicorn se comporta de esta manera: responder a la solicitud POST da un error 503 [si carga útil> 13k], mientras que el error no ocurre si la aplicación responde a un GET. ¡Exactamente el mismo código!
¿Alguien puede explicar este comportamiento tan molesto? ¿Cambiar a camarera es la única solución para solucionar este problema? Siento que modificar Gunicorn "a mano" no es una solución viable ...

Seguí adelante y abrí un PR para llamar a shutdown () antes de close (). Francamente, es un poco salvaje que Heroku continúe recomendando Gunicorn cuando está roto por defecto en Heroku.

Si, como dice correctamente
AFAIK, muchos clientes eligen Heroku solo porque no debería requerir un conocimiento profundo en arquitecturas de servidores y detalles de configuración ...: |

@RinaldoNani ¿a qué te refieres? Además, ¿de qué trabajador estamos hablando? .

@benoitc Este problema afecta a varios tipos de trabajadores, como se menciona en:
https://github.com/benoitc/gunicorn/issues/840#issuecomment -482491267

Hola @benoitc. Como mencioné en una publicación anterior, hemos implementado una aplicación Flask / FlaskRestPlus bastante simple en Heroku, siguiendo con cuidado las pautas de Heroku para la implementación de aplicaciones del lado del servidor Python / Flask (que, según tengo entendido, sugieren el uso de Gunicorn sync "web" trabajadores ).

El comportamiento de nuestra aplicación refleja el título de este hilo.

Probado localmente, todo funciona bien, la aplicación entrega 20k + JSON sin problemas; pero cuando la aplicación se implementa en Heroku, el problema del error 503 se vuelve sistemático: incluso sin tráfico literalmente, la salida no se entrega.
Como señalaron otros, los registros muestran que a nivel HTTP todo parece estar bien (se registra un código de respuesta 200).
Si la carga útil es inferior a 13k, Heroku / Gunicorn responde a los POST como se esperaba.
Seguimos la idea de @mikkelhn de evitar los puntos finales POST (?!?) Y usar GET en su lugar, y esta parece una forma (no muy agradable) de abordar el problema.

No somos expertos en Gunicorn y, francamente, esperábamos que nuestro caso de uso simple pudiera funcionar "fuera de la caja".
Si tiene alguna sugerencia para ayudarnos, estaremos eternamente agradecidos :)

@RinaldoNani Disparo en la oscuridad ... en algún lugar de su controlador de solicitudes, intente leer todo request.data . Por ejemplo:

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

¿Eso tiene algún efecto en tus errores?

Estoy escribiendo esto a la 1:00 a.m. después de apresurarme con el problema H18 durante más de 2 semanas (no podía esperar para compartir).

Estoy trabajando con un conjunto de datos enorme y respondiendo registros de 18K a 20K para trazar. H18 vino como un error muy aleatorio. A veces funcionaría bien, pero arrojaría "La longitud del encabezado del contenido no coincide" en todos los navegadores. Probé casi todas las soluciones discutidas sobre este problema pero no tuve suerte. Había 2 cosas que probé que funcionaron finalmente:

  1. Se cambió la solicitud POST a GET.
  2. Mis datos tenían valores NaN / Null, así que cambié mi modelo y proporcioné un valor predeterminado. (Creo que esto resolvió el problema)
    Después de esto, dejé de recibir este error.
    ¡Espero que esto pueda ayudar a alguien!
¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

leonardbinet picture leonardbinet  ·  4Comentarios

zenglingyu picture zenglingyu  ·  4Comentarios

benoitc picture benoitc  ·  4Comentarios

Abraxos picture Abraxos  ·  4Comentarios

lordmauve picture lordmauve  ·  3Comentarios