Estou trabalhando em um aplicativo gRPC que será veiculado por trás do HAProxy usando o modo http. Se o aplicativo do servidor imediatamente (ou seja, antes de enviar qualquer resposta) abortar a chamada de streaming de resposta com um erro específico, o aplicativo cliente receberá CANCELLED
um erro em vez daquele que foi enviado. Os detalhes do erro serãoRST_STREAM recebido com código de erro 8. Estou trabalhando com HAProxy 2.3.2 e grpc 1.34.0.
Para cada uma dessas solicitações, a entrada de log HAProxy possui SD--
sinalizadores definidos noestado da sessão na desconexãocampo, por exemplo.
<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"
Na documentação do HAProxy, esses sinalizadores são definidos da seguinte forma:
- S: a sessão TCP foi abortada inesperadamente pelo servidor ou o servidor a recusou explicitamente.
- D: a sessão estava na fase DATA.
Adicionalmente:
SDA conexão com o servidor morreu devido a um erro durante a transferência de dados. Isso geralmente significa que o haproxy recebeu um RST do servidor ou uma mensagem ICMP de um equipamento intermediário durante a troca de dados com o servidor. Isso pode ser causado por uma falha no servidor ou por um problema de rede em um equipamento intermediário.
Sabendo disso, e tendo em mente que a conexão HTTP foi aberta com a intenção de streaming de dados, como solução alternativa tentei enviar uma mensagem gRPC vazia antes de gerar o erro. A solução ajudou parcialmente - os códigos de erro poderiam ter sido recebidos pelo cliente para a maioria das solicitações, mas o problema ainda acontecia de vez em quando.
Na próxima etapa, inspecionei o tráfego de rede usando o wireshark. A seguir está um rastreamento da resposta HTTP servida pelo servidor gRPC em um evento de interrupção imediata da chamada:
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)
Assim, o servidor envia cabeçalhos de resposta com detalhes sobre erros End Stream
e End Headers
sinalizadores definidos. E então fecha o stream com NO_ERROR
código. De acordo com a resposta fornecida emhttps://stackoverflow.com/questions/55511528/should-grpc-server-side-half-closing-implicitly-terminate-the-client/55522312está tudo bem nesta fase. Também revisei brevemente oRFC 7540e não consegui encontrar nada que estivesse errado nos termos do protocolo HTTP/2.
A resposta HTTP do servidor gRPC citada é seguida por HAProxy originando TCP ACK
e, em seguida, HAProxy despacha sua resposta para o cliente.
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)
Percebe-se que os flags e todo o conteúdo do HEADERS
frame estão no lugar, então os detalhes do erro são passados para o cliente, mas o código do RST_STREAM
foi alterado para CANCEL
. Com efeito o cliente recebe todos os dados esperados, mas depois recebe inesperados RST_STREAM(CANCEL)
que estão sendo mapeados no CANCELLED
erro gRPC, conforme especificado noDocumentação gRPC.
No decorrer de uma investigação mais aprofundada, referi-me ao código-fonte do HAProxy. Eu descobri que o código está definido noh2_do_shutr
função demux_h2.c
(experimentos com compilações personalizadas do HAProxy provaram que é realmente este lugar). A ramificação do código envolvida possui o seguinte comentário:
uma resposta final já foi fornecida, não queremos mais esse stream. Isto pode acontecer quando o servidor responde antes do final de um upload e fecha rapidamente (redirecionar, negar, ...)
Então esses são os detalhes que consegui reunir sobre o assunto. Não tenho certeza se o problema está no núcleo do gRPC (sendo muito confuso em termos de manipulação de fluxos HTTP2) ou no HAProxy (sendo muito descuidado ao reescrever RST_STREAM
códigos). A questão final é: como posso ajustar a configuração do servidor principal HAProxy e gRPC para funcionar corretamente em um evento de interrupção imediata da chamada. A configuração mínima do HAProxy que reproduz o problema é a seguinte:
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
Eu preparei umrepositório com exemplo mínimocontendo cliente e servidor python simples. Ele também contém docker-compose
ambiente de rede, incluindo HAProxy configurado.