Ich arbeite an einer gRPC-Anwendung, die hinter HAProxy im HTTP-Modus bereitgestellt wird. Wenn die Serveranwendung den Antwort-Streaming-Aufruf sofort (d. h. vor dem Senden von Antworten) mit einem bestimmten Fehler abbricht, erhält die Clientanwendung CANCELLED
anstelle des gesendeten Fehlers einen Fehler. Die Fehlerdetails werden seinRST_STREAM mit Fehlercode 8 empfangen. Ich arbeite mit HAProxy 2.3.2 und grpc 1.34.0.
Für jede dieser Anfragen sind im HAProxy-Logeintrag SD--
Flags gesetzt inSitzungsstatus bei TrennungFeld, zB.
<134>Jan 9 18:09:39 1a8328663d74 haproxy[8]: 172.28.0.4:41698 [09/Jan/2021:18:09:39.346] grpc-server-fe grpc-server-fe/grpc-server-be 0/0/0/-1/+0 -1 +115 - - SD-- 1/1/0/0/0 0/0 "POST http://proxy:6000/service.Service/StreamStream HTTP/2.0"
In der HAProxy-Dokumentation werden diese Flags wie folgt definiert:
- S: Die TCP-Sitzung wurde vom Server unerwartet abgebrochen oder der Server hat sie ausdrücklich abgelehnt.
- D: Die Sitzung war in der DATA-Phase.
Zusätzlich:
SDDie Verbindung zum Server ist während der Datenübertragung aufgrund eines Fehlers abgebrochen. Dies bedeutet normalerweise, dass haproxy beim Datenaustausch mit dem Server eine RST vom Server oder eine ICMP-Nachricht von einem Zwischengerät empfangen hat. Dies kann durch einen Serverabsturz oder ein Netzwerkproblem auf einem Zwischengerät verursacht werden.
Da ich das wusste und bedachte, dass die HTTP-Verbindung mit der Absicht des Datenstreamings geöffnet wurde, habe ich als Workaround versucht, eine leere gRPC-Nachricht zu senden, bevor ich den Fehler gemeldet habe. Die Lösung hat teilweise geholfen – die Fehlercodes konnten vom Client für die meisten Anfragen empfangen werden, aber das Problem trat trotzdem von Zeit zu Zeit auf.
Als nächsten Schritt habe ich den Netzwerkverkehr mit Wireshark untersucht. Es folgt eine Ablaufverfolgung der HTTP-Antwort, die vom gRPC-Server bei einem sofortigen Anrufabbruch bereitgestellt wird:
HyperText Transfer Protocol 2
Stream: SETTINGS, Stream ID: 0, Length 0
Length: 0
Type: SETTINGS (4)
Flags: 0x01
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0000 = Stream Identifier: 0
Stream: HEADERS, Stream ID: 1, Length 88, 200 OK
Length: 88
Type: HEADERS (1)
Flags: 0x05
.... ...1 = End Stream: True
.... .1.. = End Headers: True
.... 0... = Padded: False
..0. .... = Priority: False
00.0 ..0. = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
[Pad Length: 0]
Header Block Fragment: 88400c636f6e74656e742d74797065106170706c69636174...
[Header Length: 120]
[Header Count: 4]
Header: :status: 200 OK
Header: content-type: application/grpc
Header: grpc-status: 7
Header: grpc-message: Details sent by the server
Stream: RST_STREAM, Stream ID: 1, Length 4
Length: 4
Type: RST_STREAM (3)
Flags: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
Error: NO_ERROR (0)
Der Server sendet also Antwortheader mit Details zum Fehler End Stream
und End Headers
gesetzten Flags. Und schließt dann den Stream mit NO_ERROR
Code. Laut der Antwort inhttps://stackoverflow.com/questions/55511528/should-grpc-server-side-half-closing-implicitly-terminate-the-client/55522312alles ist in diesem Stadium in Ordnung. Ich habe auch kurz dieRFC 7540und konnte nichts finden, was in den Bedingungen des HTTP/2-Protokolls nicht stimmt.
Auf die zitierte HTTP-Antwort des gRPC-Servers folgt das von HAProxy stammende TCP ACK
, und anschließend versendet HAProxy seine Antwort an den Client.
HyperText Transfer Protocol 2
Stream: HEADERS, Stream ID: 1, Length 75, 200 OK
Length: 75
Type: HEADERS (1)
Flags: 0x05
.... ...1 = End Stream: True
.... .1.. = End Headers: True
.... 0... = Padded: False
..0. .... = Priority: False
00.0 ..0. = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
[Pad Length: 0]
Header Block Fragment: 885f106170706c69636174696f6e2f67727063000b677270...
[Header Length: 120]
[Header Count: 4]
Header: :status: 200 OK
Header: content-type: application/grpc
Header: grpc-status: 7
Header: grpc-message: Details sent by the server
Stream: RST_STREAM, Stream ID: 1, Length 4
Length: 4
Type: RST_STREAM (3)
Flags: 0x00
0000 0000 = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
Error: CANCEL (8)
Es ist ersichtlich, dass die Flags und der gesamte Inhalt des HEADERS
Frames vorhanden sind, sodass die Fehlerdetails an den Client weitergegeben werden, der Code von sich jedoch RST_STREAM
in geändert hat CANCEL
. Tatsächlich erhält der Client alle erwarteten Daten, danach jedoch unerwartete Daten, RST_STREAM(CANCEL)
die auf einen gRPC-Fehler abgebildet werden CANCELLED
, wie in dergRPC-Dokumentation.
Im Zuge weiterer Untersuchungen habe ich auf den HAProxy-Quellcode verwiesen. Ich habe festgestellt, dass der Code in derh2_do_shutr
Die Funktion vonmux_h2.c
(Experimente mit benutzerdefinierten HAProxy-Builds haben bewiesen, dass es tatsächlich dieser Ort ist). Der betroffene Codezweig hat den folgenden Kommentar:
eine endgültige Antwort wurde bereits bereitgestellt, wir möchten diesen Stream nicht mehr. Dies kann passieren, wenn der Server vor dem Ende eines Uploads antwortet und schnell schließt (Umleitung, Ablehnung, ...)
Das sind also die Details, die ich zu diesem Problem zusammentragen konnte. Ich bin mir nicht ganz sicher, ob das Problem beim gRPC-Kern liegt (der in Bezug auf die Handhabung von HTTP2-Streams zu chaotisch ist) oder bei HAProxy (der beim Umschreiben RST_STREAM
von Codes zu nachlässig ist). Die letzte Frage ist, wie ich die Konfiguration von HAProxy und dem gRPC-Kernserver so anpassen kann, dass sie bei einem sofortigen Anrufabbruch korrekt funktionieren. Die minimale HAProxy-Konfiguration, die das Problem reproduziert, ist wie folgt:
global
log stdout local0
listen grpc-server-fe
bind *:6000 proto h2
mode http
log global
option logasap
option httplog
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
server grpc-server-be server:6000 proto h2
Ich habe einRepository mit Minimalbeispielenthält einfachen Python-Client und -Server. Es enthält auch docker-compose
eine Netzwerkumgebung einschließlich konfiguriertem HAProxy.