El problema fundamental que aborda este artículo es la gestión de la carga de trabajo en sistemas distribuidos con requisitos de procesamiento en tiempo casi real y alta disponibilidad, particularmente en el contexto de escaneos de seguridad. La necesidad de escanear millones de entidades (cuentas, zonas DNS) con mayor frecuencia y para todos los usuarios, sin degradar el servicio, expuso cuellos de botella en un sistema existente.

Históricamente, los sistemas de escaneo y monitoreo de seguridad han luchado con el equilibrio entre la exhaustividad del análisis y la latencia de detección. A medida que las amenazas se automatizan y aceleran, la ventana para detectar y remediar configuraciones erróneas se reduce drásticamente. Esto empuja los sistemas de seguridad hacia arquitecturas de streaming de eventos y procesamiento distribuido, donde la eficiencia y la capacidad de respuesta son críticas. La solución no fue simplemente añadir más recursos, sino una reingeniería profunda de los componentes clave para optimizar su comportamiento bajo carga.

Arquitectura del Sistema

El sistema de escaneo se basa en un scheduler que publica mensajes en Apache Kafka, una plataforma de streaming de eventos distribuida. Estos mensajes son consumidos por 'checkers', microservicios escritos en Go, que realizan escaneos específicos. Los resultados de los escaneos se envían a una API interna que los persiste en una base de datos PostgreSQL.

Para escalar, se implementaron varias optimizaciones. En Kafka, se abordó el problema de 'head-of-line blocking' debido al procesamiento ordenado por partición. Los checkers fueron modificados para consumir mensajes en lotes y procesarlos concurrentemente usando goroutines. Se introdujo una estrategia de 'carril lento/carril rápido' para los checkers, donde los mensajes de procesamiento lento se dirigen a un grupo de consumidores dedicado para evitar bloquear a los mensajes rápidos. La persistencia en PostgreSQL se optimizó reemplazando inserciones individuales en bucle con un enfoque híbrido: UNNEST para pequeños lotes y COPY para grandes volúmenes, reduciendo drásticamente los viajes de ida y vuelta a la base de datos. Los problemas de latencia de la API se resolvieron cambiando de una configuración activo-activo a activo-pasivo, asegurando que la instancia activa de la API estuviera geográficamente colocalizada con la base de datos primaria. Finalmente, el scheduler fue rediseñado para programar zonas independientemente de las cuentas, aleatorizar los tiempos de last_scheduled_at y aplicar un rate limiting adaptativo para distribuir uniformemente la carga de escaneo a lo largo del tiempo, evitando picos de tráfico en Kafka.

Flujo de Escaneo de Seguridad

  1. 1 Scheduler Identifica cuentas/zonas para escanear y publica mensajes.
  2. 2 Apache Kafka Plataforma de streaming de eventos, distribuye mensajes a particiones.
  3. 3 Checkers (Go Microservices) Consumen mensajes en lotes, procesan concurrentemente (goroutines).
  4. 4 API Interna Recibe resultados de escaneo de los checkers.
  5. 5 PostgreSQL Database Persiste los insights de seguridad usando inserciones optimizadas.
CapaTecnologíaJustificación
messaging Apache Kafka Plataforma de streaming de eventos distribuida para desacoplar el scheduler de los checkers y gestionar la cola de escaneos. Particiones para ordenamiento, grupos de consumidores.
compute Go Microservices (Checkers) Servicios especializados que realizan las tareas de escaneo. Utilizan goroutines para procesamiento concurrente de mensajes de Kafka. Procesamiento en lotes, goroutines por mensaje, estrategia 'slow/fast lane'.
storage PostgreSQL Base de datos relacional para persistir los resultados de los escaneos (insights de seguridad). Uso de UNNEST y COPY para inserciones masivas.
networking API Interna Interfaz para que los checkers envíen los resultados de los escaneos a la base de datos. Cambio de activo-activo a activo-pasivo para reducir latencia.
orchestration Scheduler (Custom) Componente que programa los escaneos, publicando mensajes en Kafka. Programación independiente de zonas, aleatorización de `last_scheduled_at`, rate limiting adaptativo.

Trade-offs

Ganancias
  • ▲▲ Throughput de escaneo
  • Frecuencia de escaneo
  • Estabilidad del sistema
  • Cobertura de escaneo
Costes
  • Complejidad de reintento en checkers (por procesamiento por lotes)
  • Uso de memoria en checkers (por procesamiento por lotes)
  • Bloat en tablas del sistema Postgres (con COPY a tabla temporal)
for _, msg := range batch {
  go func(m Message) {
    // process message m
    // send results to API
  }(msg)
}
Los checkers consumen mensajes de Kafka en lotes y procesan cada mensaje en una goroutine separada para aumentar la concurrencia.
INSERT INTO table (col1, col2, ...) 
SELECT s.col1, s.col2, ... 
FROM UNNEST($1::type1[], $2::type2[], ...) AS s(col1, col2, ...) 
ON CONFLICT DO UPDATE ...
Ejemplo conceptual de cómo se usaría UNNEST para inserciones de lotes pequeños, evitando múltiples `INSERT ... VALUES`.
func computeRate(free, pro, biz, ent int64) rate.Limit {
  r := float64(free)/freeScanInterval.Seconds() +
       float64(pro)/proScanInterval.Seconds() +
       float64(biz)/bizScanInterval.Seconds() +
       float64(ent)/entScanInterval.Seconds()
  if r < 1 {
    r = 1
  }
  r *= rateLimitBufferFactor
  return rate.Limit(r)
}
Función para calcular la tasa de escaneo requerida basándose en el número de cuentas por plan y sus intervalos de escaneo, con un factor de buffer.

Fundamentos Teóricos

El problema de 'head-of-line blocking' en sistemas de colas es un concepto bien estudiado en la teoría de colas y redes de computadoras, donde un elemento lento en una secuencia ordenada puede retrasar a todos los elementos subsiguientes. La solución de dividir el tráfico en 'carriles' (slow/fast lanes) es una aplicación práctica de la priorización de tráfico, un principio común en QoS (Quality of Service) de redes. La optimización de inserciones masivas en bases de datos relacionales, como el uso de COPY o UNNEST en PostgreSQL, se relaciona con la eficiencia de las operaciones de E/S y la minimización de la sobrecarga transaccional, conceptos fundamentales en el diseño de sistemas de bases de datos. El rate limiting adaptativo para la programación de tareas se alinea con los principios de control de congestión y equilibrio de carga, buscando una distribución uniforme de la demanda sobre los recursos disponibles, una preocupación central en sistemas distribuidos a gran escala.