Resumen del proyecto

Buena parte del tráfico que llega a un sitio web comercial no es humano. Una parte es benigna: rastreadores de buscadores, monitores de disponibilidad, generadores de vistas previas. El resto no lo es: ataques de relleno de credenciales, scrapers, bots de compra y clics automatizados que consumen presupuestos publicitarios en silencio y envenenan cada número analítico sobre el que un negocio toma decisiones. El problema es que todo eso entra por la misma puerta que los clientes reales, y para cuando aparece en un informe semanal el dinero ya está gastado.

Nuestro cliente se propuso cerrar esa brecha con una plataforma que responde a una pregunta —¿este visitante es una persona o una máquina?— mientras la visita todavía está ocurriendo. No un proceso por lotes nocturno, no un informe: un veredicto devuelto en el mismo momento en que carga la página, para que los sistemas del propio cliente puedan actuar de inmediato.

Pasamos ocho meses construyendo ese producto de principio a fin, desde el recolector de señales del lado del navegador hasta los servicios de scoring y la plataforma de datos que hay debajo. El encargo transcurrió a lo largo de 2022 y se cerró en 2023.

Bajo NDA

Este proyecto está cubierto por un acuerdo de confidencialidad. Se omiten el cliente, el nombre del producto y cualquier detalle identificativo, y el trabajo descrito a continuación se cuenta a nivel de arquitectura y no de detalles de implementación.

El reto

Tres restricciones definieron el trabajo de ingeniería, y cada una tiraba en contra de las otras.

Tenía que ser en tiempo real. Un veredicto que llega después de que el visitante ya haya convertido —o ya haya vaciado un presupuesto— es un registro de auditoría, no una defensa. La puntuación tenía que completarse dentro de una petición, con un presupuesto de latencia lo bastante pequeño como para que un cliente lo pusiera de buen grado en la ruta crítica de la carga de su propia página.

Tenía que ser de carga alta. La plataforma funcionaba a volumen real de alta carga, procesando decenas de miles de peticiones por minuto en toda la base de clientes, con un tráfico que se disparaba sin aviso: el lanzamiento de una campaña por parte del cliente y un ataque desde el otro lado son idénticos para quien planifica capacidad. El pico nunca era la media, y el sistema tenía que dimensionarse para el pico sin dejar de ser asequible en la media.

Tenía que ser probatorio. Todo veredicto tenía que ser explicable y reexaminable a posteriori. Los clientes no actúan sobre una puntuación sin explicación, y ni la lógica de detección ni los modelos detrás de ella pueden mejorar sin seguir teniendo a mano las señales en bruto que produjeron las decisiones de ayer. Eso significaba guardarlo todo: un problema de retención y analítica situado justo detrás de un problema de latencia.

Tiempo real, gran volumen y retención total son tres requisitos que ninguna base de datos satisface a la vez. Casi toda la arquitectura que sigue se deriva de negarse a ceder en ninguno de ellos.

Recolección de señales en JavaScript puro

El lado cliente de la plataforma es un recolector que se ejecuta dentro de la página del cliente y reúne las pruebas a partir de las que se emite un veredicto: características del navegador y del entorno, comportamiento de renderizado y de tiempos, patrones de interacción y las muchas pequeñas inconsistencias que separan a un navegador real conducido por una persona de uno automatizado que viste el user agent de un navegador real.

Está escrito en JavaScript plano: sin framework, sin runtime de compilación, sin dependencias. Fue una restricción deliberada y no una preferencia:

  • Se ejecuta en la página de otra persona. El recolector va incrustado en miles de sitios que no controlamos, junto a todo lo demás que esos sitios cargan. No puede dar por hecho un sistema de módulos, un conjunto de polyfills ni una línea base concreta de navegador, y no debe chocar con nada que ya esté en la página.
  • Tiene que ser pequeño y rápido. Todo lo que se envía a cada visitante de cada cliente se mide contra el presupuesto de rendimiento del propio cliente. El runtime de un framework habría sido más grande que el recolector entero.
  • Opera en un entorno hostil. Los sujetos de la medición intentan activamente derrotarla. La recolección de señales tiene que ser defensiva ante una página que puede estar instrumentada, parcheada o emulada a su alrededor, y eso aboga por código sin capas entre él y las APIs de navegador que lee.

El trabajo del recolector termina en reunir y enviar señales. Todo el juicio ocurre en el servidor, donde la lógica no es visible para aquello que está siendo juzgado.

Go en la ruta caliente

Todos los servicios de la ruta de la petición están escritos en Go. La carga de trabajo se acerca mucho a la forma ideal del lenguaje: cantidades enormes de peticiones pequeñas, independientes y en su mayoría limitadas por E/S que hay que atender de forma concurrente con una latencia predecible.

Dos propiedades importaron más en la práctica. Las goroutines hicieron la concurrencia por petición lo bastante barata como para que la ingesta, el enriquecimiento y el scoring pudieran ramificarse internamente sin un pool de hilos que ajustar. Y el runtime de Go nos dio el comportamiento de latencia de cola sobre el que se vendía el producto: el número que importa en un sistema en tiempo real no es la respuesta media, sino el pequeño porcentaje más lento, y ahí es exactamente donde un runtime más pesado se gasta su presupuesto.

