Capa de interoperabilidad · Prueba técnica de arquitectura

6. Resiliencia, performance y escalabilidad

Conclusión. El pico 10x de lecturas no llega al legado: se resuelve desde Redis, con cuotas por partner que impiden que uno consuma la capacidad de todos. Lo que sí llega al legado pasa por adaptadores con límite de concurrencia, circuit breaker y Kafka como amortiguador. Los fallos asíncronos se reintentan con espera creciente y terminan en colas de fallidos con alerta. En el demo, con el pico 10x, el p95 de stock y precio queda en 1,6 ms sin errores y las llamadas al ERP no crecen; con el ERP caído, las lecturas siguen respondiendo (sección 6.8).

6.1 Rate limiting por partner y canal

Dos niveles:

Nivel Dónde Qué limita Contador
1. Tope general KrakenD Total por endpoint y un máximo igual para cualquier cliente En memoria de cada instancia
2. Cuota por partner Envoy (rate limit global de la malla) Por partner_id, nivel y grupo de rutas Compartido en Redis

Cuotas iniciales por nivel, en peticiones por segundo:

Nivel Lecturas (stock y precio) Órdenes Uso previsto
bronze 20 2 Partner nuevo o en pruebas
silver 100 10 Marketplace mediano
gold 500 50 Marketplace principal, e-commerce propio

Trade-off: dos lugares donde se configuran límites y un servicio de cuotas más que operar. Se eligió así porque KrakenD comunitario no tiene cuotas por partner (sección 3.4). Riesgo de construcción declarado: si el rate limit global de Envoy se complica, el plan B es un filtro de cuota en la plantilla de servicio con el mismo contador en Redis.

6.2 Cache: dónde, qué, TTL e invalidación

Dónde Qué TTL (time to live, vigencia) Invalidación
Redis (modelo de lectura) Stock, precio, producto, segmento Sin vencimiento funcional; TTL de seguridad de 24 horas (stock) y 7 días (precio) Por evento: cada cambio reescribe la llave
Gateway (microcache) Respuestas de GET /v1/stock y GET /v1/prices, por partner y consulta 2 segundos, solo en modo promo Por tiempo
Sidecar y gateway JWKS de Keycloak 10 minutos Por tiempo, con refresco ante llave desconocida
pricing-service Política de canal y listas de precios 60 segundos en memoria Por tiempo

No se cachea el estado de órdenes, las reservas ni nada de la ruta de crear orden.

El modelo de lectura no es una cache por vencimiento: es una vista materializada que se actualiza por evento, y la invalidación es el evento mismo. Se descartó la cache con vencimiento delante del legado porque cada vencimiento vuelve a golpearlo, y una avalancha de fallos de cache en plena promo lo tumba igual. Los tópicos de estado de Kafka son compactados (conservan el último valor por llave), así que reconstruir Redis desde cero es releerlos.

Frescura prometida, como objetivo de diseño:

Dato Frescura objetivo (p95) Umbral de stale: true
Stock 10 segundos desde el cambio en el WMS 5 minutos
Precio 60 segundos desde el cambio en el ERP 30 minutos

Trade-off: el dato puede estar levemente desactualizado. Se acepta porque se declara (asOf, stale), se mide, y porque la decisión que importa, prometer la unidad, no se toma con este dato sino con la reserva.

6.3 Flujo B: consulta masiva en promo, antes y después

En el AS-IS cada consulta de un canal termina en una consulta en línea al WMS o al ERP. Una promo que multiplica por 10 las consultas multiplica por 10 la carga de sistemas que no escalan, y el checkout propio cae junto con las consultas de los partners.

Flujo B en el AS-IS

En el TO-BE hay dos rutas con ritmos distintos: la de escritura va al ritmo de los cambios del legado y la de lectura al ritmo de los canales.

Flujo B en el TO-BE

Presupuesto de latencia p95, como objetivo de diseño: borde (Cloudflare y balanceador) 20 ms, gateway 10 ms, malla con dos sidecars 5 ms, servicio 15 ms, Redis 5 ms. Total interno: 55 ms, con margen amplio frente a los 300 ms. Capacidad: con el supuesto S4 de 2.000 consultas por segundo y lotes promedio de 10 SKU, son 20.000 lecturas de llave por segundo, holgado para un Redis con una réplica por zona.

Falla Comportamiento
Llave ausente en Redis Se lee de la base del servicio y se repone la llave
Redis caído Lectura desde la base con concurrencia limitada; si se satura, 503 con Retry-After
Kafka o adaptador detenido Se sigue respondiendo con el último dato; al superar el umbral, stale: true y alerta
ERP, WMS o enlace on-prem caídos Igual. El canal no percibe la caída del legado

