Невозможно получить ошибку 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.