o problema
No começo de 2013 o monitoramento do Facebook rodava sobre o ODS, um TSDB construído em cima do HBase. Para escrever, funcionava. Para ler, não: o percentil 90 das consultas tinha subido para vários segundos, o que travava a automação de alarme, e uma análise interativa sobre alguns milhares de séries levava dezenas de segundos. Consultas maiores sobre dados esparsos davam timeout, porque o store estava tunado para priorizar escrita. O efeito mais caro não era a latência em si — era que os engenheiros passaram a se autocensurar, deixando de fazer as perguntas que a ferramenta não aguentava.
Trocar o HBase estava fora de questão: havia cerca de 2 PB ali dentro. Cache também não fechava. O read-through existente servia dashboards que compartilhavam séries, mas errava justamente na consulta ao ponto mais recente, que caía direto no HBase. Um write-through em Memcache exigiria um ciclo de read/write para anexar cada ponto novo a uma série existente. E a escala crescia: na primavera de 2015 eram 2 bilhões de séries únicas e cerca de 12 milhões de pontos por segundo, mais de 1 trilhão por dia. A 16 bytes por ponto, guardar só as últimas 26 horas custaria 16 TB de RAM.
a ideia
Duas observações sustentam o sistema inteiro. Primeira: 85% das consultas ao ODS pediam dados das últimas 26 horas. Segunda: quem olha monitoramento não olha ponto individual, olha agregado e transição de estado — saber que o serviço quebrou agora vale mais que saber que ele quebrou há uma hora.
Disso sai um write-through cache em memória das últimas 26 horas, que aceita perder pequenas quantidades de dado no caminho de escrita desde que continue respondendo. O HBase segue como fonte da verdade e como resposta para consulta histórica. E a viabilidade veio da compressão: 16 bytes por ponto viram 1,37 bytes em média, redução de 12x, o que transforma os 16 TB em algo que cabe em oitenta máquinas por cluster.
como funciona
Um ponto é uma tupla de string key, timestamp de 64 bits e um double. A chave define o shard por hash; escalar é adicionar host e reajustar o hash. Os dados vão para blocos de duas horas.
Timestamps usam delta-of-delta com prefixo de tamanho variável. O cabeçalho do bloco guarda um timestamp alinhado à janela de duas horas e o primeiro delta em 14 bits. Depois, para cada ponto, D = (tn − tn−1) − (tn−1 − tn−2). Se D é zero, um bit 0. Entre [−63, 64], 10 mais 7 bits. Entre [−255, 256], 110 mais 9 bits. Entre [−2047, 2048], 1110 mais 12 bits. Fora disso, 1111 mais D em 32 bits. As faixas foram escolhidas por amostragem da produção — a de 9 bits existe porque muita série chega a cada 4 minutos e um ponto faltando ainda cabe ali.
Valores são XORados com o anterior. Se o XOR é zero, um bit 0. Se não, bit 1 mais um bit de controle: se o bloco de bits significativos cabe dentro da janela do valor anterior, controle 0 e só os bits significativos; senão controle 1, 5 bits para a contagem de leading zeros, 6 bits para o comprimento e os bits em si. 51% dos valores viram um bit; os 30% com controle 10 ficam em 26,6 bits médios, os 19% restantes em 36,9.
Em memória, cada shard tem um TSmap: um vector de shared_ptr para as séries mais um unordered_map case-insensitive por nome. O vector permite varredura paginada, o mapa dá lookup constante, e copiar o vector leva microssegundos — o que evita seção crítica longa contra o fluxo de escrita. Um spin lock de 1 byte por série basta, porque cada série escreve pouco. O bloco aberto é uma string append-only; blocos fechados são imutáveis e copiados para slabs grandes contra fragmentação. A leitura devolve o bloco comprimido cru pela RPC: descompressão é problema do cliente.
Persistência vai para GlusterFS com 3x de replicação: um log append-only por shard, com flush a cada 64 kB, um block file a cada duas horas e um checkpoint que marca o flush bem-sucedido. Não é write-ahead log. Quando um nó cai, o ShardManager, baseado em Paxos, redistribui os shards; o cliente de escrita bufferiza um minuto e descarta o que for mais velho que isso. O novo dono relê cerca de 16 GB por shard e volta funcional em uns 5 minutos.
o que isso custou
O log não é WAL: um crash antes do flush perde alguns segundos de dado. As duas réplicas ficam em regiões diferentes e recebem o stream sem nenhuma tentativa de consistência entre si — divergência é aceita, e a leitura vai na instância mais próxima que estiver viva. Durante recuperação, a resposta volta parcial e marcada como parcial, e cabe a quem chamou decidir se dá para trabalhar assim.
O modelo de dados é pobre de propósito: uma string por série, sem o conjunto de tags do OpenTSDB nem os metadados por evento do InfluxDB. Extrair estrutura da chave é trabalho de camada superior. Só 26 horas cabem ali; o resto é HBase, com roll-up temporal que perde granularidade. E há duas cópias completas em memória, ineficiência que os autores assumem — “high availability trumps resource efficiency”. A compressão também amarra o leitor ao bloco de duas horas: quem quer trinta segundos de dado decodifica duas horas.
A admissão mais útil está na seção de experiência: o protótipo do TSDB comprimido em memória saiu em pouco tempo, e foram vários meses de trabalho para torná-lo tolerante a falhas.
onde isso aparece hoje
O par delta-of-delta para timestamps e XOR para floats virou vocabulário padrão de série temporal. O TSDB do Prometheus documenta explicitamente sua compressão como derivada deste paper, e o Facebook abriu depois uma implementação do motor em open source, o Beringei. A separação entre uma camada quente em memória com janela curta e um store frio em disco também deixou de ser exótica.
Dentro da própria casa, o paper mostra o que a latência baixa destravou: uma busca de correlação por PPMCC sobre até 1 milhão de séries, usada para responder “o que aconteceu perto da hora em que meu serviço quebrou”; visualizações densas de horizon charts; e o roll-up, que saiu de jobs map-reduce sobre o HBase para uma varredura direta dos blocos fechados. O trabalho futuro declarado era uma camada intermediária em flash, com cerca de duas semanas de dado em resolução cheia.