6.4 Protección de los legados (backpressure)

Backpressure es el mecanismo por el que un sistema lento frena a quien le envía trabajo en lugar de ahogarse.

  1. Por diseño: los canales no llegan al legado. Solo los adaptadores lo hacen.
  2. Bulkhead por sistema (límite de concurrencia por dependencia): máximo inicial de 10 llamadas simultáneas al ERP, 10 al WMS y 20 al OMS, más un límite de llamadas por segundo.
  3. Kafka como amortiguador: los comandos hacia el OMS se consumen al ritmo que el OMS aguanta. Un pico de órdenes alarga la cola, no la carga del OMS.
  4. Circuit breaker por sistema (corte automático de llamadas a una dependencia que falla): abre con 50% de fallos en una ventana de 20 llamadas y prueba de nuevo a los 30 segundos. Abierto, el adaptador pausa su consumo en vez de quemar reintentos.
  5. Timeouts de 2 segundos por llamada al legado. Nunca una espera sin límite.
  6. Sondeo adaptativo: si el WMS responde lento, el adaptador alarga su intervalo.

Trade-off: con el legado lento, el dato tarda más en llegar a la capa (crece la frescura) y las órdenes tardan más en registrarse en el OMS. Se prefiere eso a que el legado caiga.

6.5 Reintentos y DLQ

Una DLQ (dead letter queue) es la cola donde quedan los mensajes que fallaron tras agotar los reintentos, para revisarlos o reprocesarlos sin bloquear el flujo.

Caso Política
gRPC de reserva 2 reintentos con espera exponencial y jitter (variación aleatoria de la espera), solo ante errores transitorios; presupuesto total de 300 ms
Llamadas de adaptadores al legado 3 intentos (200 ms, 1 s, 5 s) con jitter, solo en operaciones idempotentes
Consumo de eventos Reintento inmediato hasta 3 veces; luego al tópico de reintento con esperas de 1 minuto, 10 minutos y 1 hora; luego DLQ
Registro en OMS Igual, pero no va a DLQ antes de 24 horas: es un paso que debe lograrse
Webhooks a partners 8 intentos en 24 horas (10 s, 1 min, 5 min, 30 min, 1 h, 3 h, 6 h, 12 h); luego DLQ por partner

Trade-off: Kafka no trae reintentos ni DLQ nativos (con SNS y SQS, las colas y notificaciones de AWS, sí vienen), así que se construyen con tópicos. Se eligió Kafka porque permite releer el pasado para reconstruir el modelo de lectura y garantiza orden por llave.

6.6 Plan para promos

Antes:

  1. Prueba de carga a 10x del tráfico esperado, en un entorno igual al productivo, dos semanas antes.
  2. Subir el mínimo de réplicas de inventory-service, pricing-service y gateway por escalado programado. No depender del autoescalado reactivo en el primer minuto.
  3. Precalentar el modelo de lectura y verificar la frescura.
  4. Revisar cuotas con cada partner; subir de nivel a quien lo necesite.
  5. Congelar despliegues desde 24 horas antes.

Durante:

  1. Activar el modo promo: microcache de 2 segundos en el gateway.
  2. Subir la frecuencia de sondeo del WMS solo para los SKU en promoción.
  3. Tablero de promo a la vista: p95, frescura, cuotas agotadas, llamadas al legado, órdenes por estado.
  4. Degradaciones preparadas, de menor a mayor: servir dato con stale: true, bajar el tope visible de stock, reducir cuotas de los niveles bajos.

Después: revisión con datos, ajuste de colchones de seguridad y de cuotas.

6.7 Escalabilidad

Componente Cómo escala
Gateway y servicios de lectura Horizontal, por uso de procesador (CPU) y peticiones por segundo; sin estado
order-service y payment-service Horizontal; el orden lo da la partición de Kafka
Adaptadores No escalan con el tráfico: su límite es el legado
Redis Réplicas de lectura por zona; partición por llave si el catálogo crece 10x
PostgreSQL Una instancia por servicio; réplica de lectura para el respaldo de lecturas
Kafka Más particiones y consumidores

6.8 Evidencia

El mismo escenario de carga se corre contra los dos lados: el AS-IS (simuladores de ERP y WMS consultados directo, como hoy lo harían los canales) y el TO-BE (por la capa). La escala del demo es 20 consultas por segundo en base y 200 en pico: lo que importa es la proporción 10x. Las cifras son de un solo equipo de desarrollo con Docker Desktop, no de producción: sin red entre zonas, sin cifrado en tránsito, sin Cloudflare ni malla. Lo que se compara es el comportamiento a la misma escala y en el mismo equipo; la latencia de producción tiene su presupuesto en la sección 6.3. El umbral de stale del precio se bajó a 45 s para la prueba de caída (en el diseño es 30 minutos). Archivos crudos en tests/load/results/.

