Como construímos o pipeline sub-30ms do tracio.ai
Da coleta de sinais ao ID de visitante em menos de 30ms: nossa arquitetura usando Go, ClickHouse, Redis e processamento distribuído.
Quando decidimos construir o motor de identificação de dispositivos do tracio.ai, tínhamos um requisito inegociável: todo o pipeline — do recebimento dos sinais criptografados ao retorno de um ID de visitante — precisava ser concluído em menos de 30 milissegundos no 95º percentil. Este artigo é um passo a passo detalhado da arquitetura que construímos para atingir essa meta.
Visão geral do pipeline
O pipeline de identificação tem cinco estágios: descriptografia dos sinais, normalização dos sinais, cálculo de hash, resolução de identidade e serialização da resposta. Cada estágio é otimizado de forma independente, e os estágios que podem rodar em paralelo o fazem. O orçamento total é de 30ms, alocado aproximadamente como: descriptografia 2ms, normalização 3ms, hashing 2ms, resolução de identidade 20ms, serialização 1ms. Os 2ms restantes são margem de segurança.
A descriptografia dos sinais reverte o transporte criptografado do lado do cliente. Usamos os pacotes de criptografia do Go com aceleração por hardware, que concluem a descriptografia de um payload típico de 4KB em menos de 1ms. A normalização faz o parsing do JSON de sinais, valida os tipos e aplica transformações específicas de plataforma — por exemplo, normalizando strings de user agent para remover ruído específico de versão.
Resolução de identidade distribuída
A resolução de identidade — determinar se este dispositivo já foi visto antes — é o estágio mais sensível à latência. Armazenamos perfis de dispositivos no Redis, particionados em um cluster usando uma camada distribuída de roteamento de chaves. O roteamento distribui as chaves com base na impressão digital de nível de hardware, o que garante que buscas para o mesmo dispositivo sempre atinjam o mesmo nó Redis.
Nossa implementação de sharding usa nós virtuais (150 por nó físico) para garantir distribuição uniforme. Quando um nó é adicionado ou removido, apenas 1/N das chaves precisam ser remapeadas, onde N é o número de nós. Implementamos a camada de roteamento em Go com tempo de busca O(log n) e zero alocações.
Redis como armazenamento de identidade
Escolhemos o Redis em vez das alternativas (Memcached, ScyllaDB, DynamoDB) por causa de seus tempos de resposta consistentemente abaixo de um milissegundo e do suporte a estruturas de dados complexas. Cada perfil de dispositivo é armazenado como um hash Redis com campos para o hash de cada nível de sinal, o ID de visitante, o timestamp da última visualização e metadados de confiança.
A consulta de resolução de identidade é uma única chamada HGETALL seguida de uma comparação dos hashes dos sinais recebidos com os hashes armazenados. Se o nível de hardware corresponder, retornamos o ID de visitante existente com alta confiança. Se apenas o nível de software corresponder, realizamos uma comparação de similaridade dos dados em nível de sinal para determinar se é o mesmo dispositivo com um navegador atualizado. Se nada corresponder, geramos um novo ID de visitante.
ClickHouse para armazenamento de eventos
Todo evento de identificação é gravado no ClickHouse de forma assíncrona. Usamos um gravador com buffer que agrupa inserções em lotes — coletando eventos por 100ms ou até acumular 1.000 eventos, o que ocorrer primeiro. Esse batching é crítico porque o ClickHouse tem melhor desempenho com inserções grandes (milhares de linhas por vez) em vez de inserções de linhas individuais.
Nosso esquema no ClickHouse é otimizado para os dois padrões de consulta mais comuns: buscar todos os eventos de um ID de visitante específico e agregar eventos ao longo de períodos de tempo. Usamos um motor MergeTree com chave primária de (visitor_id, timestamp), o que proporciona buscas pontuais rápidas e varreduras de intervalo eficientes. Views materializadas mantêm métricas diárias e horárias pré-agregadas.
Alcançando sub-30ms em escala
Três decisões arquiteturais foram críticas para atingir nossa meta de latência. Primeiro, o pipeline é totalmente em streaming — começamos a processar os sinais antes de todo o corpo da requisição HTTP ter sido recebido. Segundo, as buscas no Redis usam pooling de conexões com conexões persistentes, eliminando o overhead do handshake TCP. Terceiro, as gravações no ClickHouse são totalmente assíncronas e nunca bloqueiam o caminho da resposta.
Em testes de carga com 50 mil requisições/segundo, nossa latência p50 é de 12ms, a p95 é de 24ms e a p99 é de 38ms. A p99 ocasionalmente ultrapassa nossa meta de 30ms durante o rebalanceamento do cluster Redis, mas a p95 permanece consistentemente abaixo de 30ms. Para clientes com requisitos de latência mais rígidos, oferecemos clusters Redis dedicados que eliminam a contenção multi-tenant.