Gunicorn: POST schlägt fehl, wenn eine Antwort von >13k auf Heroku . zurückgegeben wird

Erstellt am 4. Aug. 2014  ·  34Kommentare  ·  Quelle: benoitc/gunicorn

Hallo, wir haben dieses Problem in der Produktion mit Flask + Gunicorn + Heroku festgestellt und konnten weder eine Ursache noch einen Workaround finden.

Bei einer bestimmten POST-Anfrage mit POST-Parametern würde die Anfrage mit einem H18-Fehler (sock=backend) im Heroku-Router fehlschlagen, der anzeigt, dass der Server den Socket geschlossen hat, obwohl er dies nicht hätte tun sollen.

Wir haben damit begonnen, die Antwortgröße dieses fehlgeschlagenen Endpunkts zu verringern, bis wir sie auf etwa 13.000 eingegrenzt haben. Wenn wir weniger als 13k gesendet haben, funktioniert die Antwort immer. Wenn wir mehr als 13k gesendet haben, funktioniert die Antwort fast immer nicht.

Code, um dies zu reproduzieren, ist unter https://github.com/erjiang/gunicorn-issue verfügbar – stellen Sie das Repo einfach so wie es ist in Heroku bereit und befolgen Sie die Anweisungen in der README-Datei.

( Feedback Requested unconfirmed help wanted - Bugs -

Hilfreichster Kommentar

Ich konnte mit dem Testfall unter https://github.com/erjiang/gunicorn-issue reproduzieren (der Gunicorn 19.9.0, Python 2.7.14, Sync-Worker, --workers 4 ). Bemerkenswert ist, dass die Zugriffsprotokollausgabe von gunicorn meldet, dass es glaubt, ein HTTP 200 zurückgegeben zu haben.

Das Aktualisieren auf Python 3.7.3 + gunicorn master und die Reduzierung auf --workers 1 hatte keine Auswirkung auf die Reproduzierbarkeit, aber der Wechsel vom Sync-Worker zu gevent führte dazu, dass der Fehler seltener auftrat (obwohl es immer noch der Fall war). Die Verwendung von --log-level debug ergab nichts Wesentliches (die einzige zusätzliche Ausgabe während der Anfrage war die Zeile [DEBUG] POST /test1 ).

Als nächstes versuchte ich --spew , aber das Problem wurde nicht mehr reproduziert. Dies führte mich versuchen Sie , time.sleep(1) vor dem resp.close() hier , das das Problem in ähnlicher Weise verhindert.

Daher scheint die Ursache darin zu liegen, dass der Socket-Sendepuffer zum Zeitpunkt des close() möglicherweise nicht leer ist, was dazu führen kann, dass die Antwort verloren geht:

Hinweis: close() gibt die mit einer Verbindung verknüpfte Ressource frei, schließt die Verbindung jedoch nicht unbedingt sofort. Wenn Sie die Verbindung rechtzeitig beenden möchten, rufen Sie shutdown() vor close() .

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

Das Hinzufügen von sock.shutdown(socket.SHUT_RDWR) ( docs ) vor sock.close() hier löste das Problem für mich. Eine alternative Lösung könnte vielleicht sein, SO_LINGER , obwohl es nach dem, was ich gelesen habe, Kompromisse gibt.

Dokumente zu diesem Thema sind schwer zu bekommen, aber ich fand:
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

Ich hoffe, das hilft :-)

Alle 34 Kommentare

Wirklich hilfreicher Bericht, danke @erjiang.

Ich habe kein Heroku-Konto zum Testen. Kann das jemand mit einem solchen Account testen? cc @tilgovi @kennethreitz

Gerne, aber ich komme wahrscheinlich nicht so schnell dazu.

Als schnellen Gesundheitscheck habe ich es lokal ausgeführt und ein paar Dinge mit curl überprüft, um Kellnerin und Gunicorn zu vergleichen:

  • [x] Inhalt-Länge gleich
  • [x] Gleicher Textinhalt
  • [x] Gleiche Übertragungscodierung (keine gibt Chunked an, beide verwenden Content-Length)

Als nächstes bin ich gespannt, ob es auf TCP-Ebene Unterschiede gibt. Ich werde sie tcpdump und sehen, ob ich etwas faul bemerke.

Mir ist aufgefallen, dass Gunicorn selbst bei derselben Curl-Linie die Verbindung abbricht, aber die Kellnerin lässt sie offen. Davon gibt es noch keine Hinweise, aber es ist das _einzige_, was ich sehen konnte, das anders war.

@tilgovi Ich vermute, dass das Verhalten, das Sie bei der Kellnerin sehen, mit dem Thread-Worker reproduziert werden könnte. Trotzdem danke, dass du dich darum kümmerst :)

