o problema
Uma busca no Google em 2010 não é uma requisição, são milhares. O front-end espalha a query por centenas de servidores de índice, e ainda chama anúncios, corretor ortográfico, imagens, vídeo, notícias — o que o paper chama de universal search. Quando a página demora, o engenheiro vê o número agregado e não tem como adivinhar quem atrasou. Três razões, todas estruturais: ele não sabe exatamente quais serviços entraram na requisição, porque isso muda de semana em semana; ele não é especialista nas entranhas de cada um, porque cada um tem outro dono; e os serviços são compartilhados, então a lentidão pode ter vindo do tráfego de outro cliente do mesmo Bigtable.
Havia duas famílias de solução, e as duas cobravam caro. As black-box inferem a causalidade por regressão estatística sobre os logs de mensagem que já existem — são portáteis, mas imprecisas, e precisam de muito volume para acertar. As baseadas em anotação pedem que a aplicação carimbe cada registro com um identificador global — são exatas, e quebram no primeiro time que esquece de instrumentar. Some a isso o requisito de que o monitoramento esteja sempre ligado, porque comportamento estranho em produção quase nunca se reproduz sob demanda.
a ideia
Dapper fica na segunda família, mas move o ponto de instrumentação: em vez de pedir colaboração das aplicações, instrumenta o punhado de bibliotecas que todas elas usam de qualquer jeito. No ambiente do Google isso significa um modelo de threading, uma biblioteca de controle de fluxo e um framework de RPC. Instrumentados esses três, a busca web inteira é rastreável sem uma linha de código novo em nenhum time.
O segundo movimento é aceitar não ver quase nada. Dapper amostra: na primeira versão de produção, uma requisição em 1.024. A aposta é que num serviço de alto volume qualquer padrão que interesse vai reaparecer milhares de vezes — e ela se sustentou.
como funciona
O trace é uma árvore de spans. Cada span guarda nome legível, span id, parent id, trace id (inteiros de 64 bits, únicos com alta probabilidade), início, fim, tempos de RPC e zero ou mais anotações da aplicação. Span sem parent é raiz. O caso comum é um span por RPC, com anotações dos dois lados: cliente e servidor. Como os relógios são de máquinas diferentes, as ferramentas de análise usam a única garantia disponível — o cliente envia antes de o servidor receber — para limitar o skew por cima e por baixo.
A propagação é o truque: o contexto do trace vive em thread-local storage; a biblioteca de controle de fluxo carrega esse contexto para dentro de callbacks agendados em thread pool, de modo que caminhos assíncronos continuam ligados; e o framework de RPC transmite trace id e span id pela rede. O núcleo disso tudo tem menos de 1.000 linhas em C++ e menos de 800 em Java.
A coleta é fora de banda. O span vai para arquivo local, um daemon puxa, e o dado termina numa célula Bigtable regional — um trace por linha, um span por coluna, aproveitando o layout esparso. A latência mediana até o repositório fica abaixo de 15 segundos. Nada disso viaja no header de resposta do RPC, e por duas razões: um trace pode ter milhares de spans enquanto a resposta tem menos de dez kilobytes, e há middleware que responde ao chamador antes de os próprios backends terminarem, o que quebra o aninhamento perfeito que a coleta in-band pressupõe.
Há ainda uma segunda amostragem, no coletor: hash do trace id num escalar entre 0 e 1, guarda se ficar abaixo do coeficiente. Como a decisão depende do trace id, descarta-se o trace inteiro, nunca spans soltos — e o time controla a vazão de escrita no Bigtable mexendo num parâmetro de configuração.
o que isso custou
O custo do tracing completo é medido e é grande: amostrando tudo, a latência média da busca web sobe 16,3%. Só a partir de 1/16 o efeito cai para dentro do erro experimental. Ou seja, a barateza do Dapper é comprada jogando dado fora, e a taxa fixa penaliza exatamente os serviços de baixo tráfego, que precisariam amostrar mais. A amostragem adaptativa, parametrizada por traces por unidade de tempo, ainda estava em implantação quando o paper saiu.
A frescura do dado também é irregular: o percentil 98 da latência de coleta é bimodal — abaixo de dois minutos em cerca de 75% do tempo, e muitas horas nos outros 25%. Para apagar incêndio em serviços de armazenamento compartilhado, que precisam de agregado em dez minutos, os autores admitem que Dapper não serve.
E há o que o modelo simplesmente não enxerga. Escritas coalescidas fazem um trace levar a culpa por trabalho de vários. Cargas batch não têm onde pendurar um trace id. Dapper aponta onde está lento, não por quê: uma requisição que ficou na fila atrás de outra parece lenta sozinha. Informação de kernel não se amarra bem ao contexto de usuário. Payload de RPC não é registrado, por privacidade. O índice custa quase o mesmo que os dados. E a transparência depende da homogeneidade do parque — mesmo assim, 40 aplicações C++ e 33 Java precisaram de propagação manual.
onde isso aparece hoje
O vocabulário venceu. Trace, span, parent id, annotation, sampling rate: é assim que se fala de tracing distribuído hoje, e Zipkin, Jaeger e o modelo de dados do OpenTelemetry são reconhecíveis linha a linha a partir deste paper. A parte menos copiada é a mais interessante do relato: o resultado que os autores não previram foi Dapper virar plataforma. Abrir o repositório por uma API — acesso por trace id, acesso em massa via MapReduce, acesso indexado por serviço e máquina — rendeu inferência automática de dependências entre serviços, um console de tráfego entre clusters construído em menos de duas semanas e o cruzamento com o sistema de exceções pelo id do trace.
O repositório em si é Bigtable, o que dá a medida de como a infraestrutura do Google se alimenta de si mesma. E o achado da seção sobre cauda longa — degradação momentânea de rede no caminho crítico não mexe na vazão, mas domina o outlier de latência — é a semente do problema que The Tail at Scale trata como assunto próprio três anos depois.