Front End

18 abr, 2013

Otimizando HTTP: keep-alive e pipelining

Publicidade

A última atualização importante para a especificação HTTP foi em 1999, tempo esse em que o RFC 2616 padronizou o HTTP 1.1 e introduziu tanto o suporte necessário para keep-alive e pipelining. Enquanto o HTTP 1.0 precisava rigorosamente do modelo “único pedido por conexão”, HTTP 1.1 reverteu esse comportamento: por padrão, um cliente e servidor HTTP 1.1 mantém a conexão aberta, a menos que o cliente indique o contrário (via cabeçalho Connection: close).

Por que se preocupar? Configurar uma conexão TCP é muito dispendioso! Mesmo em um caso otimizado, uma rota completa de um único caminho entre o cliente e o servidor pode demorar de 10 a 50ms. Agora multiplique isso três vezes para completar o handshake TCP, e já estamos vendo um teto de 150ms! O keep-alive nos permite reutilizar a mesma conexão entre pedidos diferentes e amortizar esse custo.

O único problema é, muitas vezes, como desenvolvedores, temos a tendência de esquecer isso. Dê uma olhada em seu próprio código, quantas vezes você reutilizou uma conexão HTTP? O mesmo problema é encontrado na maioria dos wrappers de API, e mesmo nas bibliotecas HTTP padrão da maioria das linguagens, que desabilitam o keep-alive por padrão.

HTTP pipelining

A boa notícia é que o keep-alive é suportado por todos os navegadores modernos e na maior parte trabalha de forma não convencional. Infelizmente, o suporte para pipelining não está tão bem na fita assim: nenhum navegador o suporta oficialmente, e poucos desenvolvedores  pensam sobre isso. O que é lamentável, porque pode trazer benefícios significativos de desempenho!

xkeepalive-pipelining

Enquanto o keep-alive nos ajuda a amortizar o custo de criação de uma conexão TCP, o pipelining nos permite quebrar o rigoroso modelo “enviar um pedido, aguarde resposta”. Em vez disso, podemos enviar várias solicitações, em paralelo, pela mesma conexão, sem esperar por uma resposta em forma de série. Isso pode parecer uma otimização menor no início, mas vamos considerar o seguinte cenário: pedido 1 e pedido 2 são feitos em pipeline, o pedido 1 leva 1.5s para renderizar no servidor, enquanto o pedido 2 leva 1s. Qual é o tempo total de execução?

É claro que a resposta depende da quantidade de dados enviados de volta, mas o limite inferior é, na verdade, 1.5s! Uma vez que os pedidos são feitos em pipeline, tanto o pedido 1 quanto o a pedido 2 podem ser processados pelo servidor em paralelo. Por isso, o pedido 2 é concluído antes do pedido 1, mas é enviado imediatamente após o pedido 1 estar completo. Menos conexões, tempos de resposta mais rápidos – faz você se perguntar por que ninguém anuncia que sua API suporta HTTP pipelining?

HTTP keep-alive & pipelining em Ruby

Infelizmente, muitas bibliotecas HTTP padrão revertem para HTTP 1.0: uma conexão, uma solicitação. A própria rede/http do Ruby usa um comportamento pouco conhecido, em que por padrão um cabeçalho “Connection: close” é acrescentado a cada pedido, exceto quando você está usando a forma de bloco:

require 'net/http'

start = Time.now
Net::HTTP.start('127.0.0.1', 9000) do |http|
  r1 = http.get "/?delay=1.5"
  r2 = http.get "/?delay=1.0"

  p Time.now - start # => 2.5 - doh! keepalive, but no pipelining
end

Com o exemplo acima, a gente recebe os benefícios do keep-alive, mas infelizmente net/http não oferece suporte para pipelining. Para permitir isso, você vai ter que usar um net-http-pipeline, que é uma biblioteca independente:

require 'net/http/pipeline'

start = Time.now
Net::HTTP.start 'localhost', 9000 do |http|
  http.pipelining = true

  reqs = []
  reqs << Net::HTTP::Get.new('/?delay=1.5')
  reqs << Net::HTTP::Get.new('/?delay=1.0')

  http.pipeline reqs do |res|
    puts res.code
    puts res.body[0..60].inspect
  end

  p Time.now - start # => 1.5 - keep-alive + pipelining!
end

EM-HTTP & Goliath: keep-alive + pipelining

Enquanto o pipelining é desativado na maioria dos navegadores, devido a muitas questões relacionadas com proxies e caches, ele não deixa de ser uma otimização útil, ou para falar com o seu parceiro de API. A boa notícia é que Apache, Nginx, HAProxy e outros o suportam, mas o problema é que a maioria dos servidores de aplicação, mesmo os que afirmam ser “HTTP 1.1”, geralmente não o suportam.

O suporte verdadeiro para keep-alive e pipelining é uma das razões pelas quais construímos em-http-request e Goliath para nossa pilha no PostRank. Um exemplo simples em ação:

require 'goliath'

class Echo < Goliath::API
  use Goliath::Rack::Params
  use Goliath::Rack::Validation::RequiredParam, {:key => 'delay'}

  def response(env)
    EM::Synchrony.sleep params['delay']
    [200, {}, params['delay']]
  end
end

 

require 'em-http-request'

EM.run do
  conn = EM::HttpRequest.new('http://localhost:9000/')
  start = Time.now

  r1 = conn.get :query => {delay: 1.5}, :keepalive => true
  r2 = conn.get :query => {delay: 1.0}

  r2.callback do
    p Time.now - start # =>  1.5 - keep-alive + pipelining
    EM.stop
  end
end

O tempo de execução total é 1.5s. Se a sua API pública ou privada for construída em cima de HTTP, então keep-alive e pipelining são características que você deve aproveitar sempre que puder.

Otimizando HTTP: interrogue o seu código!

Veja slides aqui.

Enquanto nós amamos gastar tempo otimizando nossos algoritmos, ou tornando os bancos de dados mais rápidos, muitas vezes esquecemos o básico: a criação de conexões TCP é dispendiosa, e o pipelining pode levar a grandes vitórias. Você usa reutiliza as conexões HTTP em seu código? Será que o servidor do seu aplicação suporta pipelining? As respostas são geralmente “não, e eu não tenho certeza”, o que é algo que temos que mudar!

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.igvita.com/2011/10/04/optimizing-http-keep-alive-and-pipelining/