Papers
The Tail at Scale
Jeffrey Dean, Luiz André Barroso · Communications of the ACM, Vol. 56, 2013
O problema: por que a cauda de latência importa quando o sistema é grande
O ponto de partida do artigo é que usuários percebem um serviço como fluido quando ele responde em torno de 100 milissegundos, e que a computação em escala de data center tornou possível oferecer essa experiência mesmo consultando datasets de múltiplos terabytes espalhados por milhares de máquinas. O problema é que, quando uma única requisição do usuário precisa consultar dezenas ou centenas de servidores em paralelo para montar a resposta, a latência final da requisição é determinada pelo componente mais lento entre todos eles, não pela latência média. Os autores mostram isso com um exemplo numérico: se cada servidor individual tem um 99º percentil de latência de 1 segundo (mas normalmente responde em 10 milissegundos), e uma requisição precisa esperar por 100 desses servidores em paralelo, cerca de 63% das requisições do usuário acabarão levando mais de 1 segundo — porque basta um único servidor lento entre os cem para atrasar a resposta inteira. Em um sistema real do Google, uma latência de 10ms no percentil 99 por requisição-folha se converte em cerca de 140ms no percentil 99 agregado, e os 5% de requisições mais lentas respondem por metade de toda a latência observada no percentil 99. Ou seja, em sistemas com alto fan-out, a cauda de latência deixa de ser um evento raro e passa a dominar a experiência de boa parte dos usuários.
De onde vem a variabilidade de latência
O artigo argumenta que a variabilidade de latência não é um defeito raro de hardware ruim, mas uma característica praticamente inevitável de qualquer máquina compartilhada em produção. Entre as fontes de variabilidade discutidas estão: contenção por recursos compartilhados na mesma máquina (CPU, cache, largura de banda de rede e disco disputados entre múltiplos processos ou serviços rodando lado a lado); atividades de manutenção em segundo plano e daemons do próprio sistema, que ocasionalmente roubam ciclos de processamento; pausas de coleta de lixo em runtimes gerenciados; mecanismos de gerenciamento de energia que reduzem a frequência do processador para economizar energia e depois precisam de tempo para voltar ao desempenho máximo; e efeitos de enfileiramento, em que uma rajada de requisições concorrentes se acumula em filas internas do sistema operacional, do middleware de rede ou da própria aplicação, atrasando algumas requisições mesmo que a capacidade média do sistema seja suficiente. A conclusão prática é que, mesmo com hardware bem dimensionado e sem nenhuma falha real acontecendo, uma fração pequena mas constante das requisições individuais vai ser lenta simplesmente por essas interações normais do sistema — e é justamente essa fração que se amplifica quando muitos componentes precisam responder juntos.
As técnicas propostas para tolerar a cauda
Em vez de tentar eliminar toda variabilidade individual — o que os autores consideram impraticável —, o artigo propõe técnicas para tornar o sistema como um todo previsível mesmo com partes individuais imprevisíveis, em paralelo ao raciocínio clássico de tolerância a falhas via redundância. Duas técnicas atuam dentro de uma única requisição: em 'hedged requests', o cliente envia a mesma requisição a uma réplica secundária se a primeira réplica ainda não respondeu depois de um tempo próximo ao percentil 95 esperado, e usa a resposta que chegar primeiro, cancelando a outra — isso limita a carga extra a poucos por cento enquanto reduz bastante a cauda. Em 'tied requests', variação mais agressiva, o cliente enfileira a requisição simultaneamente em múltiplos servidores desde o início, mas avisa cada um sobre a existência do outro, de modo que quando um servidor começa a processar (ou termina), ele manda cancelar a cópia nas filas dos demais; em um sistema real do Google essa técnica reduziu a latência mediana em cerca de 16% e a latência no percentil 99,9 em cerca de 40%. Outras duas técnicas operam entre requisições ao longo do tempo: micro-particionamento, que mantém muito mais partições de dados do que máquinas físicas para permitir redistribuir carga em incrementos pequenos e rapidamente; e canary requests, que enviam um número pequeno de requisições de teste contra código ou configuração nova antes de um rollout amplo, para evitar que um bug provoque falhas correlacionadas em muitos servidores ao mesmo tempo.
Legado: tolerância à cauda em sistemas atuais
A contribuição central do artigo — tratar latência de cauda como um problema de engenharia de sistemas com técnicas próprias, em vez de um detalhe estatístico a ser tolerado passivamente — se tornou um princípio de projeto amplamente adotado em infraestrutura distribuída depois de 2013. A ideia de enviar uma requisição redundante a uma réplica alternativa quando a primeira demora mais do que o esperado apareceu, em variações próprias, em frameworks de RPC usados amplamente na indústria, que hoje oferecem políticas de hedging configuráveis como parte da própria biblioteca de comunicação entre serviços, sem que o desenvolvedor da aplicação precise reimplementar a lógica manualmente. O raciocínio de que sistemas com alto fan-out amplificam a cauda de latência de componentes individuais também influenciou como equipes de operação avaliam SLOs internos: em vez de olhar só a latência média de um serviço isolado, times passaram a monitorar percentis altos (p99, p999) justamente nos pontos onde muitas chamadas paralelas convergem. A noção de canary requests, por sua vez, é hoje prática padrão de implantação gradual (canary deployment) antes de expor uma mudança de código a todo o tráfego de produção. Em conjunto, essas ideias ajudaram a consolidar a visão de que, em sistemas de larga escala, controlar a cauda da distribuição de latência é tão importante quanto controlar a média — e que isso pode ser feito com técnicas de engenharia específicas, não apenas com mais hardware.
Veja as ideias em ação
Pratique na escola de System Design os desenhos que usam este paper.