Medida AS-IS TO-BE
p95 de stock y precio en tráfico base Stock 128 ms, precio 196 ms 3 ms los dos
p95 de stock y precio en pico 10x 5.001 ms los dos: el timeout del canal 1,6 ms los dos
Tasa de error en pico 10x 100% en stock y en precio 0% en stock y en precio
Llamadas por segundo al ERP en pico 10x 200 (20 en la base: crecen 10x con el tráfico); el ERP alcanza a atender 67 0,9, igual que en la base: es el sondeo del adaptador y no depende de las consultas. Igual en el WMS
ERP lento (3 s de latencia) Precio con p95 de 5.001 ms (el timeout) y 99,6% de error p95 de precio 1,9 ms y 0% de error; el circuit breaker del adaptador abre y las llamadas al ERP bajan de 0,9 a 0,1 por segundo
Con el ERP caído, ¿responde stock y precio? Precio no (0,03% de respuestas correctas); stock sí, porque viene del WMS. Con el WMS caído, stock no Sí, los dos: 100% de respuestas correctas durante 3 minutos de caída, y lo mismo con el WMS caído. El precio sale stale: true pasado el umbral y vuelve a stale: false 5 s después de que el ERP regresa
Frescura p95: del cambio en el legado a la respuesta de la API No aplica: cada consulta lee el legado en línea Precio 1,2 s (objetivo 60 s); stock 1,2 s (objetivo 10 s)
Orden creada dos veces por reintento Sí: 20 de 20 órdenes reintentadas tras un timeout quedaron duplicadas No: 20 primeros envíos vencidos por el timeout del canal y reintentados con la misma Idempotency-Key dieron 20 órdenes en la capa y 20 en el OMS, 0 de más

Criterio de éxito en el TO-BE para el pico 10x: p95 menor a 300 ms, errores menores a 0,1% y llamadas al ERP y al WMS que no superan la base más 10%. Los seis escenarios de falla dirigida están en el anexo F.

La misma medición en la nube. El AS-IS se repitió en AWS el 14/11/2026 (17:30 a 17:48 UTC): simuladores del ERP, WMS y OMS en el clúster EKS (us-east-1, dos nodos m7i-flex.xlarge en dos zonas) y el generador de carga k6 dentro del mismo clúster, con los mismos scripts, la misma imagen y las mismas perillas que en el equipo de desarrollo. Sin internet, sin Cloudflare, sin gateway de entrada y sin malla: lo que cambia frente a la corrida local es la red entre nodos y el tope de procesador. Archivos crudos en tests/load/results/as-is-nube/.

Medida AS-IS en el equipo AS-IS en la nube TO-BE en la nube
p95 de stock y precio en tráfico base Stock 128 ms, precio 196 ms Stock 129 ms, precio 196 ms Stock 10,7 ms, precio 8,6 ms
p95 de stock y precio en pico 10x 5.001 ms los dos 5.000 ms los dos: el timeout del canal Stock 12,0 ms, precio 10,3 ms
Tasa de error en pico 10x 100% en stock y en precio 100% en stock y en precio 0% (36.001 consultas)
Llamadas por segundo al ERP en pico 10x 200; el ERP atiende 67 200 (20 en la base); el ERP atiende 66,6 0,9, igual que en la base
ERP lento (3 s de latencia) Precio con p95 de 5.001 ms y 99,6% de error Precio con p95 de 5.000 ms y 99,5% de error Precio con p95 de 8,6 ms y 0% de error
Con el ERP o el WMS caído Precio no (0,03%) con el ERP caído; stock no con el WMS caído Igual: precio 0,03% con el ERP caído, stock 0,03% con el WMS caído Stock y precio responden el 100% con el ERP caído y con el WMS caído
Orden creada dos veces por reintento 20 de 20 20 de 20 (40 órdenes en el OMS para 20 órdenes lógicas) 0 de más en 20
Un partner excede su cuota (escenario 6) No aplica No aplica Bronze a 5 veces su cuota: 20,2 aceptadas por segundo de 100 pedidas, 9.580 respuestas 429 (80%) con Retry-After; gold sin errores, p95 13,2 ms

