tcp: broken pipe. E agora?
tl;dr: Kong Ingress Controller era o culpado. Suas configurações padrões de timeout estavam fechando a conexão antes que o arquivo pudesse ser totalmente enviado. Se você está enfrentando esse problema em uma longa requisição, cheque as configurações do seu proxy reverso, pois ele pode conter uma configuração diferente de sua aplicação. ;-)
Nós, do time de alocação do Grupo SBF, temos um serviço HTTP escrito em Go que executa uma consulta no BigQuery e gera um grande csv como resposta. Entretanto, após algum tempo começamos a receber o seguinte erro ao invés de nosso arquivo:
write tcp 10.0.0.1:8080->10.0.0.2:38302: write: broken pipe
Isso é uma grande surpresa, pois jamais havíamos visto esse erro… Afinal, o que isso quer dizer? Uma pesquisa rápida nos trouxe a seguinte definição:
A condition in programming (also known in POSIX as EPIPE error code and SIGPIPE signal), when a process requests an output to pipe or socket, which was closed by peer
Hmm, isso definitivamente nos dá uma luz sobre o problema. Considerando que o servidor HTTP é provido pelo poderoso pacote net/http da biblioteca padrão de Go, nós temos algumas pistas por onde começar.
A Cloudflare possui um ótimo artigo sobre as configurações padrões do servidor HTTP de Go e como evitar alguns tiros no pé. Pulamos diretamente para a parte que diz respeito aos timeouts e conferimos se não esquecemos de nada.
srv := &http.Server{
ReadTimeout: 10 * time.Minute, // 10 minutes
WriteTimeout: 10 * time.Minute,
Addr: ":8080",
Handler: r,
}
Para se ter uma ideia, nossa aplicação leva em média 2 minutos para completar a requisição. Isso não deveria ocorrer pois temos 10 minutos até que um erro 504 seja retornado.
Curiosamente, não recebemos um erro ao enviar a requisição para um servidor local. Melhor! Comparando nosso ambiente local com o ambiente de produção, percebemos que nossa conexão era fechada em exatamente 1 minuto de execução. Portanto, tem que ser algo entre nosso cliente e nosso servidor!
Sabendo que fazemos deploy para um cluster Kubernetes com o Kong Ingress Controller (controlando 😜) tomando conta de nosso proxy reverso, checamos sua documentação e… Bingo! Essa é a raíz de nosso problema! De acordo com a documentação do Kong Ingress Controller, o timeout padrão é de 60.000 milissegundos — em outras palavras, 1 minuto!
Replicando o comportamento
Antes de fazermos qualquer coisa em nossos servidores, por quê não replicamos este comportamento de forma local? Para isto, nós podemos utilizar uma imagem Docker do nginx e um simples servidor HTTP escrito em Go com uma funcionalidade parecida com a de nosso serviço.
A ideia por trás do teste é configurar um endpoint que leve bastante tempo escrevendo em um buffer, enquanto nosso proxy reverso possuirá um timeout de 2 segundos.
Servidor Go e Dockerfile
Configuração do nginx e Dockerfile
Docker Compose
Por último, utilizaremos o Docker Compose para nos auxiliar com a orquestração desses containers.
Rodando e testando
Depois de configurar nosso ambiente, podemos testá-lo com os comandos abaixo:
docker-compose up --build
para rodar nossos containerscurl localhost
para fazer uma requisição em nosso servidor
Voilà! O erro aparece, confirmando nossa teoria!
goservice_1 | 2022/04/07 01:12:14 error writing: write tcp 172.18.0.2:8080->172.18.0.3:56768: write: broken pipe
Conclusão
Caramba, investigar esse problema foi extremamente divertido! Como notamos nos nossos testes, a configuração de nosso cluster realmente era o problema. Sobrescrever as configurações de timeout com o código abaixo resolveu nosso problema instantaneamente.
Você pode ver o código-fonte na íntegra no GitHub.