100K Peticiones por Segundo con cpp-cache, Hash Routing y un Scaler Probado
100K Peticiones por Segundo con cpp-cache, Hash Routing y un Scaler Probado
Link to heading
Publicado por Simyl Research S.A.S. — Ingeniería de Backend de Alto Rendimiento
TL;DR — Una caché in-process de alto rendimiento en C++20, combinada con enrutamiento basado en hash y escalado horizontal, sostiene 100,000 peticiones por segundo en Kubernetes con una latencia p95 inferior a 5 ms y un ratio de aciertos (hit ratio) superior al 95%. Este post presenta la arquitectura, el modelo de concurrencia y las lecciones operativas aprendidas.
El Problema Central: Thundering Herd en las Cachés Link to heading
Considera cualquier operación costosa: una consulta a la base de datos, una llamada a una API externa o un cómputo complejo. Sin coordinación, múltiples hilos que soliciten la misma clave simultáneamente dispararán la operación costosa de forma independiente.
Este es el problema que resuelve cpp-cache mediante la implementación de un Escudo contra el Thundering Herd. Para entender por qué este mecanismo es crítico para la estabilidad del sistema más allá del simple almacenamiento de datos, recomendamos leer nuestro post anterior: Cuando una caché hace más que solo caché.
Arquitectura del Sistema Link to heading
El sistema completo consta de tres capas trabajando en concierto:
flowchart TD
Client["🌐 Clientes\n(navegadores, móviles, servicios)"]
LB["⚖️ Load Balancer / API Gateway\n(AWS ALB · NGINX · Envoy)"]
Router["🔀 Hash Router\n(anillo de hashing consistente)"]
subgraph GW ["Capa de Gateway — N procesos/pods de trabajo"]
GW1["Worker 1\ncpp-cache shard A"]
GW2["Worker 2\ncpp-cache shard B"]
GWN["Worker N\ncpp-cache shard Z"]
end
Backend["🗄️ Backend de Origen\n(base de datos · microservicio · API externa)"]
Client -->|HTTP/gRPC| LB
LB -->|cualquier enrutamiento| Router
Router -->|key hash → shard| GW1
Router -->|key hash → shard| GW2
Router -->|key hash → shard| GWN
GW1 -->|solo en fallos de caché| Backend
GW2 -->|solo en fallos de caché| Backend
GWN -->|solo en fallos de caché| Backend
El equilibrador de carga distribuye las conexiones; el enrutador de hash asegura que una clave determinada siempre caiga en el mismo proceso de trabajo y, por lo tanto, en la misma instancia de cpp-cache. Esta afinidad de enrutamiento convierte los fallos de caché aleatorios en una tasa de aciertos casi determinista limitada solo por la expiración LRU.
Capa 1 — cpp-cache: Arquitectura y Modelo de Concurrencia
Link to heading
cpp-cache es una caché in-process de C++20 (header-only) construida sobre las estructuras de datos de Aleph-w. Su modelo de concurrencia ha estado funcionando en producción durante más de seis años en su predecesor gateway_cache.
Componentes Internos Link to heading
Tres decisiones clave de implementación impulsan el rendimiento:
OLhashTable(direccionamiento abierto con sondeo lineal) ofrece búsquedas amigables para la caché sin saltos de punteros.- Mutex por entrada + variable de condición — diferentes claves nunca se bloquean entre sí; solo compiten los hilos que buscan la misma clave.
- Estadísticas libres de bloqueos mediante contadores
std::atomic<size_t>— los aciertos de caché nunca adquieren el mutex global, maximizando el rendimiento en el camino rápido.
Máquina de Estados de la Entrada Link to heading
Cada entrada de la caché transita por una máquina de estados bien definida que es el corazón de la prevención del thundering herd. El estado COMPUTING es el escudo: mientras un “solver” se ejecuta, cualquier otro hilo que solicite la misma clave encuentra el estado COMPUTING y espera en la variable de condición de la entrada en lugar de lanzar un cómputo duplicado. Cuando el solver termina, todos los hilos en espera son notificados atómicamente y comparten el resultado.
La Implementación de Producción en Go: gateway_cache y gw_cache
Link to heading
El protocolo de concurrencia descrito anteriormente no se originó en C++. Se probó primero en producción, escrito en Go, funcionando durante más de 6 años a 100K RPS para una plataforma de datos deportivos. Dos repositorios de Go mantienen este legado:
gateway_cache— la biblioteca de producción original.gw_cache— una variante independiente con una estructura más limpia.
Elegir entre Go y C++ Link to heading
Ambas implementaciones ejecutan el mismo protocolo de concurrencia verificado. En la práctica: comienza con Go si tu servicio ya está basado en Go — la integración es trivial y el historial en producción es largo. Usa C++ cuando necesites garantías de latencia p99 de sub-milisegundo, estés integrando la caché en un binario nativo o estés procesando cargas donde las pausas del recolector de basura (GC) sean inaceptables.
Capa 2 — Hash Routing Link to heading
El enrutamiento por hash de clave transforma una flota de cachés independientes en una caché lógicamente particionada y colectivamente enorme. Sin él, cada trabajador cachea todo el espacio de claves de forma independiente: se desperdicia memoria y la carga del backend se multiplica con el tamaño de la flota.
Con 150 nodos virtuales por nodo físico, el desequilibrio de carga se mantiene por debajo del 5% para distribuciones de claves típicas.
Capa 3 — El Escalador Predictivo Link to heading
A diferencia de las configuraciones estándar de Kubernetes que dependen del Horizontal Pod Autoscaler (HPA) basado en CPU o memoria — que a menudo reacciona demasiado lento a picos rápidos de tráfico — nosotros utilizamos un Escalador Predictivo propio.
Cómo funciona: Escalado basado en PID Link to heading
Nuestro escalador opera como un controlador de bucle cerrado (tipo PID) que escala en función de las Peticiones por Segundo (RPS) en tiempo real. Su eficiencia se basa en dos factores clave:
- Conocimiento de Capacidad Determinista: Hemos medido empíricamente cuántas RPS puede sostener un solo pod sin degradar la latencia.
- Lógica Anticipatoria: Al analizar las tendencias históricas y la velocidad actual de crecimiento del tráfico, el escalador “pronostica” el número de réplicas necesarias y las aprovisiona antes de que el tráfico alcance el pico.
Esto evita los picos de latencia de “arranque en frío” típicos del escalado reactivo y asegura que la flota esté siempre dimensionada correctamente para la carga entrante.
Lecciones de Producción Link to heading
- El enrutamiento por hash es el multiplicador de fuerza — la afinidad de clave es más impactante que cualquier aumento de tamaño de caché.
- TTL negativo + circuit breaker — pareja esencial; uno evita avalanchas por claves inexistentes, el otro limita la degradación del backend.
- Escalado predictivo sobre HPA reactivo — escalar basado en RPS y tendencias (tipo PID) es muy superior a escalar por CPU para tráfico de alto rendimiento.
- El failover es el eslabón más débil — Kubernetes reprograma los pods sin considerar el estado de la caché; el pico de caché fría es el mayor riesgo operativo.
- El periodo de calentamiento (warmup) importa — sube el tráfico linealmente a los nuevos pods durante 2–5 minutos.
Resumen Link to heading
| Capa | Tecnología | Contribución Clave |
|---|---|---|
| Caché L1 | cpp-cache (C++20) | Single-flight, LRU, ~50 µs de latencia |
| Enrutamiento | Hashing Consistente (xxHash) | Afinidad de clave, Capacidad efectiva N× |
| Caché L2 | ElastiCache Redis | Almacén compartido, ~500 µs de latencia |
| Origen | Aurora + microservicios | Recibe ~0.25% del tráfico total a 100K RPS |
| Scaler | Controlador PID Propio | Basado en RPS, predictivo y con análisis de tendencia |
La combinación sostiene 100K RPS con p95 por debajo de 5 ms y más del 95% de hit ratio, enviando menos del 1% del tráfico al origen.
En Simyl Research, diseñamos y construimos sistemas como este desde cero, desde la arquitectura de alto rendimiento hasta las operaciones en Kubernetes. Si tu backend está sufriendo bajo carga, contáctanos.
Tags: C++20 caching alto-rendimiento sistemas-distribuidos hash-routing kubernetes aws backend-engineering