Pensando melhor sobre conexões HTTP

Muitas vezes acabamos configurando as bordas das nossas aplicações, mais especificamente conexões HTTP, com exemplos de documentações dos frameworks que utilizamos, por exemplo o RestTemplatedo Spring, onde vemos somente uma breve referência ao uso de GZIP na documentação oficial:

To use Apache HttpComponents instead of the native java.net functionality, construct the RestTemplate as follows:
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
Apache HttpClient supports gzip encoding. To use it, construct a HttpComponentsClientHttpRequestFactory like so:
HttpClient httpClient = HttpClientBuilder.create().build();
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);

Outro exemplo é o request, muito usado em Nodejs, na documentação temos algumas referências à agents e pool, mas encontrar um exemplo dá um pouquinho mais de trabalho.

Request is designed to be the simplest way possible to make http calls. It supports HTTPS and follows redirects by default.
var request = require('request');
request('http://www.google.com', function (error, response, body) {
console.log('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body); // Print the HTML for the Google homepage.
});

Sei que um desenvolvedor mais experiente e com tempo para pensar provavelmente não utilizará a configuração default. Mas já me deparei algumas vezes com isso em minha carreira.

Vou tentar criar alguns exemplos úteis e incrementais dessas configurações, tanto em Java quanto em Nodejs.

Com o que se preocupar?

Connection pooling

Abrir conexões é muito custoso para qualquer aplicação, como qualquer operação de I/O (o DNS é resolvido, um socket é aberto, acontece o handshake se for uma conexão HTTPS, etc). Assim, manter uma certa quantidade de conexões abertas e reutilizá-las, evitando todo esse processamento, pode melhorar bastante a performance da sua aplicação.

Para isso, no RestTemplate podemos utilizar um HttpClient personalizado:

O HttpClient é um objeto bastante pesado, então o ideal é que tenhamos apenas uma instância dele em nossa aplicação, não se preocupe ele é totalmente Threadsafe.

No exemplo acima estamos mantendo um máximo de 100 conexões abertas por rota (uma rota é basicamente um conjunto de protocolo:host:porta) e um máximo de 1000 conexões abertas no total (por exemplo tendo 12 rotas, mesmo o total por rota sendo de 100, a soma de todas as conexões abertas não ultrapassará 1000, é sempre bom saber quantas rotas seu HttpClient vai acessar para chegar aos melhores valores para cada caso).

Com o request a configuração é bem simples, mas requer uma outra dependência.

Acima temos o agentkeepalive, uma lib feita para gerenciar conexões HTTP, nesse caso configuramos 100 conexões por rota também, o conceito é idêntico ao do HttpClient só que para HTTP devemos usar apenas Agent (uma rota nesse caso é composta por host:porta).

GZIP

Comprimir os dados trafegados também é muito importante, pois diminui o overhead de rede na hora da comunicação com outros sistemas. No caso do RestTemplate o exemplo da documentação já atende essa necessidade. Porém, se você não quer utilizar compressão (não recomendado na maioria dos casos), basta chamar esse método no builder:

No request a compressão não vem habilitada por padrão, então para habilitar basta:

Essa configuração, no entanto, apenas habilita GZIP para receber conteúdo comprimido (por exemplo, um GET), para enviar conteúdo comprimido (por exemplo, um POST) é um pouco mais complicado, pois é necessário adicionar os headers apropriados e fazer a compressão do body na mão:

DNS

Se sua infraestrutura é totalmente estática, ou seja, você não usa DNS para resolução de hosts, seu IP de destino nunca irá mudar. Nesse caso, é melhor que as conexões que você mantém abertas no seu connection pool fiquem lá o máximo de tempo possível. Para isso, no RestTemplate, não precisamos fazer nenhuma configuração adicional e no request podemos substituir o agentkeepalive pelo forever-agent, desta forma:

O forever-agent é uma lib que mantém suas conexões abertas para sempre (pelo menos enquanto elas estiverem em uso).

Agora, se você tem uma infraestrutura dinâmica, estará sujeito a mudanças de IPs de hosts, por exemplo, em um blue/green deployment, onde a infraestrutura é imutável e tende a ser sempre substituída por uma nova, com novos IPs. Neste caso, é necessário ter um tempo máximo de vida para suas conexões, senão sua infraestrutura anterior (blue) nunca vai poder ser desligada e sua infraestrutura atual (green) vai receber requests apenas das novas conexões que forem abertas sob demanda. Para configurar esse tempo máximo de vida no RestTemplate é só fazer o seguinte:

Desta forma, garantimos que nenhuma conexão ficará aberta por mais de 30 minutos, então em uma situação de blue/green temos um tempo bem definido para saber que determinada infraestrutura não está mais sendo usada.

No request, a configuração é bem similar, utilizando o agentkeepalive (essa feature fomos nós do Viva que implementamos na lib quando identificamos essa necessidade, o PR foi esse https://github.com/node-modules/agentkeepalive/pull/50):

O único detalhe aqui é que o TTL é em milissegundos.

Retry

Em certos casos, retentar um request pode poupar o usuário de tomar um erro na tela, pode poupar sua aplicação de falhar, pode ser… melhor. Por exemplo, sua aplicação tem alguma dependência instável (um cluster que constantemente tem máquinas respondendo normalmente e outras falhando, máquinas sendo reiniciadas, falhas de rede, etc), nesse caso fazer uma retentativa pode garantir que sua aplicação consiga se comunicar ao invés de falhar (ao custo de demorar mais para responder).

Aqui temos um tradeoff a ser feito: O que é melhor para sua aplicação? Falhar rapidamente ou responder com sucesso porém mais lentamente? Quantas vezes uma retentativa vai ser executada? Devemos diminuir o timeout e confiar que uma retentativa será respondida mais rápido do que a primeira tentativa? Devemos aumentar o timeout e não fazer nenhuma retentativa?

São vários fatores e responder essa pergunta só é possível com contexto, sabendo disso, vou dar exemplos de como configurar retentativas, primeiro com o RestTemplate:

O código acima configura uma quantidade de 3 retentativas, também poderíamos implementar alguma lógica relacionada com a IOException que ocorreu ou com o contexto do request.

Para retentativas baseadas no response do request:

Aqui utilizamos uma classe útil do HttpClient, a DefaultServiceUnavailableRetryStrategy que recebe como argumentos: A quantidade de retentativas (no caso 3) e o intervalo entre as retentativas (no caso 1 milissegundo). Internamente ela verifica se recebeu um status code 503 e decide até quando retentar com base nos argumentos do construtor, se você precisar de alguma lógica diferente desta, basta implementar a interface ServiceUnavailableRetryStrategy e adicionar no builder.

O request não tem nada relacionado à retentativa builtin, então podemos utilizar a lib async para obter um comportamento parecido com o que configuramos acima:

Timeouts

Um timeout mal configurado ou esquecido pode derrubar sua aplicação em um momento de instabilidade de alguma de suas dependências, por isso é importante entender como configurar e o que significam os timeouts de um request. No HttpClient que estamos usando com o RestTemplate temos:

  • connectionRequestTimeout: O tempo limite para aguardar por uma conexão do pool de conexões.
  • connectTimeout: O tempo limite para estabelecer uma conexão com o host desejado.
  • socketTimeout: O tempo máximo de espera para finalizar a comunicação com o host de destino.

Todos esses timeouts são definidos em milissegundos. Para configurar, é necessário criar um RequestConfig com eles e setar no seu HttpClient:

No request essa configuração é mais simples, basta setar o valor do timeout e ele será usado tanto para connect timeout quanto para socket timeout:

Não temos como configurar connectionRequestTimeout no request. O comportamento da lib ao chegar ao limite de sockets é enfileirar o request atual, que será executado assim que um socket estiver livre para isso.

Preemptive authentication

No contexto de integração entre sistemas é muito útil já enviar a autenticação para um request que o cliente sabe que é protegido. Isso economiza o tratamento do challenge-response, que acaba fazendo com que o mesmo request seja feito duas vezes, uma sem a autenticação que resulta na challenge-response e outro com autenticação para conseguir a real resposta do request.

A documentação do HttpClient sugere popular o cache de autenticação antes para enviar a autenticação preemptiva, mas com o RestTemplate não utilizamos diretamente o HttpClient, então podemos apelar para o protocolo HTTP e fazer o seguinte:

Aqui utilizamos um interceptor que tem o papel de interceptar todos os requests que passam pelo RestTemplate e realizar alguma lógica comum, antes ou depois de sua execução. Nesse caso o BasicAuthorizationInterceptor encoda o usuário e senha recebidos e adiciona o header de basic authentication com o valor correto antes da execução do request.

A configuração do request em Nodejs é mais simples, podemos definir uma autenticação da seguinte forma:

A autenticação preemptiva aqui é configurada pelo atributo sendImmediately, que por padrão é true e poderia ser omitido.

User-Agent

Para ter uma maneira fácil de saber quem está realizando um request é aconselhável configurarmos sempre um User-Agent para a nossa aplicação, quando temos necessidade de rastrear e/ou simular algum problema em produção essa informação é muito útil.

No RestTemplate basta setar o User-Agent no HttpClient utilizado:

No request devemos simplesmente enviar o header HTTP com o User-Agent:

Logging

É sempre interessante ter rastreabilidade dos requests que nossa aplicação faz, mas ao invés de logar no meio do nosso código, copiando e colando um formato de método em método, devemos centralizar essa lógica de forma que ela seja reutilizada automaticamente e que novos requests já tenham logs desde a sua criação, no RestTemplate podemos implementar um interceptor para isso:

Nosso interceptor aqui loga alguns dados do request e do response, poderíamos também separá-lo em uma outra classe para torná-lo mais testável e organizar melhor o nosso código, além de usar Perf4J para calcular o tempo.

O request possui um módulo próprio para debug, para ativá-lo basta fazer o seguinte:

Com isso teremos um output em JSON com bastante informações sobre o request feito e o response recebido.

Conclusão

Temos muitos detalhes de configuração para fazer requests HTTP de forma otimizada, controlada, reproduzível e rastreável, espero que as informações que compilei acima sejam úteis para você, independente da linguagem em que você codifica atualmente!

Mais Informações

  1. http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#rest-client-access
  2. https://github.com/request/request
  3. https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/connmgmt.html#d5e374
  4. https://github.com/node-modules/agentkeepalive
  5. https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/authentication.html
  6. https://nodejs.org/api/zlib.html
  7. https://en.wikipedia.org/wiki/Domain_Name_System
  8. https://github.com/request/forever-agent
  9. https://martinfowler.com/bliki/BlueGreenDeployment.html
  10. https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/fundamentals.html#d5e316
  11. http://caolan.github.io/async/docs.html#retry
  12. https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/config/RequestConfig.html
  13. https://en.wikipedia.org/wiki/Challenge-response_authentication
  14. https://github.com/request/request-debug