Я работаю над приложением 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.