Während eines Antwort-Streaming-Aufrufs mit HAProxy im HTTP-Modus kann kein gRPC-Fehler empfangen werden

Während eines Antwort-Streaming-Aufrufs mit HAProxy im HTTP-Modus kann kein gRPC-Fehler empfangen werden

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 CANCELLEDanstelle 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 Streamund End Headersgesetzten Flags. Und schließt dann den Stream mit NO_ERRORCode. 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 HEADERSFrames vorhanden sind, sodass die Fehlerdetails an den Client weitergegeben werden, der Code von sich jedoch RST_STREAMin 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_shutrDie 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_STREAMvon 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-composeeine Netzwerkumgebung einschließlich konfiguriertem HAProxy.

verwandte Informationen