Hallo zusammen,
Ich habe das gleiche Problem. Hat jemand von euch die Gelegenheit, dieses Thema genauer zu untersuchen?
@tilgovi @erjiang @benoitc

Beifall
Maxime

@maximkgn verwendest du auch eine Flasche? Noch mehr Details?

Ich verwende Django 1.7.
Wir hatten eine bestimmte Post-Antwort, die immer länger als 13k war, und mit einer gewissen Wahrscheinlichkeit von ~0,5 würde die Antwort im Client auf etwas über 13k gekürzt. In den Heroku-Protokollen sahen wir den gleichen h18-Fehler, und nachdem wir sichergestellt hatten, dass in unserem Python-Code kein Fehler auftritt, mussten wir schlussfolgern, dass er in der Gunicorn-Schicht zwischen Heroku und unserem Python auftritt.
Als wir zu Kellnerin/uwsgi wechselten, trat der Fehler nicht mehr auf. .

@maximkgn was passiert, wenn Sie die Einstellung --threads ?

Kann das jemand testen?

Ich habe das gleiche Problem mit Flakon und Gunicorn (getestete Versionen 19.3 und 19.4.5). @benoitc Ich habe 1, 2 und 4 Threads (mit der Option --threads) ausprobiert, und es macht keinen Unterschied.

Lassen Sie mich wissen, ob ich helfen kann, dies in irgendeiner Weise zu testen?

@cbaines wie sehen die Anfragen aus?

Friendpaste ist in der Lage, mehr als 1 Mio. Posts zu akzeptieren.

hatte nie eine antwort. Schließe das Problem, da es nicht reproduzierbar ist. Fühlen Sie sich frei, einen bei Bedarf wieder zu öffnen.

Reproduziert sich auch nach dem Aktualisieren von Abhängigkeiten, um Flask 1.0.2 und gunicorn 19.9.0 einzuschließen. Könnte jedoch nett sein, jemanden bei Heroku darauf aufmerksam zu machen - ich habe gehört, dass sie einige engagierte Python-Leute haben.

Siehe neuestes Commit hier: https://github.com/erjiang/gunicorn-issue/

Ich erhalte diesen H18-Fehler auch regelmäßig bei einer großen GET-Anfrage.

Der Wechsel zur Kellnerin hat das Problem behoben. Ich bin mir nicht sicher, warum Gunicorn es produziert, aber es wird genau der gleiche Code ausgeführt.

Antworttext ist 21,54 KB

Reproduziert sich auch nach dem Aktualisieren von Abhängigkeiten, um Flask 1.0.2 und gunicorn 19.9.0 einzuschließen. Könnte jedoch nett sein, jemanden bei Heroku darauf aufmerksam zu machen - ich habe gehört, dass sie einige engagierte Python-Leute haben.

Siehe neuestes Commit hier: https://github.com/erjiang/gunicorn-issue/

Ich habe ein Support-Ticket auf Heroku erstellt. Werde hier aktualisieren, wenn etwas Nützliches dabei herauskommt.

@benoitc sieht so aus, als hätte @erjiang ein reproduzierbares Beispiel geliefert. Könnten wir das wieder öffnen?

Wieder geöffnet. Ich werde mich selbst zuordnen und nachsehen, wenn ich kann.

Reproduziert sich auch nach dem Aktualisieren von Abhängigkeiten, um Flask 1.0.2 und gunicorn 19.9.0 einzuschließen. Könnte jedoch nett sein, jemanden bei Heroku darauf aufmerksam zu machen - ich habe gehört, dass sie einige engagierte Python-Leute haben.
Siehe neuestes Commit hier: https://github.com/erjiang/gunicorn-issue/

Ich habe ein Support-Ticket auf Heroku erstellt. Werde hier aktualisieren, wenn etwas Nützliches dabei herauskommt.

Hast du eine Antwort von heroku bekommen?

Ich konnte mit dem Testfall unter https://github.com/erjiang/gunicorn-issue reproduzieren (der Gunicorn 19.9.0, Python 2.7.14, Sync-Worker, --workers 4 ). Bemerkenswert ist, dass die Zugriffsprotokollausgabe von gunicorn meldet, dass es glaubt, ein HTTP 200 zurückgegeben zu haben.

Das Aktualisieren auf Python 3.7.3 + gunicorn master und die Reduzierung auf --workers 1 hatte keine Auswirkung auf die Reproduzierbarkeit, aber der Wechsel vom Sync-Worker zu gevent führte dazu, dass der Fehler seltener auftrat (obwohl es immer noch der Fall war). Die Verwendung von --log-level debug ergab nichts Wesentliches (die einzige zusätzliche Ausgabe während der Anfrage war die Zeile [DEBUG] POST /test1 ).

