Cómo construimos la canalización de menos de 30ms de tracio.ai
De la recolección de señales al ID de visitante en menos de 30ms: nuestra arquitectura con Go, ClickHouse, Redis y procesamiento distribuido.
Cuando nos propusimos construir el motor de identificación de dispositivos de tracio.ai, teníamos un requisito innegociable: toda la canalización —desde la recepción de señales cifradas hasta la devolución de un ID de visitante— debía completarse en menos de 30 milisegundos en el percentil 95. Este artículo es un recorrido detallado por la arquitectura que construimos para alcanzar ese objetivo.
Visión general de la canalización
La canalización de identificación tiene cinco etapas: descifrado de señales, normalización de señales, cálculo de hash, resolución de identidad y serialización de la respuesta. Cada etapa se optimiza de forma independiente, y las etapas que pueden ejecutarse en paralelo lo hacen. El presupuesto total es de 30ms, asignado aproximadamente así: descifrado 2ms, normalización 3ms, hashing 2ms, resolución de identidad 20ms, serialización 1ms. Los 2ms restantes son margen.
El descifrado de señales revierte el transporte cifrado del lado del cliente. Usamos los paquetes de criptografía de Go con aceleración por hardware, que completan el descifrado de un payload típico de 4KB en menos de 1ms. La normalización analiza el JSON de señales, valida los tipos y aplica transformaciones específicas de cada plataforma; por ejemplo, normaliza las cadenas de user agent para eliminar el ruido específico de cada versión.
Resolución de identidad distribuida
La resolución de identidad —determinar si este dispositivo se ha visto antes— es la etapa más sensible a la latencia. Almacenamos los perfiles de dispositivo en Redis, fragmentados en un clúster mediante una capa de enrutamiento de claves distribuido. El enrutamiento distribuye las claves según la huella del nivel de hardware, lo que garantiza que las búsquedas del mismo dispositivo siempre alcancen el mismo nodo de Redis.
Nuestra implementación de sharding usa nodos virtuales (150 por nodo físico) para asegurar una distribución uniforme. Cuando se añade o elimina un nodo, solo es necesario remapear 1/N de las claves, donde N es el número de nodos. Implementamos la capa de enrutamiento en Go con un tiempo de búsqueda O(log n) y cero asignaciones.
Redis como almacén de identidad
Elegimos Redis frente a las alternativas (Memcached, ScyllaDB, DynamoDB) por sus tiempos de respuesta consistentes de menos de un milisegundo y su compatibilidad con estructuras de datos complejas. Cada perfil de dispositivo se almacena como un hash de Redis con campos para el hash de cada nivel de señal, el ID de visitante, la marca de tiempo de la última vez visto y los metadatos de confianza.
La consulta de resolución de identidad es una única llamada HGETALL seguida de una comparación de los hashes de señales entrantes con los hashes almacenados. Si el nivel de hardware coincide, devolvemos el ID de visitante existente con alta confianza. Si solo coincide el nivel de software, realizamos una comparación de similitud de los datos a nivel de señal para determinar si se trata del mismo dispositivo con un navegador actualizado. Si no coincide nada, generamos un nuevo ID de visitante.
ClickHouse para el almacenamiento de eventos
Cada evento de identificación se escribe en ClickHouse de forma asíncrona. Usamos un escritor con búfer que agrupa las inserciones en lotes: recopila eventos durante 100ms o hasta que se acumulan 1,000 eventos, lo que ocurra primero. Este agrupamiento es crucial porque ClickHouse rinde mejor con inserciones grandes (miles de filas a la vez) que con inserciones de filas individuales.
Nuestro esquema de ClickHouse está optimizado para los dos patrones de consulta más habituales: buscar todos los eventos de un ID de visitante concreto y agregar eventos a lo largo de periodos de tiempo. Usamos un motor MergeTree con una clave primaria de (visitor_id, timestamp), que proporciona búsquedas puntuales rápidas y escaneos de rango eficientes. Las vistas materializadas mantienen métricas diarias y por hora preagregadas.
Lograr menos de 30ms a escala
Tres decisiones de arquitectura fueron cruciales para alcanzar nuestro objetivo de latencia. Primero, la canalización es totalmente en streaming: empezamos a procesar las señales antes de haber recibido todo el cuerpo de la petición HTTP. Segundo, las búsquedas en Redis usan pooling de conexiones con conexiones persistentes, lo que elimina la sobrecarga del handshake TCP. Tercero, las escrituras en ClickHouse son totalmente asíncronas y nunca bloquean la ruta de respuesta.
En pruebas de carga con 50K peticiones/segundo, nuestra latencia p50 es de 12ms, la p95 de 24ms y la p99 de 38ms. La p99 supera ocasionalmente nuestro objetivo de 30ms durante el reequilibrado del clúster de Redis, pero la p95 se mantiene siempre por debajo de 30ms. Para los clientes con requisitos de latencia más estrictos, ofrecemos clústeres de Redis dedicados que eliminan la contención multiinquilino.