El AS-IS da en la nube lo mismo que en el equipo de desarrollo: su límite no es la máquina, es que cada consulta llega en línea al legado. El TO-BE se midió en la nube el 15/11/2026 (16:49 a 17:11 UTC, y el escenario 6 repetido a las 17:40 UTC), con los mismos scripts, la misma imagen de k6 y las mismas perillas, entrando por el gateway de entrada de Istio con HTTPS. Los seis escenarios cumplen. Sus p95 son mayores que en el equipo de desarrollo por el TLS del gateway, el mTLS de la malla y la red entre nodos. La primera corrida del escenario 6 incumplió: a bronze se le aceptaron las 100 lecturas por segundo, sin ningún 429, porque el servicio de cuotas no tenía usuario en Redis; se arregló y se repitió, y el detalle está en el README de la corrida. Archivos crudos en tests/load/results/to-be-nube/.

El flujo de órdenes (flujo A) se verificó de punta a punta el 11/11/2026, con todas las piezas reales detrás del borde y el token de cada partner (make f4-e2e). Son pruebas de comportamiento, que pasan o fallan, y no de carga; no tienen columna AS-IS porque el AS-IS no tiene saga ni compensación que probar (sección 4). Algunos tiempos de reintento se acortaron para que la prueba dure minutos (el reintento hacia el OMS, por ejemplo, de 1 minuto a 3 s); la saga, los vencimientos y la consulta a la pasarela usan los valores del diseño. Límite de la medición del OMS: el simulador no puede reproducir un OMS real y lento que confirme una orden después de que el adaptador ya la buscó. oms-adapter consulta si la orden existe antes de cada intento, pero no envía una llave de idempotencia al OMS; el "0 de más" vale para el simulador y queda por probar contra un OMS real.

Prueba Resultado en el TO-BE
Escenario 4: la pasarela rechaza el pago Orden CANCELLED con PAYMENT_REJECTED, reserva RELEASED, el vendible baja con la reserva y vuelve a su valor; el partner recibe payment.rejected y order.cancelled firmados
Escenario 5: webhook de pago duplicado Un solo evento registrado, un solo cambio de estado y un solo payment.approved al partner. Un webhook con firma inválida recibe 401 en el borde
Escenario 5: webhook de pago perdido payment-service consulta a la pasarela y la orden se resuelve sola al minuto
OMS caído La orden queda CONFIRMED con omsSync pendiente; cuando el OMS vuelve se registra una sola vez
F9: la misma orden 5 veces en paralelo Una orden y una reserva: un 202 y cuatro 409 REQUEST_IN_PROGRESS; el sexto envío recibe la respuesta original
100 pedidos simultáneos por la última unidad Gana uno; los 99 restantes reciben INSUFFICIENT_STOCK; sin sobreventa
F7: un partner pide la orden de otro 404 NOT_FOUND, igual que por una orden inexistente
F6 y F8: trazabilidad y datos personales El correlationId de la orden aparece en los logs de los cinco componentes por los que pasa (order, inventory, payment, oms-adapter y webhook-service); ningún dato personal de la corrida aparece en los logs de los servicios ni del borde
Camino feliz PENDING_PAYMENT, CONFIRMED, DISPATCHED, DELIVERED. El despacho y la entrega salen del WMS simulado por wms-adapter (DispatchStarted, DeliveryConfirmed), ya no se publican a mano, y la reserva pasa a CONSUMED; el receptor del partner verifica la firma de cada webhook y recibe los eventos de la orden en su secuencia. Un evento de pago viaja por otro tópico y puede cruzarse con uno de orden: el partner ordena por time

La malla se probó en vivo el 14/11/2026 en un clúster local de Kubernetes (kind, Kubernetes in Docker: un clúster que corre en contenedores del equipo), con Istio y sus sidecars, el proveedor de identidad, el gateway y el gateway de entrada (make mesh-kind-test). El clúster se crea para la prueba y se borra al final.

Prueba Resultado en el TO-BE
Escenario 6: un partner excede su cuota El partner bronze pide 100 lecturas por segundo, 5 veces su cuota: recibe 20,1 por segundo y el resto (79,9%) en 429 RATE_LIMITED con Retry-After. El partner gold, al mismo tiempo: 0 errores y p95 de 6,6 ms (8,2 ms sin el exceso del otro)
mTLS estricto Una carga sin sidecar no logra conectarse a un servicio, ni con un token válido
Llamada directa a un servicio, sin pasar por el gateway 403 dentro de la malla
Salida al legado Solo el adaptador de cada sistema llega a él
Circuit breaker de la malla Expulsa al ERP lento, responde 503 de inmediato y se recupera a los 30 s

En el AS-IS el escenario 6 no aplica: no hay cuotas por partner y cada canal llega al legado sin límite propio. Archivo crudo: tests/load/results/to-be/escenario-6-cuota-20261114T202635Z.json.