Als nächstes versuchte ich --spew , aber das Problem wurde nicht mehr reproduziert. Dies führte mich versuchen Sie , time.sleep(1) vor dem resp.close() hier , das das Problem in ähnlicher Weise verhindert.

Daher scheint die Ursache darin zu liegen, dass der Socket-Sendepuffer zum Zeitpunkt des close() möglicherweise nicht leer ist, was dazu führen kann, dass die Antwort verloren geht:

Hinweis: close() gibt die mit einer Verbindung verknüpfte Ressource frei, schließt die Verbindung jedoch nicht unbedingt sofort. Wenn Sie die Verbindung rechtzeitig beenden möchten, rufen Sie shutdown() vor close() .

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

Das Hinzufügen von sock.shutdown(socket.SHUT_RDWR) ( docs ) vor sock.close() hier löste das Problem für mich. Eine alternative Lösung könnte vielleicht sein, SO_LINGER , obwohl es nach dem, was ich gelesen habe, Kompromisse gibt.

Dokumente zu diesem Thema sind schwer zu bekommen, aber ich fand:
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

Ich hoffe, das hilft :-)

Volle STR:

  1. Erstellen Sie ein kostenloses Heroku-Konto unter https://signup.heroku.com
  2. Installieren Sie die Heroku-CLI (siehe https://devcenter.heroku.com/articles/heroku-cli)
  3. Melden Sie sich mit heroku login der CLI an
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (dies erstellt eine kostenlose Heroku-App mit zufällig generiertem Namen und konfiguriert eine Git-Fernbedienung namens heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (fehlt >75% der Zeit)
  8. Wenn Sie fertig sind, führen Sie heroku destroy , um die App zu löschen.

@tilgovi Klingt so, als hätte @edmorley eine plausible Erklärung dafür sock.shutdown() hinzuzufügen, aber ich weiß nicht genug, um zu sagen, ob es die richtige Lösung ist oder ob es andere Situationen negativ beeinflussen würde.

Hallo, ich habe das gleiche Problem mit einer Antwortgröße von 503 KB. Antwortdaten sind ein JSON-Array.
Beobachtetes Verhalten ist:

  1. Ich sehe einen abgeschnittenen Antworttext und der HTTP-Client (Chrome, curl) wartet immer noch auf die Antwort.
  2. ~75 % der Anfragen haben eine Antwortzeit zwischen 120 und 130 Sekunden. Die restlichen Anfragen lösen unter 400 ms auf.
  3. Anfragen mit kleiner Antwortgröße sind schnell.

Es ist auf beiden

  1. lokale Docker-Installation unter Windows 10
  2. Ausführen des Docker-Containers auf AWS ECS

Einrichtung der Laufzeitumgebung
meinheld-gunicorn-docker image markiert als _python3.6_ mit Python 3.6.7, Flask 1.0.2, -restplus 0.12.1, simpe Flask-caching

Docker-Konfiguration : 3 CPUs, RAM 1024 MB

Gunicorn-Konfiguration :

  • Arbeiter = 2*CPUs + 1 (vom Dokument empfohlen)
  • threads=1 (gleiches Verhalten mit 2*CPUs-Threads)
  • worker_class=" egg:meinheld#gunicorn_worker "

In https://github.com/benoitc/gunicorn/issues/2015 hatte jemand anderes Probleme mit einem hängenden meinheld-Worker, und die Verwendung eines anderen Worker-Typs löste das Problem. Ich frage mich, ob es ein allgemeines Problem damit gibt. @stapetro kannst du einen anderen Arbeiter ausprobieren?

Hallo @jamadden ,
Ihr Vorschlag hat das Problem behoben. Es gibt kein Problem mit den Worker-Klassen _gevent_ und _gthread_. Ich bin von meinheld weggezogen. Vielen Dank für die schnelle Antwort und Hilfe! :)

Volle STR:

  1. Erstellen Sie ein kostenloses Heroku-Konto unter https://signup.heroku.com
  2. Installieren Sie die Heroku-CLI (siehe https://devcenter.heroku.com/articles/heroku-cli)
  3. Melden Sie sich mit heroku login der CLI an
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (dies erstellt eine kostenlose Heroku-App mit zufällig generiertem Namen und konfiguriert eine Git-Fernbedienung namens heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (fehlt >75% der Zeit)
  8. Wenn Sie fertig sind, führen Sie heroku destroy , um die App zu löschen.

Ich hatte ein sehr ähnliches Verhalten in meiner App und stellte fest, dass es bei Verwendung von curl -H anstelle von curl --data (da es sich um eine GET-Anforderung handelt) für meine App (Django, Gunicorn, Heruko) funktioniert. Ich habe die gunicorn-issue App nicht getestet. Dachte, das könnte jemandem nützlich sein.

@mikkelhn Jass . Eine App mit Flask/Flask RestPlus und Gunicorn verhält sich folgendermaßen: Die Antwort auf die POST-Anfrage gibt einen 503-Fehler [wenn Nutzlast > 13k], während der Fehler nicht auftritt, wenn die App auf ein GET antwortet. Genau der gleiche Code!
Kann sich jemand dieses sehr nervige Verhalten erklären? Ist der Wechsel zur Kellnerin die einzige Problemumgehung, um dieses Problem zu beheben? Ich glaube, dass das Modifizieren von Gunicorn "von Hand" keine praktikable Lösung ist ...

Ich ging voran und öffnete einen PR, um shutdown() vor close() aufzurufen. Ehrlich gesagt ist es ein bisschen wild, dass Heroku Gunicorn weiterhin empfiehlt, wenn es standardmäßig auf Heroku kaputt ist.

Wenn, wie @erijang richtig sagt, Heroku Gunicorn empfiehlt, wenn Gunicorn nicht der richtige Weg ist: Welche einfachen und praktikablen Alternativen zu Gunicorn sind (und wie man sie am besten auf Heroku konfiguriert)?
AFAIK, viele Kunden entscheiden sich für Heroku, nur weil es keine tiefen Kenntnisse in Serverarchitekturen und Konfigurationsdetails erfordern sollte... :|

@RinaldoNani was meinst du? Und von welchem ​​Arbeiter sprechen wir? .

@benoitc Dieses Problem betrifft mehrere Worker-Typen, wie erwähnt in:
https://github.com/benoitc/gunicorn/issues/840#issuecomment -482491267

Hallo @benoitc. Wie ich in einem früheren Beitrag erwähnt habe, haben wir eine ziemlich einfache Flask / FlaskRestPlus-App auf Heroku bereitgestellt, wobei wir die Richtlinien von Heroku für die serverseitige Anwendungsbereitstellung von Python/Flask sorgfältig befolgen (die, wie ich verstanden habe,

Das Verhalten unserer App spiegelt den Titel dieses Threads wider.

Lokal getestet, alles funktioniert einwandfrei, die App liefert 20k+ JSON ohne Probleme; Aber wenn die App auf Heroku bereitgestellt wird, wird das 503-Fehlerproblem systematisch: Selbst ohne Datenverkehr wird die Ausgabe nicht geliefert.
Wie andere darauf hingewiesen haben, zeigen die Protokolle, dass auf HTTP-Ebene alles in Ordnung zu sein scheint (200 Antwortcode wird protokolliert).
Wenn die Nutzlast weniger als 13.000 beträgt, reagieren Heroku/Gunicorn wie erwartet auf POSTs.
Wir folgten der Idee von @mikkelhn , POST-Endpunkte (?!?) zu vermeiden und stattdessen GET zu verwenden, und dies scheint eine (nicht sehr schöne) Möglichkeit zu sein, das Problem anzugehen.

Wir sind keine Gunicorn-Experten und haben ehrlich gesagt erwartet, dass unser einfacher Anwendungsfall "out of the box" funktionieren könnte.
Wenn Sie Vorschläge haben, uns zu helfen, werden wir Ihnen für immer dankbar sein :)

@RinaldoNani Im Dunkeln request.data lesen. Beispielsweise:

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

Hat das einen Einfluss auf deine Fehler?

Ich schreibe dies um 1:00 Uhr, nachdem ich mich jetzt über 2 Wochen mit der H18-Ausgabe beschäftigt habe (konnte es kaum erwarten, sie zu teilen).

Ich arbeite mit riesigen Datensätzen und reagiere auf 18 000 bis 20 000 Datensätze, um sie zu zeichnen. H18 kam als sehr zufälliger Fehler. Es würde manchmal gut funktionieren, würde aber in allen Browsern "Content-Header-Länge stimmt nicht überein" auslösen. Ich habe fast alle zu diesem Problem besprochenen Lösungen ausprobiert, hatte aber kein Glück. Es gab 2 Dinge, die ich ausprobiert habe, die letztendlich funktioniert haben:

  1. Die POST-Anfrage wurde in GET geändert.
  2. Meine Daten hatten NaN/Null-Werte, also habe ich mein Modell geändert und einen Standardwert angegeben. (Ich denke, das hat das Problem gelöst)
    Danach bekam ich diesen Fehler nicht mehr.
    Hoffe das konnte jemandem helfen!
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen