Gunicorn: Le POST échoue lors du retour d'une réponse > 13k sur Heroku

Créé le 4 août 2014  ·  34Commentaires  ·  Source: benoitc/gunicorn

Bonjour, nous avons rencontré ce problème en production en utilisant Flask + Gunicorn + Heroku et nous n'avons pas pu trouver de cause ou de solution de contournement.

Pour une requête POST particulière avec des paramètres POST, la requête échouerait avec une erreur H18 (sock=backend) dans le routeur de Heroku indiquant que le serveur a fermé le socket alors qu'il n'aurait pas dû.

Nous avons commencé à réduire la taille de réponse de ce point de terminaison défaillant jusqu'à ce que nous la réduisions à environ 13 000. Si nous envoyions moins de 13k, la réponse fonctionnerait toujours. Si nous envoyions plus de 13k, la réponse ne fonctionnerait presque toujours pas.

Le code pour reproduire cela est disponible sur https://github.com/erjiang/gunicorn-issue - il suffit de déployer le référentiel sur Heroku tel quel et de suivre les instructions du fichier README.

( Feedback Requested unconfirmed help wanted - Bugs -

Commentaire le plus utile

J'ai pu reproduire à l'aide du cas de test sur https://github.com/erjiang/gunicorn-issue (qui utilise gunicorn 19.9.0, Python 2.7.14, sync worker, --workers 4 ). Il est à noter que la sortie du journal d'accès de gunicorn indique qu'il pense avoir renvoyé un HTTP 200.

La mise à jour vers Python 3.7.3 + gunicorn master , et la réduction à --workers 1 n'ont eu aucun effet sur la reproductibilité, mais le passage du travailleur de synchronisation à gevent a rendu l'erreur moins fréquente (même si c'était toujours le cas). L'utilisation de --log-level debug n'a rien révélé de significatif (la seule sortie supplémentaire lors de la requête était la ligne [DEBUG] POST /test1 ).

Ensuite, j'ai essayé --spew , mais le problème ne s'est plus reproduit. Cela m'a amené à essayer d'ajouter un time.sleep(1) avant le resp.close() ici, ce qui a également empêché le problème.

En tant que tel, il semble que la cause en soit que le tampon d'envoi du socket pourrait ne pas être vide au moment du close() , ce qui peut entraîner la perte de la réponse :

Remarque : close() libère la ressource associée à une connexion mais ne ferme pas nécessairement immédiatement la connexion. Si vous souhaitez mettre fin à la connexion rapidement, appelez le shutdown() avant le close() .

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

L'ajout de sock.shutdown(socket.SHUT_RDWR) ( docs ) avant le sock.close() ici a résolu le problème pour moi. Une solution alternative pourrait peut-être être d'utiliser SO_LINGER , bien que d'après ce que j'ai lu, il y a des compromis.

Les docs sur ce sujet sont difficiles à trouver, mais j'ai trouvé:
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

J'espère que cela pourra aider :-)

Tous les 34 commentaires

Rapport vraiment utile, merci @erjiang.

Je n'ai pas de compte Heroku pour tester. Quelqu'un avec un tel compte peut-il le tester? cc @tilgovi @kennethreitz

Heureux de mais je n'y arriverai probablement pas de sitôt.

Pour vérifier rapidement l'état d'esprit, je l'ai exécuté localement et j'ai vérifié quelques éléments avec curl pour comparer la serveuse et le gunicorn :

  • [x] Contenu-Longueur identique
  • [x] Même contenu du corps
  • [x] Même encodage de transfert (aucun des deux ne spécifie de segmentation, les deux utilisent Content-Length)

Ensuite, je suis curieux de savoir s'il existe des différences au niveau TCP. Je vais les vider et voir si je remarque quelque chose de louche.

J'ai remarqué que même avec la même ligne de curl, gunicorn supprime la connexion mais la serveuse la laisse ouverte. Aucun indice à ce sujet pour le moment, mais c'est la _seule_ chose que j'ai pu voir qui était différente.

@tilgovi Je suppose que le comportement que vous voyez avec la serveuse pourrait être reproduit avec le travailleur fileté. En tout cas merci de t'en occuper :)

Salut tout le monde,
Je rencontre le même problème. L'un d'entre vous a-t-il eu l'occasion d'approfondir cette question ?
@tilgovi @erjiang @benoitc

Acclamations
Maxime

@maximkgn utilisez -vous également un flacon ? Plus de détails ?

J'utilise Django 1.7.
Nous avons eu une certaine réponse post qui était toujours plus longue que 13k, et avec une certaine probabilité ~0,5 la réponse dans le client serait tronquée à un peu plus de 13k. Dans les journaux heroku, nous avons vu la même erreur h18, et après nous être assurés qu'aucune erreur ne se produise dans notre code python, nous avons dû conclure que cela se produisait dans la couche gunicorn entre heroku et notre python.
Lorsque nous sommes passés à serveuse/uwsgi, le bogue a cessé de se produire. .

@maximkgn que se passe-t-il si vous utilisez le paramètre --threads ?

Quelqu'un peut-il tester ça ?

J'ai le même problème avec flask et gunicorn (versions testées 19.3 et 19.4.5). @benoitc J'ai essayé 1, 2 et 4 threads (avec l'option --threads), et cela ne fait aucune différence.

Faites-moi savoir si je peux aider à tester cela de quelque manière que ce soit?

@cbaines à quoi ressemblent les demandes ?

Friendpaste est capable d'accepter plus de 1 million de messages... il n'y a donc certainement pas de limite à l'intérieur de gunicorn.

jamais eu de réponse. fermer le problème car il n'est pas reproductible. N'hésitez pas à en rouvrir un si besoin.

Se reproduit toujours après la mise à jour des dépendances pour inclure Flask 1.0.2 et gunicorn 19.9.0. Cela pourrait être bien d'attirer l'attention de quelqu'un chez Heroku à ce sujet - j'ai entendu dire qu'ils avaient des gens dédiés à Python.

Voir le dernier commit ici : https://github.com/erjiang/gunicorn-issue/

Je reçois également régulièrement cette erreur H18 sur une grande demande GET.

Passer à la serveuse a résolu le problème. Je ne sais pas pourquoi gunicorn le produit, mais le même code exact est exécuté.

le corps de la réponse est de 21,54 Ko

Se reproduit toujours après la mise à jour des dépendances pour inclure Flask 1.0.2 et gunicorn 19.9.0. Cela pourrait être bien d'attirer l'attention de quelqu'un chez Heroku à ce sujet - j'ai entendu dire qu'ils avaient des gens dédiés à Python.

Voir le dernier commit ici : https://github.com/erjiang/gunicorn-issue/

J'ai créé un ticket d'assistance sur Heroku. Sera mis à jour ici si quelque chose d'utile en découle.

@benoitc ressemble à @erjiang a fourni un exemple reproductible. Pourrions-nous ouvrir cette sauvegarde ?

Ré-ouvert. Je m'auto-attribuerai et je regarderai quand je le pourrai.

Se reproduit toujours après la mise à jour des dépendances pour inclure Flask 1.0.2 et gunicorn 19.9.0. Cela pourrait être bien d'attirer l'attention de quelqu'un chez Heroku à ce sujet - j'ai entendu dire qu'ils avaient des gens dédiés à Python.
Voir le dernier commit ici : https://github.com/erjiang/gunicorn-issue/

J'ai créé un ticket d'assistance sur Heroku. Sera mis à jour ici si quelque chose d'utile en découle.

As-tu eu une réponse de heroku ?

J'ai pu reproduire à l'aide du cas de test sur https://github.com/erjiang/gunicorn-issue (qui utilise gunicorn 19.9.0, Python 2.7.14, sync worker, --workers 4 ). Il est à noter que la sortie du journal d'accès de gunicorn indique qu'il pense avoir renvoyé un HTTP 200.

La mise à jour vers Python 3.7.3 + gunicorn master , et la réduction à --workers 1 n'ont eu aucun effet sur la reproductibilité, mais le passage du travailleur de synchronisation à gevent a rendu l'erreur moins fréquente (même si c'était toujours le cas). L'utilisation de --log-level debug n'a rien révélé de significatif (la seule sortie supplémentaire lors de la requête était la ligne [DEBUG] POST /test1 ).

Ensuite, j'ai essayé --spew , mais le problème ne s'est plus reproduit. Cela m'a amené à essayer d'ajouter un time.sleep(1) avant le resp.close() ici, ce qui a également empêché le problème.

En tant que tel, il semble que la cause en soit que le tampon d'envoi du socket pourrait ne pas être vide au moment du close() , ce qui peut entraîner la perte de la réponse :

Remarque : close() libère la ressource associée à une connexion mais ne ferme pas nécessairement immédiatement la connexion. Si vous souhaitez mettre fin à la connexion rapidement, appelez le shutdown() avant le close() .

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

L'ajout de sock.shutdown(socket.SHUT_RDWR) ( docs ) avant le sock.close() ici a résolu le problème pour moi. Une solution alternative pourrait peut-être être d'utiliser SO_LINGER , bien que d'après ce que j'ai lu, il y a des compromis.

Les docs sur ce sujet sont difficiles à trouver, mais j'ai trouvé:
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

J'espère que cela pourra aider :-)

STR complet :

  1. Créez un compte Heroku gratuit sur https://signup.heroku.com
  2. Installez la CLI Heroku (voir https://devcenter.heroku.com/articles/heroku-cli)
  3. Connectez-vous à la CLI en utilisant heroku login
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (cela crée une application Heroku gratuite avec un nom généré aléatoirement et configure une télécommande git nommée heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (échec >75% du temps)
  8. Une fois terminé, exécutez heroku destroy pour supprimer l'application.

@tilgovi On dirait que @edmorley a produit une explication plausible de ce qui ne va pas. Êtes-vous en mesure de jeter un coup d'œil et de voir quelle est la bonne solution? Je pourrais aussi soumettre un PR pour ajouter sock.shutdown() mais je ne sais pas assez pour dire si c'est la bonne solution ou si cela affecterait négativement d'autres situations.

Bonjour, j'ai rencontré le même problème avec une taille de réponse de 503 Ko. Les données de réponse sont un tableau JSON.
Le comportement observé est :

  1. Je vois un corps de réponse tronqué et le client http (Chrome, curl) attend toujours la réponse.
  2. Environ 75 % des demandes ont un temps de réponse compris entre 120 et 130 secondes. Les requêtes restantes sont résolues en moins de 400 ms.
  3. Les demandes avec une petite taille de réponse sont rapides.

C'est reproductible sur les deux :

  1. installation locale de Docker sur Windows 10
  2. Exécution du conteneur Docker sur AWS ECS

Configuration de l'environnement d'exécution
image meinheld-gunicorn-docker étiquetée comme _python3.6_ avec Python 3.6.7, Flask 1.0.2, flask-restplus 0.12.1, simpe Flask-caching

Configuration Docker : 3 CPU, RAM 1024 Mo

Configuration de la licorne :

  • travailleurs = 2*CPU + 1 (recommandé par le doc)
  • threads=1 (même comportement avec les threads 2*CPUs)
  • worker_class=" egg:meinheld#gunicorne_worker "

Dans https://github.com/benoitc/gunicorn/issues/2015, quelqu'un d'autre a eu des problèmes avec un travailleur meinheld suspendu, et l'utilisation d'un type de travailleur différent a résolu le problème. Je me demande s'il y a un problème général avec ça. @stapetro pouvez-vous essayer un autre travailleur ?

Bonjour @jamadden ,
Votre suggestion a résolu le problème. Il n'y a aucun problème avec les classes de travail _gevent_ et _gthread_. Je me suis éloigné de moi. Merci pour la réponse rapide et l'aide! :)

STR complet :

  1. Créez un compte Heroku gratuit sur https://signup.heroku.com
  2. Installez la CLI Heroku (voir https://devcenter.heroku.com/articles/heroku-cli)
  3. Connectez-vous à la CLI en utilisant heroku login
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (cela crée une application Heroku gratuite avec un nom généré aléatoirement et configure une télécommande git nommée heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (échec >75% du temps)
  8. Une fois terminé, exécutez heroku destroy pour supprimer l'application.

J'ai eu un comportement très similaire sur mon application et j'ai découvert que lorsque j'utilisais curl -H au lieu de curl --data (puisqu'il s'agit d'une requête GET), cela fonctionnait pour mon application (Django, Gunicorn, Heruko). Je n'ai pas testé sur l'application gunicorn-issue. J'ai pensé que cela pourrait être utile à quelqu'un.

@mikkelhn Ouiss. Une application avec Flask/Flask RestPlus et Gunicorn se comporte de cette manière : répondre à la requête POST donne une erreur 503 [si charge utile > 13k], tandis que l'erreur ne se produit
Quelqu'un peut-il expliquer ce comportement très ennuyeux? Le passage à la serveuse est-il la seule solution de contournement pour résoudre ce problème ? Je pense que modifier Gunicorn "à la main" n'est pas une solution viable...

Je suis allé de l'avant et j'ai ouvert un PR pour appeler shutdown() avant close(). Franchement, c'est un peu sauvage qu'Heroku continue de recommander Gunicorn alors qu'il est cassé par défaut sur Heroku.

Si, comme @erijang l' indique correctement, Heroku recommande Gunicorn alors que Gunicorn n'est pas la voie à suivre : quelles sont des alternatives simples et viables à Gunicorn (et comment les configurer au mieux sur Heroku) ?
AFAIK, de nombreux clients choisissent Heroku simplement parce qu'il ne devrait pas nécessiter une connaissance approfondie des architectures de serveur et des détails de configuration... :|

@RinaldoNani qu'est-ce que tu veux dire ? De quel travailleur parle-t-on aussi ? .

@benoitc Ce problème affecte plusieurs types de travailleurs, comme mentionné dans :
https://github.com/benoitc/gunicorn/issues/840#issuecomment -482491267

Salut @benoitc. Comme je l'ai mentionné dans un article précédent, nous avons déployé une application Flask / FlaskRestPlus assez simple sur Heroku, en suivant avec soin les directives de Heroku pour le déploiement d'applications côté serveur Python / Flask (qui, si je comprends bien, suggèrent l'utilisation de Gunicorn sync "web" ouvriers ).

Le comportement de notre application reflète le titre de ce fil.

Testée localement, tout fonctionne bien, l'application fournit 20k+ JSON sans problème ; mais lorsque l'application est déployée sur Heroku, le problème d'erreur 503 devient systématique : même avec littéralement aucun trafic, la sortie n'est pas livrée.
Comme d'autres l'ont souligné, les journaux montrent qu'au niveau HTTP, tout semble ok (le code de réponse 200 est enregistré).
Si la charge utile est inférieure à 13k, Heroku/Gunicorn répond aux POST comme prévu.
Nous avons suivi l'idée de @mikkelhn d'éviter les points de terminaison POST (?!?) et d'utiliser GET à la place, et cela semble un moyen (pas très agréable) de résoudre le problème.

Nous ne sommes pas des experts de Gunicorn, et franchement, nous nous attendions à ce que notre cas d'utilisation simple puisse fonctionner "out of the box".
Si vous avez des suggestions pour nous aider, nous vous en serons éternellement reconnaissants :)

@RinaldoNani Tourné dans le noir... quelque part dans votre gestionnaire de requêtes, essayez de lire tout request.data . Par exemple:

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

Cela a-t-il un effet sur vos erreurs ?

J'écris ceci à 1h00 du matin après avoir bousculé le problème H18 pendant plus de 2 semaines maintenant (j'avais hâte de partager).

Je travaille avec un énorme ensemble de données et je réponds à des enregistrements de 18K à 20K à tracer. H18 est venu comme une erreur très aléatoire. Cela fonctionnerait bien parfois, mais renverrait "La longueur de l'en-tête du contenu ne correspond pas" sur tous les navigateurs. J'ai essayé presque toutes les solutions discutées à propos de ce problème, mais je n'ai pas eu de chance. J'ai essayé 2 choses qui ont finalement fonctionné :

  1. Modification de la requête POST en GET.
  2. Mes données avaient des valeurs NaN/Null, j'ai donc changé de modèle et fourni une valeur par défaut. (je pense que cela a résolu le problème)
    Après cela, j'ai cessé de recevoir cette erreur.
    J'espère que cela pourra aider quelqu'un !
Cette page vous a été utile?
0 / 5 - 0 notes