Em um ambiente de produção caracterizado por alta concorrência, onde milhares de conexões HTTP/2 permanecem ativas simultaneamente, observamos falhas intermitentes em clientes que enviam dados via streaming RPC. Os sintomas reportados incluíam:
- Uma sessão funcionando corretamente por algum tempo e, subitamente, a chamada
Sendretornando EOF. - No lado do cliente, o log indicava uma exceção de tipo connection reset by peer.
- No servidor, o contexto associado à requisição encerrava com o status context canceled.
O cenário envolvia ambientes de rede desestabilizados e um processamento massivo de threads no backend. Para identificar a causa raiz, foram realizados testes de packet capture (tcpdump) simultâneos entre os pontos de extremidade ao longo de um período de monitoramento contínuo.
A análise dos registros de rede confirmou a suspeita inicial relacionada aos mecanismos de verificação de integridade do protocolo. Enquanto a comunicação normal alternava periodicamente sinais de verificação, durante as falhas identificadas, os pacotes do servidor não retornavam resposta, resultando no encerramento imediato do canal TCP.
Comportamento Padrão do Keepalive
Para evitar vazamento de recursos causados por clientes desconectados abruptamente, a biblioteca padrão do gRPC ativa o parâmetro Keepalive. Esta funcionalidade insere intervalos regulares de "ping" para garantir que o link esteja ativo.
A configuração padrão geralmente segue este modelo:
import (
"time"
"google.golang.org/grpc/keepalive"
)
// Definição da estrutura de parâmetros de vida útil
params := keepalive.ServerParameters{
Time: 60 * time.Second, // Intervalo para envio do ping
Timeout: 5 * time.Second, // Tempo máximo de espera pela resposta
}
// Aplicação nas opções do servidor
serverOpts := []grpc.ServerOption{
grpc.KeepaliveParams(params),
}
s := grpc.NewServer(serverOpts...)
Nesse esquema, se um pacote de verificação for enviado mas nenhum dado (nem mesmo dados do stream) for recebido dentro da janela de Timeout, o servidor assume que o cliente está offline e fecha a conexão.
Análise da Lógica Interna
Examinando a implementação interna em grpc/internal/transport/http2_server.go, observa-se o seguinte fluxo de controle:
- O serviço inicia um timer após enviar o ping, configurando o contador restante como
timeout. - Enquanto aguarda, o sistema verifica se houve qualquer atividade na conexão antes do contador zerar.
- Se o outstandingPing estiver ativo, novos pings são suprimidos até que a espera conclua ou uma resposta seja obtida.
- Caso o limite temporal seja atingido sem atividade registrada, o estado da conexão é marcado para fechamento imediato.
O ponto crítico reside na lógica de espera única. Se a rede sofrer congestionamento momentâneo ou o servidor estiver sob alto Carga de CPU, o pong pode ser demorado ou descartado. Como a lógica impede novos pings enquanto aguarda o primeiro, uma simples oscilação de rede pode desencadear o encerramento correto da ligação.
Estratégia de Resolução
Precisamos ajustar os tempos de tolerância para acomodar variações de latência sem sacrificar a detecção de conexões verdadeiramente mortas. A estratégia adotada envolveu ampliar significativamente a janela de Timeout.
A nova configuração proposta:
import (
"time"
"google.golang.org/grpc/keepalive"
)
func configureServerKAl() []grpc.ServerOption {
// Ajuste agressivo do timeout para evitar falsos positivos em redes instáveis
kaParams := keepalive.ServerParameters{
Time: 30 * time.Second, // Mantém intervalo razoável de verificação
Timeout: 90 * time.Second, // Aumenta drasticamente a margem de erro
}
return []grpc.ServerOption{
grpc.KeepaliveParams(kaParams),
}
}
De acordo com a documentação interna do código, o servidor aguarda a duração definida em Timeout desde o último sinal de atividade após iniciar o verificador. Desde que ocorra qualquer troca de dados válidos nesse período (incluindo mesnagens de aplicação normais), o contador é redefinido e a conexão permanece viva.
Considerando que o caso de uso envolve troca frequente de pacotes de dados, uma janela de 90 segundos oferece robustez suficiente contra picos de latência transitórios, resolvendo o problema de fecho prematuro sem comprometer a segurança do recurso.