Невозможно получить ошибку gRPC во время вызова потокового ответа с использованием HAProxy в режиме HTTP

Невозможно получить ошибку gRPC во время вызова потокового ответа с использованием HAProxy в режиме HTTP

Я работаю над приложением gRPC, которое будет обслуживаться за HAProxy с использованием режима http. Если серверное приложение немедленно (т. е. до отправки каких-либо ответов) прерывает вызов потоковой передачи ответов с определенной ошибкой, то клиентское приложение получит CANCELLEDошибку вместо той, которая была отправлена. Подробности ошибки будутПолучен RST_STREAM с кодом ошибки 8. Я работаю с HAProxy 2.3.2 и grpc 1.34.0.

Для каждого из таких запросов в записи журнала HAProxy установлены SD-- флагисостояние сеанса при отключенииполе, например.

<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"

В документации HAProxy эти флаги определены следующим образом:

  • S: сеанс TCP был неожиданно прерван сервером, или сервер явно отклонил его.
  • D: сеанс находился в фазе ДАННЫХ.

Кроме того:

СДСоединение с сервером прервалось из-за ошибки во время передачи данных. Обычно это означает, что haproxy получил RST от сервера или ICMP-сообщение от промежуточного оборудования во время обмена данными с сервером. Это может быть вызвано сбоем сервера или сетевой проблемой на промежуточном оборудовании.

Зная это и имея в виду, что HTTP-соединение было открыто с намерением потоковой передачи данных, в качестве обходного пути я попробовал отправить пустое сообщение gRPC перед тем, как выдать ошибку. Решение помогло частично — коды ошибок могли быть получены клиентом для большинства запросов, но проблема все еще возникала время от времени.

В качестве следующего шага я проверил сетевой трафик с помощью wireshark. Ниже приведена трассировка HTTP-ответа, обслуживаемого сервером gRPC в случае немедленного прерывания вызова:

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)

Итак, сервер отправляет заголовки ответа с подробностями об ошибке End Streamи End Headersустановленными флагами. А затем закрывает поток с NO_ERRORкодом. Согласно ответу, предоставленному вhttps://stackoverflow.com/questions/55511528/should-grpc-server-side-half-closing-implicitly-terminate-the-client/55522312на этом этапе все в порядке. Я также кратко рассмотрелRFC7540и не смог найти ничего, что было бы не так с точки зрения протокола HTTP/2.

За цитируемым HTTP-ответом сервера gRPC следует исходный TCP-ответ HAProxy ACK, после чего HAProxy отправляет свой ответ клиенту.

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)

Видно, что флаги и все содержимое кадра HEADERSна месте, поэтому сведения об ошибке передаются клиенту, но код RST_STREAMизменился на CANCEL. По сути, клиент получает все ожидаемые данные, но после этого он получает неожиданные данные, RST_STREAM(CANCEL)которые сопоставляются с CANCELLEDошибкой gRPC, как указано вдокументация gRPC.

В ходе дальнейшего расследования я обратился к исходному коду HAProxy. Я обнаружил, что код установлен вh2_do_shutrфункцияmux_h2.c(эксперименты с пользовательскими сборками HAProxy доказали, что это действительно то место). В задействованной ветке кода есть следующий комментарий:

окончательный ответ уже был предоставлен, мы больше не хотим этот поток. Это может произойти, когда сервер отвечает до окончания загрузки и быстро закрывается (перенаправление, отклонение, ...)

Вот подробности, которые мне удалось собрать по этой проблеме. Я не совсем уверен, заключается ли проблема в ядре gRPC (слишком беспорядочном в плане обработки потоков HTTP2) или в HAProxy (слишком небрежном при переписывании RST_STREAMкодов). Последний вопрос: как настроить конфигурацию HAProxy и сервера ядра gRPC для корректной работы в случае немедленного прерывания вызова. Минимальная конфигурация HAProxy, воспроизводящая проблему, выглядит следующим образом:

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

Я подготовилрепозиторий с минимальным примеромсодержащий простой клиент и сервер python. Он также содержит docker-composeсетевую среду, включая настроенный HAProxy.

Связанный контент