La lección anterior terminó con una pregunta incómoda: el pipeline de Kilómetro Cero construye, firma y despliega la versión 1.15.0 de pedidos, ejecuta pytest, verifica contratos y observa los SLO durante el canary, pero ¿qué prueba demuestra que la saga de 03-05 compensa correctamente cuando inventario cae a mitad de camino? ¿Cómo sabemos que el circuit breaker de 07-04 se abre a tiempo, o que Patroni conmuta en menos de 30 segundos, antes de que ocurra un sábado por la tarde en plena Semana de la Vendimia? En un programa que corre en un solo proceso, una prueba unitaria bien escrita responde a casi todo. En un sistema distribuido no: el no determinismo, los fallos parciales y el tiempo hacen que muchas propiedades importantes solo se puedan comprobar ejercitando dependencias reales, provocando fallos a propósito y observando el sistema completo. Esta lección cierra el Módulo 7 con las dos disciplinas que convierten la confianza en evidencia: las pruebas adaptadas a lo distribuido (integración con contenedores, contratos, carga, resiliencia y pruebas en producción) y la ingeniería del caos, que inyecta en la plataforma los mismos fallos que estudiamos en 07-03 y 07-04, con hipótesis explícitas y radio de explosión controlado.
Contenido
- Por qué probar un sistema distribuido es más difícil
- La pirámide de pruebas adaptada
- Pruebas de integración con dependencias reales: Testcontainers y la saga
- Pruebas de contrato: consumidores, productores y esquemas
- Pruebas de carga: la Semana de la Vendimia con k6
- Pruebas de resiliencia y pruebas en producción
- Simulación determinista y verificación formal: visión general
- Ingeniería del caos: principios, herramientas y ciclo de un experimento
- Experimentos de caos en Kilómetro Cero
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Por qué probar un sistema distribuido es más difícil
Una prueba clásica se apoya en tres supuestos: la misma entrada produce la misma salida, el entorno de la prueba se parece al real y, si algo falla, falla del todo. Los sistemas distribuidos rompen los tres.
| Dificultad | Qué significa en Kilómetro Cero | Consecuencia para las pruebas |
|---|---|---|
| No determinismo | Dos consumidores del tópico pedidos.eventos pueden procesar los mensajes en un orden distinto en cada ejecución; el planificador y la red deciden. |
Una prueba que pasa 99 veces y falla 1 (flaky) puede estar revelando una carrera real, no un problema de la prueba. |
| Entornos | En local no hay tres nodos de Patroni, ni particiones de Kafka, ni Kong delante. Un mock de inventario nunca devuelve UNAVAILABLE con 800 ms de latencia. |
Las pruebas unitarias con dobles son necesarias pero insuficientes: el comportamiento que importa está en la integración. |
| Fallos parciales | pagos responde, inventario no, y pedidos tiene que decidir qué hacer con P-2026-000125 a medias. |
Hay que probar estados intermedios (STOCK_RESERVADO sin pago), no solo el camino feliz y el error total. |
| Tiempo | Timeouts, reintentos con backoff, ventanas del circuit breaker, TTL de Redis, heartbeats del DetectorPhi. |
El resultado depende de cuándo ocurre cada cosa; probar "esperando 5 segundos" es lento y frágil. |
| Escala | Un error que aparece con 500 pedidos por segundo no aparece con 5. | Algunas propiedades solo se verifican con carga realista. |
De aquí sale la idea central de la lección: en un sistema distribuido, la pregunta "¿funciona?" se descompone en varias preguntas distintas, cada una con su tipo de prueba, y ninguna de ellas basta sola.
- La pirámide de pruebas adaptada
La pirámide clásica (muchas unitarias, algunas de integración, pocas de extremo a extremo) sigue siendo válida, pero en distribuido se le añaden capas que no existían y cambia el peso de otras.
| Nivel | Qué verifica | Dependencias | Velocidad | Cantidad recomendada | Ejemplo en Kilómetro Cero |
|---|---|---|---|---|---|
| Unitarias | Lógica pura de un servicio | Ninguna (dobles) | ms | Muchas | Cálculo del total de un pedido, transición de estados de SagaPedido con repositorio en memoria |
| Integración con dependencias reales | El servicio contra su base de datos, su broker, su caché | Contenedores efímeros | segundos | Bastantes | La saga contra PostgreSQL y Kafka reales (apartado 3) |
| Contrato | Que productor y consumidor de una API o evento siguen de acuerdo | Solo el contrato | ms-s | Una por pareja | pedidos ↔ inventario sobre inventario.proto (apartado 4) |
| Extremo a extremo | Un flujo completo de negocio a través de todos los servicios | Entorno completo | minutos | Muy pocas | Crear un pedido de Ana desde Kong hasta que analitica lo cuenta |
| Rendimiento y carga | Que el sistema cumple los SLO bajo la demanda prevista | Entorno parecido a producción | minutos-horas | Por versión y campaña | Semana de la Vendimia con k6 (apartado 5) |
| Resiliencia | Que los patrones de 07-04 actúan como se diseñaron ante fallos inyectados | Entorno + herramienta de inyección | minutos | Por patrón crítico | Toxiproxy entre pedidos e inventario (apartados 6 y 9) |
| En producción | Que la versión real, con datos reales, se comporta | Producción | continuo | Siempre activas | Canary de 07-05, sondas sintéticas de 07-01 |
Dos observaciones prácticas:
- Las pruebas de extremo a extremo son pocas y caras a propósito. Cada una necesita todo el entorno levantado, tarda minutos y, cuando falla, señala "algo entre siete servicios". Se reservan para dos o tres flujos que, si se rompen, hunden el negocio: crear pedido, pagar, entregar.
- Las de contrato sustituyen a muchas de extremo a extremo: si
pedidoseinventariodemuestran por separado que respetan el mismo contrato, no hace falta desplegarlos juntos para saber que se entenderán.
- Pruebas de integración con dependencias reales: Testcontainers y la saga
Un mock de PostgreSQL no hace SELECT ... FOR UPDATE, no tiene deadlocks y no rechaza una transacción por violación de clave única. Un mock de Kafka no reparte particiones ni reentrega mensajes. Testcontainers resuelve esto arrancando, desde la propia prueba, contenedores Docker efímeros con las dependencias reales, y destruyéndolos al terminar. Cada ejecución parte de un estado limpio y la prueba puede correr igual en el portátil de Marta y en el pipeline de 07-05.
Vamos a probar lo más delicado de 03-05: que la saga de pedido, cuando pagos rechaza el cobro después de haber reservado stock, compensa (libera la reserva) y termina en CANCELADA, dejando además el evento correspondiente en la outbox de 02-05.
# km0/tests/test_saga_pedido.py
import json
import pytest
import psycopg
from testcontainers.postgres import PostgresContainer
from testcontainers.kafka import KafkaContainer
from kafka import KafkaConsumer
from servicios.pedidos.saga_pedido import SagaPedido, EstadoSaga
from servicios.pedidos.repositorio import RepositorioSagas
from servicios.pedidos.outbox import PublicadorOutbox
@pytest.fixture(scope="session")
def postgres():
# Un solo contenedor de PostgreSQL para toda la sesión de pruebas:
# arrancarlo cuesta ~3 s, y cada test limpia sus tablas.
with PostgresContainer("postgres:16") as pg:
yield pg
@pytest.fixture(scope="session")
def kafka():
with KafkaContainer("confluentinc/cp-kafka:7.6.0") as kf:
yield kf
@pytest.fixture
def conexion(postgres):
# Conexión real; se crea el esquema mínimo que la saga necesita.
with psycopg.connect(postgres.get_connection_url().replace("+psycopg2", "")) as con:
with open("sql/pedidos_sagas.sql") as f:
con.execute(f.read()) # tablas sagas, outbox, mensajes_procesados
yield con
con.execute("TRUNCATE sagas, outbox, mensajes_procesados")
con.commit()
class InventarioFalso:
"""Doble del cliente gRPC de inventario. Registra las llamadas para
poder afirmar que la compensación se ejecutó."""
def __init__(self):
self.reservas = []
self.liberaciones = []
def reservar_stock(self, pedido_id, producto, unidades):
self.reservas.append((pedido_id, producto, unidades))
return {"reserva_id": f"R-{pedido_id}"}
def liberar_reserva(self, reserva_id):
self.liberaciones.append(reserva_id)
class PagosRechaza:
"""Doble de pagos que siempre rechaza el cobro."""
def cobrar(self, pedido_id, importe):
raise RuntimeError("tarjeta rechazada")
def test_saga_compensa_si_pagos_falla(conexion, kafka):
inventario = InventarioFalso()
outbox = PublicadorOutbox(conexion, bootstrap=kafka.get_bootstrap_server())
saga = SagaPedido(
repo=RepositorioSagas(conexion),
inventario=inventario,
pagos=PagosRechaza(),
outbox=outbox,
)
saga.ejecutar(pedido_id="P-2026-000125", cliente="lucia",
lineas=[("queso-curado", 2)], importe=18.40)
# 1. El estado final persistido en la tabla `sagas` es CANCELADA
fila = conexion.execute(
"SELECT estado FROM sagas WHERE pedido_id = %s", ("P-2026-000125",)
).fetchone()
assert fila[0] == EstadoSaga.CANCELADA.value
# 2. La compensación liberó exactamente la reserva que se hizo
assert inventario.reservas == [("P-2026-000125", "queso-curado", 2)]
assert inventario.liberaciones == ["R-P-2026-000125"]
# 3. El evento PedidoCancelado salió por la outbox hacia Kafka de verdad
outbox.publicar_pendientes()
consumidor = KafkaConsumer(
"pedidos.eventos",
bootstrap_servers=kafka.get_bootstrap_server(),
auto_offset_reset="earliest",
consumer_timeout_ms=5000,
)
tipos = [json.loads(m.value)["tipo"] for m in consumidor]
assert "PedidoCancelado" in tiposQué hay que entender de esta prueba, línea a línea:
- Las fixtures
postgresykafkatienenscope="session": los contenedores se arrancan una vez y se comparten. Arrancar Kafka por cada test haría la suite inutilizable. - La fixture
conexionejecuta el mismosql/pedidos_sagas.sqlque usa el despliegue real. Si alguien cambia el esquema y olvida la migración, esta prueba lo detecta. ElTRUNCATEfinal garantiza aislamiento entre tests. InventarioFalsoyPagosRechazason dobles de los otros servicios, no de la infraestructura. Este es el criterio de la pirámide: en una prueba de integración depedidosse usan de verdad sus dependencias (su base de datos, su broker) y se sustituyen los servicios ajenos, cuyo comportamiento se garantiza aparte con contratos.- Las tres aserciones cubren las tres caras de la compensación: estado persistido, acción compensatoria ejecutada y evento publicado. Si la saga marcase
CANCELADApero olvidase liberar el stock, la segunda aserción fallaría, que es exactamente el error silencioso que dejaría a "Quesería Montblanc" con unidades bloqueadas. - Para verificar el evento se consume del Kafka real del contenedor. Esto prueba el patrón Outbox completo de 02-05, incluida la serialización de la envoltura
id_evento/tipo/version/fecha_ms/origen/datos.
Con la misma estructura se escriben los otros casos que importan: inventario devuelve UNAVAILABLE en la primera reserva (debe reintentar según 07-04 y no crear dos reservas), el proceso muere entre STOCK_RESERVADO y PAGO_CONFIRMADO (al rearrancar, la saga debe retomarse desde la tabla sagas), y el mismo evento llega dos veces (la tabla mensajes_procesados debe descartar el duplicado).
- Pruebas de contrato: consumidores, productores y esquemas
Cuando pedidos (consumidor) llama a ReservarStock de inventario (productor), ambos dependen de un acuerdo: campos, tipos, códigos de error. Una prueba de contrato fija ese acuerdo en un artefacto verificable por los dos lados, sin desplegarlos juntos.
Contratos dirigidos por el consumidor (Pact)
En el enfoque consumer-driven, es el consumidor quien declara qué necesita. pedidos escribe: "cuando llamo a ReservarStock con producto=queso-curado, unidades=2, espero una respuesta con reserva_id de tipo cadena y estado=RESERVADO". Ese pact se publica en un broker de contratos y el pipeline de inventario lo verifica contra su implementación real en cada cambio. Si inventario renombra reserva_id a id_reserva, su propio pipeline falla antes de desplegar, y el mensaje de error dice qué consumidor se rompería.
Compatibilidad de esquemas protobuf y de eventos
Para gRPC y Kafka, buena parte del contrato ya está en los esquemas de 02-03 (contratos/inventario.proto) y 02-05 (la envoltura de eventos). Lo que hay que verificar es que un cambio de esquema sea compatible hacia atrás: los consumidores viejos deben seguir entendiendo los mensajes nuevos.
# km0/tests/contrato/pedidos_inventario.py
"""Contrato consumidor-productor entre pedidos e inventario.
Dos verificaciones:
1. pedidos declara los campos que usa; inventario los debe seguir sirviendo.
2. La versión nueva de inventario.proto es compatible con la publicada.
"""
import subprocess
import pytest
from google.protobuf import descriptor_pb2
from contratos import inventario_pb2, inventario_pb2_grpc
# --- 1. Expectativas del consumidor -------------------------------------
# Lo que `pedidos` usa realmente de la respuesta (grep del código + revisión).
CAMPOS_QUE_USA_PEDIDOS = {
"ReservarStockRespuesta": {"reserva_id", "estado", "unidades_reservadas"},
"ConsultarStockRespuesta": {"producto", "disponible"},
}
# Códigos gRPC ante los que `pedidos` tiene lógica de reintento/compensación.
CODIGOS_ESPERADOS = {"UNAVAILABLE", "FAILED_PRECONDITION", "DEADLINE_EXCEEDED"}
@pytest.mark.parametrize("mensaje,campos", CAMPOS_QUE_USA_PEDIDOS.items())
def test_inventario_sigue_sirviendo_los_campos_que_pedidos_usa(mensaje, campos):
descriptor = getattr(inventario_pb2, mensaje).DESCRIPTOR
campos_reales = {f.name for f in descriptor.fields}
faltan = campos - campos_reales
assert not faltan, f"inventario.proto ya no define {faltan} en {mensaje}"
def test_servicio_expone_los_metodos_del_contrato():
metodos = {m.name for m in inventario_pb2.DESCRIPTOR.services_by_name["Inventario"].methods}
assert {"ReservarStock", "ConsultarStock"} <= metodos
# --- 2. Compatibilidad hacia atrás del .proto ---------------------------
def _descriptor_set(ruta_proto: str) -> descriptor_pb2.FileDescriptorSet:
"""Compila un .proto a un FileDescriptorSet binario con protoc."""
salida = subprocess.check_output([
"protoc", "--include_imports", "-I", "contratos",
"--descriptor_set_out=/dev/stdout", ruta_proto,
])
fds = descriptor_pb2.FileDescriptorSet()
fds.ParseFromString(salida)
return fds
def _campos_por_mensaje(fds):
resultado = {}
for fichero in fds.file:
for msg in fichero.message_type:
resultado[msg.name] = {f.number: (f.name, f.type, f.label) for f in msg.field}
return resultado
def test_proto_nuevo_es_compatible_con_el_publicado():
publicado = _campos_por_mensaje(_descriptor_set("contratos/publicado/inventario.proto"))
nuevo = _campos_por_mensaje(_descriptor_set("contratos/inventario.proto"))
for nombre, campos_viejos in publicado.items():
assert nombre in nuevo, f"Se ha eliminado el mensaje {nombre}"
for numero, (nombre_campo, tipo, etiqueta) in campos_viejos.items():
assert numero in nuevo[nombre], (
f"{nombre}: se ha eliminado el campo {numero} ({nombre_campo}); "
"en protobuf los campos se marcan `reserved`, no se borran"
)
assert nuevo[nombre][numero][1] == tipo, (
f"{nombre}.{nombre_campo}: ha cambiado el tipo del campo {numero}"
)Explicación:
- La primera parte codifica lo que el consumidor necesita como datos (
CAMPOS_QUE_USA_PEDIDOS) y lo comprueba contra el descriptor del.protocompilado. Es la versión ligera del enfoque Pact: no simula llamadas, pero atrapa el 90 % de las rupturas (campos renombrados o eliminados, métodos desaparecidos). - La segunda parte compara el
.protonuevo con la copia publicada (la última versión desplegada, guardada encontratos/publicado/). Las reglas de compatibilidad de protobuf son concretas: no reutilizar ni eliminar números de campo, no cambiar tipos, añadir campos solo como opcionales. Un consumidor compilado con el esquema viejo ignora los campos nuevos y sigue funcionando. - Para los eventos de Kafka, la misma idea se aplica sobre la envoltura de 02-05: el campo
versionexiste precisamente para esto, y un test análogo verifica que losdatosde la versión N+1 contienen todos los campos de la versión N. Con un schema registry la verificación puede ser automática al registrar el esquema.
- Pruebas de carga: la Semana de la Vendimia con k6
En la Semana de la Vendimia, "Bodega Roble Alto" lanza el vino-crianza con descuento y el tráfico de pedidos se multiplica por ocho respecto a un martes normal. La prueba de carga responde a una pregunta muy concreta: ¿cumple la versión 1.15.0 el SLO de 07-01 (99,9 % de éxito y p99 < 500 ms) bajo ese tráfico? Sin umbrales ligados al SLO, una prueba de carga solo genera gráficas bonitas.
k6 describe la carga en JavaScript y evalúa umbrales al terminar, devolviendo un código de salida distinto de cero si no se cumplen, lo que permite integrarla como una etapa más del pipeline.
// km0/tests/carga/vendimia.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Métricas propias, con nombres alineados con las de Prometheus de 07-01
const errores = new Rate('km0_pedidos_error_rate');
const duracionPedido = new Trend('km0_crear_pedido_ms', true);
export const options = {
// Perfil de la campaña: rampa de entrada, meseta de 20 min al x8, bajada.
stages: [
{ duration: '3m', target: 50 }, // 50 usuarios virtuales: tráfico base
{ duration: '5m', target: 400 }, // subida hasta el pico (x8)
{ duration: '20m', target: 400 }, // meseta: aquí se mide de verdad
{ duration: '3m', target: 0 }, // drenado
],
thresholds: {
// Umbrales = SLO de 07-01. Si se incumplen, k6 sale con error.
'km0_pedidos_error_rate': ['rate<0.001'], // 99,9 % de éxito
'km0_crear_pedido_ms': ['p(99)<500', 'p(95)<250'], // p99 < 500 ms
'http_req_failed': ['rate<0.001'],
},
};
const BASE = __ENV.KM0_URL || 'https://staging.km0.example/api/v1';
const TOKEN = __ENV.KM0_TOKEN; // JWT de Keycloak para un cliente de pruebas
const productos = ['vino-crianza', 'vino-crianza', 'vino-crianza', 'queso-curado', 'tomate-rosa'];
export default function () {
// 1. Consulta del catálogo (lectura barata, mucho más frecuente que el pedido)
const cat = http.get(`${BASE}/catalogo/productos?mercado=girona`);
check(cat, { 'catalogo 200': (r) => r.status === 200 });
// 2. Crear pedido: la operación que gobierna el SLO
const producto = productos[Math.floor(Math.random() * productos.length)];
const cuerpo = JSON.stringify({
cliente: `carga-${__VU}`,
lineas: [{ producto, unidades: 1 + Math.floor(Math.random() * 3) }],
});
const res = http.post(`${BASE}/pedidos`, cuerpo, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TOKEN}`,
'X-Request-Id': `k6-${__VU}-${__ITER}`, // Kong lo propaga: trazable en Tempo
},
tags: { operacion: 'crear_pedido' },
});
const ok = check(res, {
'pedido 201': (r) => r.status === 201,
'devuelve id': (r) => r.status === 201 && r.json('pedido_id') !== undefined,
});
errores.add(!ok);
duracionPedido.add(res.timings.duration);
sleep(1 + Math.random() * 2); // tiempo de "pensar" del usuario
}Cómo leer el guion:
stagesreproduce la forma del tráfico, no solo su volumen. El pico llega tras una rampa, como ocurre cuando se envía el boletín de la campaña, y hay 20 minutos de meseta porque los problemas de memoria, conexiones agotadas o lag de Kafka aparecen con el tiempo, no en el primer minuto.thresholdstraduce el SLO a condiciones ejecutables. Que el umbral tenga los mismos números quealertas.ymlde 07-01 no es casualidad: si la prueba pasa en staging pero la alerta salta en producción, la diferencia está en el entorno, no en el criterio.- El
X-Request-Idcon prefijok6-permite filtrar en Loki y Tempo (07-02) las trazas de la prueba, y también excluirlas de las métricas de negocio si la prueba se ejecuta contra producción. - Se mezclan lecturas de catálogo y escrituras de pedidos en proporción realista. Una prueba que solo crea pedidos sobrecarga
km0_pedidospero deja Redis ycatalogofríos, y no descubriría que la caché se invalida demasiado a menudo. - Durante la meseta se observan además las métricas del lado del servidor: el HPA de 07-05 debe escalar
pedidos, el lag del consumidor deanaliticano debe crecer sin límite ykm0_circuit_estadodebe permanecer en cerrado. La prueba de carga es también una prueba de la automatización.
Locust es la alternativa en Python, con la misma filosofía (usuarios virtuales con comportamiento programado) y una interfaz web para seguir la prueba en directo; la elección entre ambos es de preferencia de lenguaje.
- Pruebas de resiliencia y pruebas en producción
Pruebas de resiliencia
Los patrones de 07-04 tienen una propiedad incómoda: solo actúan cuando algo va mal, así que en el camino feliz nunca se ejecutan. Una suite que no inyecta fallos puede tener el 100 % de cobertura de líneas y no haber ejercitado jamás la apertura de un circuit breaker. Las pruebas de resiliencia inyectan tres familias de fallo:
| Fallo inyectado | Herramienta típica | Qué debe observarse (07-04) |
|---|---|---|
Latencia (p. ej. +800 ms en inventario) |
Toxiproxy, tc netem |
con_timeout corta la espera; el PresupuestoReintentos no se agota; el p99 sube pero no explota |
Errores (5xx, UNAVAILABLE) |
Doble del servicio, Toxiproxy reset_peer |
reintentar aplica backoff con jitter; tras N fallos el CircuitBreaker abre (km0_circuit_estado=2) y km0_circuit_rechazos_total crece |
| Partición (corte total entre dos servicios) | Toxiproxy timeout, Chaos Mesh NetworkChaos |
El fallback responde con datos de caché (km0_fallback_total); la saga entra en COMPENSANDO si corresponde; nada se queda colgado |
El apartado 9 recoge una de estas pruebas con código.
Pruebas en producción
Por muy fiel que sea staging, hay cosas que solo existen en producción: los datos reales, el tráfico real y las integraciones reales (la pasarela de pagos, la flota de reparto). "Probar en producción" no significa saltarse las pruebas anteriores, sino añadir técnicas que limitan el daño de lo que aún no se sabe:
- Canary (07-05): la versión nueva recibe un 5 % del tráfico y se compara automáticamente contra el SLO. Es una prueba en producción con rollback automático.
- Feature flags: la funcionalidad se despliega apagada y se enciende por segmentos (primero para
jsalaympuig, luego para el mercado de Lleida, luego para todos). Separa el despliegue del lanzamiento y permite apagar sin redesplegar. - Shadow traffic (tráfico sombra): Kong duplica las peticiones reales hacia la versión nueva, cuya respuesta se descarta pero se mide. Permite ver cómo se comporta
pedidos 1.16.0con el tráfico de un sábado sin que ningún cliente lo sufra. Exige cuidado con los efectos secundarios: el tráfico sombra no debe cobrar ni reservar stock de verdad. - Synthetic monitoring (07-01): las sondas blackbox ejecutan continuamente un flujo de compra de prueba con un cliente ficticio. Son la prueba de extremo a extremo que nunca deja de ejecutarse.
- Simulación determinista y verificación formal: visión general
Hay una familia de técnicas que atacan directamente el no determinismo del apartado 1. Conviene conocerlas aunque en Kilómetro Cero no se apliquen de forma exhaustiva:
- Jepsen somete a bases de datos y sistemas de coordinación (Cassandra, etcd, Kafka, PostgreSQL con Patroni...) a particiones, relojes desviados y caídas, mientras un verificador comprueba si el historial de operaciones observado respeta las garantías prometidas (linealizabilidad, aislamiento de transacciones). Sus informes públicos son la mejor lectura para entender qué significan de verdad las promesas de consistencia de 03-01, y buena parte de las "sorpresas" que documentan son lo que uno debería esperar de la tecnología que elija.
- TLA+ es un lenguaje de especificación con el que se describe el algoritmo (por ejemplo, la saga de 03-05 con sus estados y compensaciones) y un comprobador de modelos explora todos los entrelazados posibles buscando estados que violen un invariante ("nunca hay un pedido COMPLETADA sin PAGO_CONFIRMADO"). Encuentra errores de diseño que ninguna prueba encontraría, porque no dependen de que la ejecución concreta pase por el caso malo.
- Simulación determinista (el enfoque de FoundationDB): el sistema entero se ejecuta en un único hilo con un planificador y una red simulados que se controlan con una semilla. Los fallos se inyectan sistemáticamente y, cuando algo se rompe, la misma semilla reproduce el fallo exactamente. Es la respuesta más radical al problema del flaky test: si es reproducible, deja de ser flaky.
Estas técnicas son costosas y exigen que el sistema se haya diseñado para ellas. Para la mayoría de plataformas, incluida Kilómetro Cero, el camino realista es combinar pruebas de integración y contrato, carga con umbrales de SLO, y la disciplina del siguiente apartado.
- Ingeniería del caos: principios, herramientas y ciclo de un experimento
La ingeniería del caos es la práctica de provocar fallos a propósito en un sistema para descubrir debilidades antes de que las descubra un incidente. No es "romper cosas": es experimentación con método científico. Sus principios:
- Definir el estado estable en términos de métricas de negocio y de SLO, no de recursos internos: "se crean 40 pedidos/min con 99,9 % de éxito", no "la CPU está al 40 %".
- Formular una hipótesis sobre qué pasará: "si matamos al líder de Patroni, el estado estable se mantiene salvo un bache de escrituras de menos de 30 s".
- Variar eventos del mundo real: los fallos que se inyectan son los que ocurren (caída de nodo, latencia, partición, disco lleno, reloj desviado), no fallos exóticos.
- Ejecutar en producción, o lo más cerca posible, porque es el único entorno cuyo comportamiento importa; pero con radio de explosión controlado: un pod, un mercado, un 5 % del tráfico, con botón de parada y en horario con el equipo presente.
- Automatizar los experimentos que ya se han pasado una vez, para que sigan pasando cuando la plataforma cambie. Un experimento que solo se ejecutó a mano en marzo no dice nada sobre la versión de septiembre.
Herramientas
| Herramienta | Dónde actúa | Qué inyecta | Uso en Kilómetro Cero |
|---|---|---|---|
| Chaos Mesh / Litmus | Kubernetes (CRD) | Muerte de pods, NetworkChaos (latencia, pérdida, partición), IOChaos, StressChaos, TimeChaos |
Experimentos declarativos sobre k8s/, programables como cron |
| Toxiproxy | Proxy TCP entre dos servicios | Latencia, jitter, límite de ancho de banda, cortes, reset de conexión | Pruebas de resiliencia reproducibles en local y en CI |
tc netem |
Kernel Linux (una máquina) | Latencia, pérdida, reordenación, corrupción de paquetes | Nodos de Kafka/Patroni fuera de Kubernetes, vía Ansible |
| Scripts propios | Cualquiera | kill -9, llenar disco (fallocate), desviar el reloj |
Casos que las herramientas no cubren |
Ciclo de un experimento
flowchart TD
A[Definir estado estable<br/>SLO y métricas de negocio] --> B[Formular hipótesis<br/>qué esperamos que ocurra]
B --> C[Acotar radio de explosión<br/>un pod, un mercado, 5 % tráfico]
C --> D[Ejecutar inyección<br/>Chaos Mesh / Toxiproxy / tc]
D --> E{¿Se mantiene<br/>el estado estable?}
E -- Sí --> F[Hipótesis confirmada<br/>automatizar y ampliar radio]
E -- No --> G[Abortar: detener inyección<br/>y restaurar]
G --> H[Analizar con logs y trazas<br/>07-02 · corregir diseño 07-03/07-04]
H --> B
F --> I[Registrar resultado<br/>y siguiente experimento]
La flecha de G a H es donde está el valor: cada hipótesis refutada es un incidente que no ocurrirá un sábado. Y el botón de abortar no es opcional: antes de ejecutar hay que saber exactamente cómo se detiene la inyección y cuánto tarda el sistema en volver al estado estable.
Game days
Un game day es una sesión planificada (medio día, equipo completo, con Jordi y Marta de guardia simulada) en la que se ejecutan varios experimentos seguidos y se practica también la parte humana: ¿saltó la alerta correcta de 07-01? ¿El runbook de 07-03 sirvió? ¿Cuánto tardó en localizarse la causa con las trazas de 07-02? El resultado es un postmortem sin víctimas.
- Experimentos de caos en Kilómetro Cero
La siguiente tabla es el plan del primer game day de la plataforma. Cada fila conecta un fallo real con el mecanismo de 07-03 o 07-04 que debería absorberlo.
| Experimento | Inyección | Hipótesis | Métricas a observar | Resultado esperado gracias a... |
|---|---|---|---|---|
Matar el líder de Patroni de km0_inventario |
Chaos Mesh PodChaos sobre el pod líder del statefulset |
Failover automático; escrituras de inventario fallan < 30 s; ninguna reserva se pierde ni se duplica |
patroni_master, errores 5xx de inventario, km0_stock_unidades{producto} antes/después, latencia de ReservarStock |
Patroni + etcd con fencing (07-03); reintentos con backoff en pedidos (07-04) |
Partición entre pedidos e inventario |
Toxiproxy timeout / NetworkChaos partition |
Circuit breaker abre en < 10 s; pedidos nuevos fallan rápido con mensaje claro; sagas en curso compensan; al restaurar, semiabierto → cerrado | km0_circuit_estado, km0_circuit_rechazos_total, km0_fallback_total, estados en tabla sagas, p99 de /api/v1/pedidos |
CircuitBreaker y con_timeout (07-04); saga COMPENSANDO → CANCELADA (03-05) |
| Latencia de 300 ms en Redis | Toxiproxy latency sobre el proxy de Redis |
El catálogo se degrada (p99 sube) pero no falla; la caché se salta por timeout y se lee de catalogo |
km0_peticion_duracion_segundos{servicio="catalogo"}, tasa de aciertos de caché, km0_fallback_total{operacion="redis"} |
Fallback y degradación controlada (07-04) |
| Llenar el disco de un broker de Kafka | fallocate en el volumen del broker 2 vía Ansible |
El broker sale del ISR; productores siguen escribiendo en los otros dos con acks=all; ningún evento de pedidos.eventos se pierde; alerta de disco salta |
kafka_under_replicated_partitions, kafka_isr_shrinks_total, lag de analitica, alerta KafkaDiscoLleno |
ISR y factor de replicación 3 (07-03); alertas de 07-01 |
Reloj desviado +5 min en un pod de pedidos |
Chaos Mesh TimeChaos |
Los JWT de Keycloak se rechazan como "aún no válidos" en ese pod; el /salud/listo lo saca del balanceo |
Errores 401 por pod, kube_pod_status_ready |
Sondas de 07-03 y 07-05; validación de tokens de 06-01 |
Partición entre pedidos e inventario con Toxiproxy
Toxiproxy se coloca entre pedidos y el puerto gRPC de inventario; pedidos se configura para apuntar al proxy (inventario-proxy:50051) en lugar de al servicio directo. Desde la prueba se manipula el proxy mediante su API HTTP y se observan las métricas de Prometheus que pedidos expone en /metrics (07-01).
# km0/tests/caos/particion_inventario.py
"""Experimento: partición entre pedidos e inventario.
Hipótesis: con inventario inalcanzable, el circuit breaker de pedidos abre
en menos de 10 s, las peticiones fallan rápido (< 100 ms) y, al restaurar
la red, el circuito vuelve a cerrado en menos de 60 s sin intervención.
"""
import time
import requests
from toxiproxy import Toxiproxy
from prometheus_client.parser import text_string_to_metric_families
TOXIPROXY = Toxiproxy(server_host="toxiproxy", server_port=8474)
PEDIDOS_METRICS = "http://pedidos:8000/metrics"
PEDIDOS_API = "http://pedidos:8000/api/v1/pedidos"
CIRCUITO = "inventario" # etiqueta `dependencia` de km0_circuit_estado
def metrica(nombre: str, etiquetas: dict) -> float:
"""Lee un valor concreto del endpoint /metrics de pedidos."""
texto = requests.get(PEDIDOS_METRICS, timeout=2).text
for familia in text_string_to_metric_families(texto):
if familia.name == nombre:
for muestra in familia.samples:
if all(muestra.labels.get(k) == v for k, v in etiquetas.items()):
return muestra.value
raise KeyError(f"{nombre}{etiquetas} no encontrada")
def esperar_hasta(condicion, timeout_s: float, cada_s: float = 0.5) -> float:
"""Devuelve los segundos que tardó en cumplirse la condición o lanza AssertionError."""
inicio = time.monotonic()
while time.monotonic() - inicio < timeout_s:
if condicion():
return time.monotonic() - inicio
time.sleep(cada_s)
raise AssertionError(f"la condición no se cumplió en {timeout_s} s")
def crear_pedido_de_prueba() -> tuple[int, float]:
inicio = time.monotonic()
r = requests.post(PEDIDOS_API, json={
"cliente": "caos", "lineas": [{"producto": "calabacin", "unidades": 1}]
}, headers={"X-Request-Id": "caos-particion"}, timeout=5)
return r.status_code, (time.monotonic() - inicio) * 1000
def test_particion_inventario():
proxy = TOXIPROXY.get_proxy("inventario")
# --- Estado estable: circuito cerrado y un pedido de control funciona
assert metrica("km0_circuit_estado", {"dependencia": CIRCUITO}) == 0
estado, _ = crear_pedido_de_prueba()
assert estado == 201
rechazos_antes = metrica("km0_circuit_rechazos_total", {"dependencia": CIRCUITO})
# --- Fase 1: latencia de 800 ms (por encima del timeout de 300 ms de 07-04)
proxy.add_toxic(name="lento", type="latency", attributes={"latency": 800, "jitter": 100})
try:
for _ in range(5):
crear_pedido_de_prueba() # deben agotar el timeout, no colgarse
# --- Fase 2: corte total (partición)
proxy.add_toxic(name="corte", type="timeout", attributes={"timeout": 0})
# Hipótesis 1: el circuito abre en < 10 s
t_apertura = esperar_hasta(
lambda: metrica("km0_circuit_estado", {"dependencia": CIRCUITO}) == 2,
timeout_s=10,
)
print(f"circuito abierto en {t_apertura:.1f} s")
# Hipótesis 2: con el circuito abierto se falla rápido
estado, ms = crear_pedido_de_prueba()
assert estado == 503, "con inventario caído se espera 503 explícito"
assert ms < 100, f"fallo lento ({ms:.0f} ms): el circuito no está cortocircuitando"
assert metrica("km0_circuit_rechazos_total", {"dependencia": CIRCUITO}) > rechazos_antes
finally:
# Botón de abortar: siempre se restaura la red, pase lo que pase
proxy.destroy_toxics()
# Hipótesis 3: recuperación automática (semiabierto -> cerrado) en < 60 s
t_cierre = esperar_hasta(
lambda: metrica("km0_circuit_estado", {"dependencia": CIRCUITO}) == 0,
timeout_s=60,
)
print(f"circuito cerrado de nuevo en {t_cierre:.1f} s")
estado, _ = crear_pedido_de_prueba()
assert estado == 201Puntos clave:
- El experimento empieza comprobando el estado estable (circuito cerrado, un pedido de control funciona). Si el sistema ya estaba mal antes de inyectar nada, el resultado no diría nada.
- Se inyecta en dos fases porque son fallos distintos de 07-04: la latencia ejercita
con_timeouty el presupuesto de reintentos; el corte ejercita el circuit breaker. Un corte limpio es en realidad el caso fácil; la latencia "casi aceptable" es la que agota hilos. - Las hipótesis son numéricas y observables desde fuera (
km0_circuit_estado, tiempo de respuesta), las mismas métricas que ve Grafana. Si la prueba pasa, el panel de 07-01 mostrará exactamente esa secuencia durante un incidente real. - El
finallycondestroy_toxics()es el botón de abortar. Sin él, un fallo en una aserción dejaría la partición activa. - El bloque final verifica lo que más se olvida: que el sistema vuelve solo. Un circuit breaker que abre bien pero necesita un reinicio para cerrar convierte cada fallo transitorio en un incidente.
El mismo experimento como manifiesto de Chaos Mesh
Una vez validado con Toxiproxy, el experimento se declara en Kubernetes para ejecutarlo periódicamente contra el entorno real:
# km0/k8s/caos/particion-pedidos-inventario.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: particion-pedidos-inventario
namespace: km0-prod
labels:
experimento: "07-06"
spec:
action: partition # corte bidireccional; otras: delay, loss, duplicate, corrupt
mode: one # radio de explosión: UN solo pod de pedidos, no todos
selector:
namespaces: [km0]
labelSelectors:
app: pedidos
direction: both
target:
mode: all
selector:
namespaces: [km0]
labelSelectors:
app: inventario
duration: "90s" # se restaura solo al expirar: abortar automáticomode: onelimita el daño a un único pod depedidos: el resto sigue atendiendo, así que el SLO global apenas se resiente aunque la hipótesis falle. Ampliar el radio (mode: fixed-percental 50 %) es el paso siguiente, solo después de que el experimento pequeño haya pasado varias veces.durationes el abortado automático: pase lo que pase, a los 90 segundos la partición desaparece. Para ungame dayse añade además la anotación de pausa (experiment.chaos-mesh.org/pause: "true") como parada manual.- El manifiesto se versiona en
k8s/caos/y se aplica desde el pipeline GitOps de 07-05 con unSchedulede Chaos Mesh, por ejemplo cada martes a las 11:00, en horario de oficina. Las hipótesis se comprueban con la misma clase de aserciones del guion anterior, ahora ejecutadas contra Prometheus.
Errores Comunes y Consejos
- Simular la infraestructura en lugar de usarla. Un mock de PostgreSQL o de Kafka valida la lógica del servicio contra una idea de cómo se comporta la infraestructura, no contra la infraestructura. Con Testcontainers el coste es de unos segundos por sesión; el beneficio es detectar el
deadlock, la migración olvidada o el evento mal serializado. - Tratar los tests intermitentes como ruido. Un test que falla una de cada cincuenta veces en un sistema distribuido suele estar mostrando una carrera real. Reintentarlo automáticamente hasta que pase esconde el problema; lo correcto es capturar los logs y trazas de la ejecución fallida (07-02) y reproducirla.
- Umbrales de carga sin relación con el SLO. "La prueba de carga pasó" no significa nada si el umbral era p95 < 2 s y el SLO es p99 < 500 ms. Los umbrales de k6 y los de
alertas.ymldeben salir del mismo documento. - Probar solo el corte limpio. Un servicio caído del todo es el fallo más fácil de manejar: se detecta rápido. La latencia alta pero por debajo del timeout, o los errores en el 10 % de las peticiones, son los que agotan hilos y presupuestos. Inyectar siempre las tres familias: latencia, errores y partición.
- Caos sin hipótesis ni botón de abortar. "Vamos a matar un nodo a ver qué pasa" no es un experimento, es un incidente autoinfligido. Sin estado estable definido no se puede saber si el resultado es bueno; sin abortado, no se puede parar cuando es malo.
- Ejecutar el experimento una sola vez. Cada versión nueva de
pedidoso cada cambio enresiliencia.pypuede deshacer lo que se verificó. Los experimentos que ya pasan se automatizan (Chaos MeshSchedule, etapa del pipeline) para que sigan pasando. - Contratos escritos por el productor. Si
inventariodocumenta su API pero nadie verifica qué usa realmentepedidos, un campo "que nadie usa" puede eliminarse y romper la saga. El contrato lo declara el consumidor y lo verifica el productor. - Confundir el radio de explosión con la duración. Un experimento corto sobre todos los pods puede hacer más daño que uno largo sobre un pod. El radio se acota primero por alcance (un pod, un mercado, un porcentaje de tráfico) y después por tiempo.
Ejercicios
Ejercicio 1: prueba de integración de la idempotencia
En 02-05 se diseñó el consumidor idempotente de analitica con la tabla mensajes_procesados. Escribe, con la misma estructura de fixtures que tests/test_saga_pedido.py, una prueba de integración que publique dos veces el mismo evento PedidoCompletado (mismo id_evento) en pedidos.eventos y verifique que km0_analitica contabiliza una sola venta. Indica qué se usa real y qué se sustituye por un doble, y por qué.
Ejercicio 2: umbrales y perfil de carga para la Semana del Queso Artesano
La Semana del Queso Artesano multiplica el tráfico por cuatro (no por ocho) pero concentra las compras en dos horas por la tarde, con un pico muy abrupto. Adapta tests/carga/vendimia.js: modifica stages para representar ese perfil, ajusta la mezcla de productos y añade un umbral para que la prueba falle si el HPA de 07-05 no ha escalado pedidos a al menos 4 réplicas durante la meseta (pista: k6 puede consultar un endpoint HTTP; supón que existe GET /internal/replicas en staging).
Ejercicio 3: diseñar un experimento de caos
Diseña, en formato de la tabla del apartado 9, un experimento para el siguiente fallo real: el consumidor de reparto.posiciones en reparto se queda atascado (el proceso vive pero no consume mensajes, por ejemplo por un bloqueo interno). Define estado estable, inyección (¿con qué herramienta?), hipótesis, métricas a observar y qué mecanismo de 07-01 a 07-05 debería absorberlo. Indica también el radio de explosión y el botón de abortar.
Soluciones
Solución 1. Se usan reales PostgreSQL (la tabla mensajes_procesados y la de ventas son el núcleo de lo que se prueba) y Kafka (la reentrega es precisamente lo que hay que reproducir); se sustituye por un doble cualquier servicio externo al que analitica llame (ninguno, en este caso). Esqueleto:
def test_evento_duplicado_se_cuenta_una_vez(conexion, kafka):
productor = KafkaProducer(bootstrap_servers=kafka.get_bootstrap_server())
evento = envolver(tipo="PedidoCompletado", origen="pedidos",
datos={"pedido_id": "P-2026-000126", "importe": 42.0})
for _ in range(2): # mismo id_evento las dos veces
productor.send("pedidos.eventos", json.dumps(evento).encode())
productor.flush()
consumidor = ConsumidorAnalitica(conexion, bootstrap=kafka.get_bootstrap_server())
consumidor.procesar_hasta_vacio(timeout_s=5)
ventas = conexion.execute("SELECT count(*) FROM ventas WHERE pedido_id = %s",
("P-2026-000126",)).fetchone()[0]
procesados = conexion.execute("SELECT count(*) FROM mensajes_procesados WHERE id_evento = %s",
(evento["id_evento"],)).fetchone()[0]
assert ventas == 1 and procesados == 1La aserción sobre mensajes_procesados comprueba el mecanismo, y la de ventas el efecto de negocio; ambas hacen falta, porque un consumidor podría insertar en mensajes_procesados y aun así duplicar la venta si no lo hace en la misma transacción.
Solución 2. Perfil con pico abrupto y meseta corta, umbral de réplicas comprobado dentro de la iteración con una métrica Gauge:
import { Gauge } from 'k6/metrics';
const replicas = new Gauge('km0_pedidos_replicas');
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '1m', target: 200 }, // pico abrupto: x4 en un minuto
{ duration: '10m', target: 200 },
{ duration: '2m', target: 0 },
],
thresholds: {
'km0_pedidos_error_rate': ['rate<0.001'],
'km0_crear_pedido_ms': ['p(99)<500'],
'km0_pedidos_replicas': ['value>=4'], // se evalúa sobre el último valor
},
};
const productos = ['queso-curado', 'queso-curado', 'queso-fresco', 'queso-fresco', 'tomate-rosa'];
// Dentro de default(), una de cada 50 iteraciones:
// if (__ITER % 50 === 0) replicas.add(http.get(`${BASE}/internal/replicas`).json('pedidos'));La rampa de un minuto es la parte interesante: el HPA de 07-05 reacciona con retardo (ventana de estabilización) y es probable que el p99 se incumpla durante los primeros dos minutos del pico. Eso no es un fallo de la prueba, es un hallazgo: o se precalienta pedidos antes de la campaña (réplicas mínimas más altas ese día), o se ajusta el HPA.
Solución 3. Estado estable: reparto.panel recibe posiciones actualizadas de furgoneta-3 cada 10 s y el lag del grupo de consumidores de reparto.posiciones es < 50 mensajes. Inyección: Chaos Mesh StressChaos no sirve (el proceso no está saturado, está bloqueado); la forma más fiel es un feature flag o variable de entorno de prueba que hace que el consumidor deje de hacer poll sin morir, o kill -STOP al proceso (lo congela sin cerrar la conexión). Radio: un solo pod del consumidor, mode: one. Hipótesis: el pod sigue pasando /salud/vivo (el proceso vive) pero debe fallar /salud/listo en menos de 30 s porque la sonda de listo comprueba la antigüedad del último mensaje consumido (07-03); Kubernetes lo reinicia o lo saca del grupo, Kafka reasigna la partición (rebalance) y el lag vuelve a bajar; la alerta de lag de 07-01 salta si dura más de dos minutos. Métricas: kafka_consumergroup_lag{group="reparto"}, kube_pod_status_ready, antigüedad de la última posición en reparto.panel. Abortar: kill -CONT o eliminar el flag; con duration de Chaos Mesh si se usa PodChaos. Si la hipótesis falla (el pod sigue "listo" mientras no consume), la corrección es de 07-03: la sonda de listo debe medir progreso, no solo que el proceso responda.
Conclusión
Esta lección ha completado la respuesta a la pregunta con la que terminó 07-05. La confianza en que la versión 1.15.0 funciona no viene de una sola prueba, sino de una pirámide adaptada a lo distribuido: unitarias para la lógica, integración con dependencias reales en contenedores para la saga y la outbox, contratos para que pedidos e inventario sigan entendiéndose sin desplegarlos juntos, carga con umbrales copiados del SLO para la Semana de la Vendimia, resiliencia con fallos inyectados para que los patrones de 07-04 se ejecuten alguna vez antes de producción, y técnicas de producción (canary, feature flags, tráfico sombra, sondas sintéticas) para lo que solo existe allí. Encima de todo ello, la ingeniería del caos convierte los fallos de 07-03 en experimentos con hipótesis, radio de explosión acotado y botón de abortar, y los game days entrenan también a las personas.
Con ella se cierra el Módulo 7, cuyo hilo ha sido una sola pregunta desdoblada en cinco: cómo sabemos que la plataforma funciona, y qué hacemos cuando no.
| Capa | Pregunta que responde | Mecanismos principales | Lección |
|---|---|---|---|
| Observabilidad | ¿Qué está pasando ahora y se cumple el SLO? | Métricas Prometheus, alertas por burn rate, sondas sintéticas, Grafana; logs JSON y trazas OpenTelemetry con trace_id |
07-01, 07-02 |
| Recuperación | Cuando algo cae, ¿cómo se detecta y se vuelve? | Health checks, DetectorPhi, failover con fencing (Patroni, ISR), checkpoints, backups PITR, RPO/RTO, runbooks y postmortems |
07-03 |
| Resiliencia | ¿Cómo evitamos que un fallo parcial se convierta en total? | Timeouts, reintentos con presupuesto, CircuitBreaker, Bulkhead, fallback y degradación, load shedding |
07-04 |
| Automatización | ¿Cómo se despliega y escala sin intervención manual ni errores humanos? | Ansible, imágenes inmutables, Kubernetes con sondas y HPA, canary contra SLO, service mesh, GitOps | 07-05 |
| Pruebas y caos | ¿Cómo demostramos todo lo anterior antes de que ocurra? | Testcontainers, contratos, k6 con umbrales de SLO, Toxiproxy, Chaos Mesh, game days | 07-06 |
Las cinco capas se apoyan entre sí: las pruebas de carga se evalúan con las métricas de 07-01, los experimentos de caos verifican los mecanismos de 07-03 y 07-04, y la automatización de 07-05 es lo que permite que todo ello se ejecute en cada versión y no solo una vez.
Con este módulo, Kilómetro Cero dispone ya de todas las piezas: comunicación, consistencia, almacenamiento, computación, seguridad y operación. Hasta ahora cada lección ha mirado una pieza; el Módulo 8 cambia la escala y mira la plataforma entera como arquitectura, a través de casos de estudio. La primera pregunta es la que subyace desde el Módulo 1, cuando el monolito Python+PostgreSQL se partió en catalogo, pedidos, inventario, pagos, reparto y analitica: qué hace que una arquitectura de microservicios funcione como sistema y no como una colección de servicios, dónde se cortan las fronteras, cómo se comparten (o no) los datos y qué precio se paga por cada decisión. Con eso empieza el último módulo.
Curso de Arquitecturas Distribuidas
Módulo 1: Introducción a los Sistemas Distribuidos
- Conceptos Básicos de Sistemas Distribuidos
- Modelos de Sistemas Distribuidos
- Ventajas y Desafíos de los Sistemas Distribuidos
- Las Falacias de la Computación Distribuida
- Tiempo, Relojes y Ordenación de Eventos
- Del Monolito a la Plataforma Distribuida: el Caso Kilómetro Cero
Módulo 2: Comunicación en Sistemas Distribuidos
- Protocolos de Comunicación
- RPC y RMI
- gRPC y Serialización de Datos
- Mensajería y Colas de Mensajes
- Patrones de Comunicación Asíncrona
Módulo 3: Consistencia y Replicación
- Modelos de Consistencia
- El Teorema CAP y PACELC
- Algoritmos de Consenso
- Replicación de Datos
- Transacciones Distribuidas y Sagas
Módulo 4: Almacenamiento Distribuido
- Particionado de Datos y Hashing Consistente
- Sistemas de Archivos Distribuidos
- Almacenamiento de Objetos
- Bases de Datos Distribuidas
- Cachés Distribuidos
Módulo 5: Computación Distribuida
- Modelos de Computación Distribuida
- MapReduce y Hadoop
- Spark y Computación en Memoria
- Procesamiento de Flujos de Datos
- Planificación de Trabajos y Pipelines de Datos
Módulo 6: Seguridad en Sistemas Distribuidos
- Autenticación y Autorización
- Cifrado y Protección de Datos
- Gestión de Identidades
- Seguridad entre Servicios: mTLS y Gestión de Secretos
- Puertas de Enlace, Limitación de Tasa y Auditoría
Módulo 7: Monitoreo y Mantenimiento
- Monitoreo de Sistemas Distribuidos
- Logs Centralizados y Trazabilidad Distribuida
- Gestión de Fallos y Recuperación
- Patrones de Resiliencia: Timeouts, Reintentos y Circuit Breaker
- Automatización y Orquestación
- Pruebas en Sistemas Distribuidos e Ingeniería del Caos
