Não é possível receber erro gRPC durante uma chamada de streaming de resposta usando HAProxy no modo HTTP

Não é possível receber erro gRPC durante uma chamada de streaming de resposta usando HAProxy no modo HTTP

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á CANCELLEDum 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 Streame End Headerssinalizadores definidos. E então fecha o stream com NO_ERRORcó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 ACKe, 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 HEADERSframe estão no lugar, então os detalhes do erro são passados ​​​​para o cliente, mas o código do RST_STREAMfoi 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 CANCELLEDerro 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_shutrfunçã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_STREAMcó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-composeambiente de rede, incluindo HAProxy configurado.

informação relacionada