Nginx Response Buffering - Langsame Clients entkoppeln, ohne Streams zu verzögern
Eine Anwendung kann eine Antwort schnell fertig erzeugen, während die Person beim Herunterladen eine langsame Verbindung nutzt. Wer sollte in dieser Situation warten: die Anwendung oder der vorgeschaltete Reverse Proxy? Die Antwort ändert sich, wenn es sich nicht um eine gewöhnliche HTML-Seite oder ein JSON-Dokument handelt, sondern um einen Stream, dessen Nutzen davon abhängt, dass jede Nachricht zeitnah eintrifft.
Das Response Buffering von Nginx liegt mitten in diesem Zeitproblem. Für einen HTTP-Upstream, der mit proxy_pass erreicht wird, ist Buffering standardmäßig aktiviert. Diese Voreinstellung ist für viele herkömmliche Antworten nützlich, aber für Server-Sent Events, schrittweisen Fortschritt oder andere zeitkritische Ausgaben nicht neutral. Das Ziel besteht deshalb nicht darin, Buffering überall abzuschalten. Stattdessen sollte festgelegt werden, was jeder Endpoint verspricht, und dieses Versprechen bewusst eingehalten werden.
Es gibt drei Uhren, nicht nur eine
An einer Antwort über einen Reverse Proxy sind mindestens drei Parteien beteiligt: der Client, Nginx und die Upstream-Anwendung. Sie können sich mit unterschiedlicher Geschwindigkeit bewegen. Die Anwendung erzeugt Bytes möglicherweise schnell, Nginx liest sie schnell, und der Client verarbeitet sie langsam. Umgekehrt kann die Anwendung alle paar Sekunden eine kleine Nachricht erzeugen, während der Client bereit ist, jede davon sofort zu empfangen.
Laut der offiziellen Referenz zum Nginx-Proxy-Modul liest Nginx bei aktiviertem Response Buffering so schnell wie möglich vom Upstream in die konfigurierten Speicherpuffer. Passt die Antwort nicht vollständig hinein, kann ein Teil in eine temporäre Datei geschrieben werden. Nginx kann die Antwort anschließend im Tempo des Clients weiter ausliefern.
Dadurch werden zwei Beziehungen voneinander getrennt. Der Upstream kommuniziert mit Nginx; der Client kommuniziert ebenfalls mit Nginx. Ein langsamer Client muss den Upstream nicht zwangsläufig während des gesamten Downloads beschäftigen. Der offizielle Reverse-Proxy-Leitfaden nennt diese Entkopplung von langsamen Clients als einen Grund, weshalb Buffering helfen kann.
Bei deaktiviertem Buffering wird die Beziehung enger. Nginx leitet Antwortdaten synchron an den Client weiter, sobald sie vom Upstream eintreffen. Das ist wertvoll, wenn der Zeitpunkt der Auslieferung zum Verhalten des Endpoints gehört, bedeutet aber auch, dass die Upstream-Seite weniger von einem langsam lesenden Client abgeschirmt ist.
Buffering ist kein Caching
Das Wort „Puffer“ lässt sich leicht mit benachbarten Funktionen verwechseln. Response Buffering ist die vorübergehende Verarbeitung einer einzelnen laufenden Antwort. Es macht diese Antwort nicht von selbst für eine spätere Anfrage wiederverwendbar. Die Wiederverwendung über mehrere Requests hinweg gehört zum Response Caching mit eigenen Directives, Schlüsseln, Frische-Regeln und Sicherheitsentscheidungen.
Request Buffering ist wiederum etwas anderes. proxy_request_buffering steuert, ob Nginx den Request Body des Clients liest, bevor er an den Upstream gesendet wird. Das beeinflusst Uploads und das Retry-Verhalten von Requests. proxy_buffering steuert die Antwort in der Gegenrichtung. Eine Änderung der einen Einstellung verändert die andere nicht automatisch.
Diese Unterscheidung ist bei der Diagnose wichtig. „Der Upload erreicht meine Anwendung verspätet“ und „Der Browser erhält gestreamte Events gesammelt in einem Schub“ können umgangssprachlich beide als Buffering beschrieben werden, weisen aber auf unterschiedliche Datenpfade und Directives hin.
Was die Response Buffer tatsächlich steuern
Bei über HTTP weitergeleiteten Antworten legt proxy_buffer_size den Puffer für den ersten Teil der Upstream-Antwort fest, der üblicherweise die Response Header enthält. proxy_buffers bestimmt Anzahl und Größe der für eine Verbindung verfügbaren Puffer. Die dokumentierten Standardwerte hängen von der Speicherseitengröße der Plattform ab, daher ist eine kopierte Zahl nicht automatisch eine Verbesserung.
Überschreitet eine gepufferte Antwort die verfügbaren Speicherpuffer, kann Nginx eine temporäre Datei verwenden. proxy_max_temp_file_size begrenzt diese temporäre Datei für eine Antwort; der Wert null deaktiviert das Puffern von Antworten in temporäre Dateien. Diese Option lässt die Antwort weder verschwinden noch beweist sie, dass der Speicherverbrauch bei Parallelität harmlos bleibt. Sie verändert, wohin überschüssige gepufferte Daten gelangen dürfen.
Ohne die tatsächliche Verteilung der Antwortgrößen, Parallelität, das Speicherbudget, das Storage-Verhalten und die Client-Population eines Dienstes gibt es keine glaubwürdige universelle Antwort wie „sechzehn Puffer verwenden“. Größere Speicherpuffer können die Nutzung temporärer Dateien verringern, gleichzeitig aber den potenziell belegten Speicher über viele gleichzeitige Requests erhöhen. Kleinere Puffer können Druck auf den Storage verlagern oder Probleme mit übergroßen Response Headern sichtbar machen. Die nützliche Frage lautet nicht, welche Zahl schnell aussieht, sondern welche Begrenzung derzeit tatsächlich beobachtbar ist.
Beim Streaming gehört das Timing zur Korrektheit
Eine gewöhnliche Seite bleibt nützlich, wenn sie in wenigen größeren statt in vielen kleinen Teilen eintrifft. Ein Fortschrittsstream ist anders. Sendet eine Anwendung im Zeitverlauf „10 %“, „20 %“ und „30 %“, während ein Intermediary diese Nachrichten sammelt, können die Bytes weiterhin korrekt sein, obwohl das Verhalten für den Benutzer falsch ist.
Server-Sent Events liefern ein konkretes Beispiel. Der WHATWG HTML Living Standard definiert Event Streams mit dem Medientyp text/event-stream und verarbeitet sie zeilenweise. Seine Verarbeitungshinweise warnen davor, dass Block Buffering die Event-Auslieferung verzögern kann. Das bedeutet nicht, dass jeder als „Stream“ bezeichnete Endpoint identische Nginx-Einstellungen benötigt, belegt aber, dass Buffering das beobachtbare SSE-Verhalten beeinflussen kann.
In einer eng begrenzten Location kann das Proxy Response Buffering deaktiviert werden, ohne gewöhnliche Routen zu verändern:
location /events/ {
proxy_pass http://app_backend;
proxy_buffering off;
}
Dieses Beispiel behandelt nur einen Mechanismus. Der Upstream muss weiterhin vollständige Event-Datensätze ausgeben und sie durch seine eigene Runtime flushen. Auch Kompression, ein weiterer Proxy, ein CDN, die Client-Bibliothek und Netzwerkbedingungen können die Auslieferung beeinflussen. Das Abschalten des Nginx-Bufferings kann eine Anwendung nicht ausgleichen, die ihre Ausgabe in einem Puffer der Programmiersprache zurückhält.
Die Anwendung kann außergewöhnliche Antworten kennzeichnen
Manchmal liefert eine Route sowohl herkömmliche Antworten als auch einen Stream. Das Proxy-Modul erkennt außerdem den Upstream-Response-Header X-Accel-Buffering. Eine Anwendung kann für eine Antwort, die Pass-through-Verhalten benötigt, X-Accel-Buffering: no zurückgeben und Buffering für normale Antworten aktiviert lassen. Nginx verarbeitet diesen Header, sofern seine Behandlung nicht mit proxy_ignore_headers deaktiviert wurde.
Dieser Header ist ein Nginx-Kontrollmechanismus und kein allgemeines HTTP-Versprechen, das jeder Intermediary versteht. Befindet sich vor Nginx ein weiterer Reverse Proxy oder ein CDN, muss dessen Buffering-Verhalten separat geprüft werden. Der anwendungsgesteuerte Ansatz ist nützlich, wenn die Anwendung die Semantik der Antwort besser kennt als ein breites URL-Muster; er erzeugt jedoch auch einen schichtübergreifenden Vertrag, der dokumentiert und getestet werden sollte.
Was bei deaktiviertem Buffering verloren geht
Eine geringere Auslieferungsverzögerung ist keine kostenlose Einstellung. Ohne Response Buffering sind Upstream-Produktion und Client-Konsum direkter gekoppelt. Ein langsamer oder pausierender Client kann den Antwortpfad länger aktiv halten. Die Auswirkungen auf die Kapazität hängen vom Upstream-Server, seinem Parallelitätsmodell, Verbindungslimits, der Antwortrate und der Zahl gleichzeitiger Streams ab; aus einer einzelnen Directive lassen sie sich nicht ableiten.
Auch die Fehlerbehandlung hat eine harte Grenze. Die proxy_next_upstream-Dokumentation weist darauf hin, dass Nginx einen Request nur dann an einen anderen Upstream weitergeben kann, wenn noch nichts an den Client gesendet wurde. Sobald eine Teilantwort sichtbar ist, kann ein zweiter Server ihren Anfang nicht unbemerkt ersetzen. Diese Einschränkung ist beim Streaming relevant, weil die Ausgabe absichtlich beginnt, bevor der Vorgang abgeschlossen ist.
Deaktiviertes Buffering definiert außerdem keine Liveness. proxy_read_timeout misst die Pause zwischen zwei aufeinanderfolgenden Leseoperationen vom Upstream und nicht die Dauer der gesamten Antwort. Ein langlebiger Stream braucht daher eine Entscheidung auf Anwendungsebene zu Leerlaufzeiten, Heartbeats, Reconnection und der Bedeutung einer ausbleibenden Nachricht. Ein höherer Timeout ohne dieses Modell kann die Fehlererkennung lediglich verschieben.
Beide Seiten von Nginx beobachten
Eine Stoppuhr im Browser kann allein nicht erklären, wo Zeit verbraucht wurde. Nginx stellt clientseitige und upstreamseitige Zeitvariablen bereit, die diese Grenze sichtbarer machen. Die Referenz zum Log-Modul definiert $request_time als Zeit vom Lesen der ersten Client-Bytes bis zum Log-Schreibvorgang nach dem Senden der letzten Antwort-Bytes. Das Upstream-Modul stellt separat $upstream_header_time und $upstream_response_time bereit.
Ein vorübergehendes oder sorgfältig beibehaltenes Access-Log-Format könnte diese Grenzen enthalten:
log_format upstream_timing
'$request status=$status bytes=$body_bytes_sent '
'request_time=$request_time '
'upstream_header_time=$upstream_header_time '
'upstream_response_time=$upstream_response_time';
Diese Werte müssen interpretiert werden. Eine hohe $request_time neben einer niedrigeren Upstream-Zeit kann zu einer langsamen Client-Auslieferung passen, beweist aber nicht, dass eine bestimmte Puffereinstellung falsch ist. Retries können mehrere Upstream-Werte erzeugen, bei Streaming-Antworten ist der „Abschluss“ absichtlich spät, und aggregierter Traffic ist aussagekräftiger als ein einzelner Request. Vergleichen Sie ähnliche Endpoints unter repräsentativen Bedingungen und prüfen Sie die Nginx Error Logs auf Warnungen zu temporären Dateien, statt anhand einer isolierten Zeile zu optimieren.
Eine Location ändern und anschließend das Versprechen prüfen
Ein vorsichtiger Prozess beginnt beim Verhalten, nicht bei Directives:
- Den Endpoint als begrenzte Antwort, große Übertragung oder zeitkritischen Stream klassifizieren.
- Das Symptom festhalten: verzögertes erstes Event, durch langsame Clients gebundener Upstream, Warnungen zu temporären Dateien, Speicherdruck oder etwas anderes.
- Client-sichtbares Timing und Upstream-Timing messen, bevor die Konfiguration geändert wird.
- Die kleinste Location-spezifische Änderung anwenden, mit der sich die Hypothese prüfen lässt.
- Normale und absichtlich langsame Clients validieren und danach Parallelität sowie Ressourcenverbrauch beobachten.
Vor einem Reload sollte der dokumentierte Konfigurationstest verwendet werden:
sudo nginx -t
Die Nginx-Referenz zu Kommandozeilenparametern erklärt, dass -t die Konfigurationssyntax prüft und versucht, referenzierte Dateien zu öffnen. Erst nach einem erfolgreichen Test sollte die geprüfte Konfiguration über das normale Service-Management-Verfahren des Servers neu geladen werden. Ein erfolgreicher Syntax-Test beweist nicht, dass Events rechtzeitig eintreffen, der Speicherverbrauch akzeptabel ist oder ein Upstream korrekt flusht. Das sind Laufzeiteigenschaften und sie benötigen Laufzeitprüfungen.
Den Standardfall gewöhnlich halten und Ausnahmen sichtbar machen
Response Buffering ist keine veraltete Optimierung, die automatisch entfernt werden sollte, und Streaming ist kein Grund, es für eine gesamte Website abzuschalten. Buffering kann einen Upstream fertig werden lassen, während Nginx einen langsameren Client bedient. Pass-through-Verhalten kann das Timing bewahren, wenn die schrittweise Auslieferung Teil des Endpoint-Vertrags ist. Beide lösen unterschiedliche Probleme.
Ein vertretbarer Ausgangspunkt ist zurückhaltend: Buffering für gewöhnliche begrenzte Antworten beibehalten, Streaming-Ausnahmen eng und sichtbar gestalten und Puffergrößen-Folklore vermeiden. Die endgültige Entscheidung sollte auf Belegen aus dem tatsächlichen Antwortpfad beruhen. Wenn Anwendung, Proxy und Client jeweils einer anderen Uhr folgen, sollte die Konfiguration mit der Frage beginnen, auf welche Uhr der Benutzer wartet.