La ruta en sí es deliberadamente corta: ingerir y validar, resolver contexto, evaluar las reglas de detección y los modelos contra las señales entrantes, devolver el veredicto. Todo lo que no tiene que ocurrir antes de la respuesta —persistencia, agregación, enriquecimiento, realimentación de los modelos— se saca de la ruta caliente y se pasa al pipeline asíncrono que hay detrás.

De una única base de datos a una plataforma de datos

La plataforma se lanzó sobre PostgreSQL, y esa fue la decisión correcta al principio. Una base de datos contenía los eventos, las cuentas, la configuración y los agregados; un único lenguaje de consulta respondía a todas las preguntas; se podía razonar sobre todo el producto en un solo sitio mientras aún estaba encontrando su forma.

No sobrevivió al contacto con el volumen de producción, y el motivo merece enunciarse con precisión: el fallo no fue «PostgreSQL es demasiado lento», fue que se le estaba pidiendo a un único almacén ser cuatro cosas distintas a la vez. Un sumidero de eventos de solo escritura y alta cadencia, un almacén de consulta de baja latencia en la ruta de la petición, un almacén analítico para agregados sobre miles de millones de filas y un índice de búsqueda para investigar sesiones individuales son cuatro cargas de trabajo con cuatro conjuntos de requisitos contradictorios. Ajustar para cualquiera de ellas empeoraba las demás.

Así que las separamos y trasladamos la plataforma a Google Cloud:

Redis: el estado de la ruta caliente. Redis guarda todo lo que una decisión de scoring necesita consultar en milisegundos de un solo dígito: estado reciente por visitante y por sesión, contadores de frecuencia y tasa, configuración resuelta y consultas de reputación. Es lo que hace posible siquiera un veredicto dentro de la petición, y absorbe el volumen de lectura que si no habría caído sobre la base de datos principal.

: la costura entre lo síncrono y lo asíncrono. Cada petición puntuada se publica como evento y la respuesta vuelve de inmediato; todo lo que hay aguas abajo consume desde ahí. Este es el desacoplamiento que permite que la ruta de la petición siga siendo corta y que el resto de la plataforma se escale, se redespliegue o se ralentice temporalmente sin que el endpoint de scoring se entere. También significa que un pico de tráfico aterriza en una cola y no en una base de datos.

Almacenamiento de objetos: eventos en bruto, guardados indefinidamente. Cada carga de señales se archiva en compatible con S3 en su forma original. Es el sitio duradero más barato donde poner datos cuyo uso futuro aún se desconoce, y es la razón por la que la lógica de detección puede reejecutarse de forma retrospectiva sobre tráfico histórico real en vez de sobre un resumen de él.

: la capa analítica. Las agregaciones sobre todo el historial de eventos, los informes de calidad del tráfico, el análisis de cohortes y campañas y el trabajo exploratorio detrás de nuevas heurísticas de detección se ejecutan aquí, sobre un almacén construido para escaneos de ese tamaño y completamente fuera de la ruta operativa.

Elasticsearch: investigación. Cuando un cliente discute un veredicto o un analista persigue un patrón nuevo, la pregunta es «enséñame estas sesiones concretas, filtradas de seis maneras»: un problema de búsqueda interactiva y no de agregación. sirve para eso, de modo que las consultas exploratorias nunca tocan los sistemas que sirven tráfico.

PostgreSQL se quedó, con un cometido mucho más estrecho: cuentas, configuración de clientes y los datos relacionales que de verdad quieren transacciones y restricciones. Quitarle las cargas para las que nunca fue adecuado es lo que volvió a hacerlo una buena elección.

Kubernetes en Google Cloud

La plataforma corre como servicios contenerizados sobre un clúster de Kubernetes en Google Cloud. Dado el perfil de tráfico, la alternativa nunca estuvo seriamente sobre la mesa: una carga que oscila un orden de magnitud en cuestión de minutos necesita infraestructura que añada capacidad por sí sola, y servicios individualmente baratos de replicar.

La división en servicios pequeños de Go se rentabiliza aquí. La ingesta, el scoring y los consumidores asíncronos tienen curvas de escalado muy distintas, y como despliegues separados cada uno escala con su propia señal: la capa de ingesta sigue la tasa de peticiones, los consumidores siguen la profundidad de la cola. Los despliegues progresivos permitían actualizar el endpoint de scoring en horario laboral sin ventana de mantenimiento, lo que para un servicio situado en la ruta de carga de página de un cliente era un requisito duro y no una comodidad.

Resultados

El cliente terminó el encargo con una plataforma en producción que puntúa tráfico en vivo en tiempo real, sostiene decenas de miles de peticiones por minuto con margen para picos varias veces mayores y conserva cada señal en bruto que ha visto jamás, de modo que una mejora de detección hecha hoy puede validarse contra años de tráfico real en lugar de contra un benchmark sintético.

Igual de importante, la arquitectura separa responsabilidades por las líneas que de verdad importan al negocio: la ruta caliente sigue siendo pequeña, rápida y aburrida, mientras que el análisis, la investigación y el trabajo con modelos ocurren detrás de una cola donde pueden crecer arbitrariamente sin amenazar nunca el tiempo de respuesta que ve un cliente.

Este es el tipo de sistema que asumimos de principio a fin: servicios de Go de alto rendimiento, una plataforma de datos guiada por eventos y la infraestructura de Kubernetes para ejecutar ambos. Si tienes una carga de trabajo con esta forma, cuéntanoslo.