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

Я работаю над приложением gRPC, которое будет обслуживаться через HAProxy в режиме http. Если серверное приложение немедленно (т. е. перед отправкой каких-либо ответов) прерывает потоковый вызов ответа с конкретной ошибкой, то клиентское приложение получит ошибку вместо той, которая была отправлена. Подробности об ошибке будут получены 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: сеанс находился на этапе ДАННЫХ.

Кроме того:

SD Соединение с сервером разорвалось из-за ошибки при передаче данных. Обычно это означает, что 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 , на данном этапе все в порядке. Я также кратко просмотрел RFC 7540 и не смог найти ничего, что было бы не так с точки зрения протокола 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кадр на месте, поэтому сведения об ошибке передаются клиенту, но код изменился наCANCEL. По сути, клиент получает все ожидаемые данные, но после этого он получает неожиданные данные.RST_STREAM(CANCEL)это отображается на gRPCCANCELLEDошибка, как указано в документации 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.

0 ответов

Другие вопросы по тегам