En las cuatro lecciones anteriores hemos escrito, sin justificarlo, direcciones como http://servicio-catalogo:3001, servicio-inventario:50051 o amqp://rabbitmq:5672. Han funcionado como nombres estables en los ejemplos, pero en un sistema real cada uno de esos nombres esconde un problema: detrás de "servicio-catalogo" habrá dos réplicas un martes por la mañana y ocho el Black Friday, cada una en un contenedor con una IP que se asigna al arrancar y se destruye al morir. ¿A cuál de esas IPs llama Pedidos cuando necesita GET /productos?ids=? ¿Quién sabe cuáles están vivas ahora mismo? ¿Cómo se reparte el trabajo entre ellas? Esta lección responde a esas tres preguntas.
Veremos por qué no se pueden cablear direcciones (las falacias de 01-02 vuelven), el registro de servicios (quién anota que una instancia existe: ella misma o un tercero), el descubrimiento del lado del cliente frente al del lado del servidor, las herramientas clásicas (Consul, Eureka) con un fragmento real de registro con health check, el descubrimiento nativo de Kubernetes por DNS que TechCorp adoptará, los algoritmos y capas del balanceo de carga y dónde ocurre en cada modelo, los health checks (liveness frente a readiness, con ambos endpoints en Express) como parte del contrato de un servicio, y la decisión de TechCorp: DNS de Kubernetes + nombres estables servicio-* como configuración. Desplegar de verdad en Kubernetes es de 05-02, el service mesh de 05-05 y la resiliencia (qué hacer cuando la instancia elegida falla) de 06-03.
Contenido
- El problema: instancias efímeras con IPs dinámicas
- Registro de servicios: quién anota que una instancia existe
- Descubrimiento del lado del cliente
- Descubrimiento del lado del servidor
- Comparativa de los dos modelos
- Herramientas: Consul y Eureka
- Descubrimiento nativo de Kubernetes:
Service,Endpointsy DNS - Balanceo de carga: algoritmos y capas
- Dónde ocurre el balanceo en cada modelo
- Health checks: liveness frente a readiness
- La decisión de TechCorp
- El problema: instancias efímeras con IPs dinámicas
En el monolito, la web llamaba a techcorp-shop:3000 y ese nombre apuntaba a una máquina fija con una IP fija. En microservicios sobre contenedores, la situación cambia en tres sentidos:
- Muchas instancias por servicio. Catálogo tiene N réplicas idénticas y N cambia con la carga (los picos ×20 de 01-05) o con un despliegue (05-04).
- Direcciones efímeras. Cada contenedor recibe una IP del rango interno al arrancar (
10.244.3.17) y esa IP desaparece con él. Reiniciar es cambiar de dirección. - Fallos parciales. Una réplica puede estar viva pero sin conexión a su base de datos, o arrancando y aún sin poder atender, o a punto de apagarse.
Cablear direcciones (CATALOGO_URL=http://10.244.3.17:3001) falla por las falacias de la computación distribuida de 01-02: "la topología no cambia" (cambia cada minuto), "la red es fiable" (no lo es), "hay un solo administrador" (Kubernetes reprograma contenedores sin preguntar). Y una lista de IPs en configuración se desactualiza antes de desplegarse.
Necesitamos tres cosas: un lugar donde se registre qué instancias existen y dónde (registro de servicios), un mecanismo para que un llamante encuentre una instancia sana en el momento de llamar (descubrimiento), y una forma de repartir las llamadas entre las instancias disponibles (balanceo de carga).
- Registro de servicios: quién anota que una instancia existe
El registro es una base de datos de "servicio → lista de instancias (IP, puerto, estado)". Hay dos maneras de rellenarlo:
| Modelo | Quién registra | Cómo se detecta la caída | Ventajas | Inconvenientes |
|---|---|---|---|---|
| Self-registration (autorregistro) | La propia instancia, al arrancar, llama al registro ("soy servicio-catalogo, estoy en 10.244.3.17:3001") y al apagarse se da de baja |
La instancia envía latidos (heartbeats) periódicos; si dejan de llegar durante un tiempo, el registro la marca como caída | Sencillo de entender; no requiere infraestructura que "mire" el servicio | Cada servicio necesita código de registro (acoplamiento a la herramienta); si el proceso muere de golpe no se da de baja y hay que esperar al timeout de latidos |
| Third-party registration (registro por terceros) | Un componente externo (el orquestador, un agente) observa qué instancias arrancan y mueren y actualiza el registro | El tercero hace comprobaciones de salud a la instancia | El servicio no sabe que existe un registro; sin código de infraestructura en el negocio | Requiere que ese tercero exista y esté integrado con la plataforma |
Consul y Eureka se usan tradicionalmente en modo self-registration (con clientes en el servicio) o con un agente local; Kubernetes es third-party puro: el propio orquestador sabe qué contenedores hay porque él los creó.
- Descubrimiento del lado del cliente
En el descubrimiento del lado del cliente (client-side discovery), es el llamante quien consulta el registro, obtiene la lista de instancias sanas, elige una (aplicando un algoritmo de balanceo) y la llama directamente.
sequenceDiagram
participant P as servicio-pedidos
participant R as Registro (Consul/Eureka)
participant C1 as catalogo 10.244.3.17 (3001)
participant C2 as catalogo 10.244.5.42 (3001)
C1->>R: registro + heartbeats
C2->>R: registro + heartbeats
P->>R: ¿instancias de servicio-catalogo?
R-->>P: [10.244.3.17:3001, 10.244.5.42:3001]
Note over P: elige una (round robin) y cachea la lista unos segundos
P->>C2: GET /productos?ids=p-501,p-777
C2-->>P: 200 OK
Consecuencias: el cliente necesita una librería que hable con el registro y balancee (Netflix Ribbon con Eureka fue el ejemplo clásico); esa librería debe existir para cada lenguaje del sistema; el balanceo puede ser muy inteligente (el cliente conoce sus latencias); y no hay un salto de red extra. Pero cada servicio lleva dentro lógica de infraestructura, y actualizar esa lógica es redesplegar todos los servicios.
- Descubrimiento del lado del servidor
En el descubrimiento del lado del servidor (server-side discovery), el llamante llama siempre a una dirección estable (un balanceador, un nombre DNS virtual, un proxy) y es esa pieza intermedia quien consulta el registro y reenvía la petición a una instancia sana.
sequenceDiagram
participant P as servicio-pedidos
participant LB as Dirección estable (Service / balanceador)
participant R as Registro (Endpoints)
participant C1 as catalogo 10.244.3.17 (3001)
participant C2 as catalogo 10.244.5.42 (3001)
Note over R: el orquestador actualiza la lista al crear/destruir instancias
P->>LB: GET http://servicio-catalogo:3001/productos?ids=...
LB->>R: instancias sanas de servicio-catalogo
R-->>LB: [10.244.3.17, 10.244.5.42]
LB->>C1: reenvío
C1-->>LB: 200 OK
LB-->>P: 200 OK
Consecuencias: el cliente es trivial (un fetch a un nombre fijo, como los de 03-01); la lógica de descubrimiento vive en la plataforma y se actualiza sin tocar servicios; funciona igual para Node.js, Go o Java. A cambio, hay una pieza más en el camino (que debe ser altamente disponible) y, en algunos casos, un salto de red extra.
- Comparativa de los dos modelos
| Criterio | Lado del cliente | Lado del servidor |
|---|---|---|
| Quién consulta el registro | El servicio llamante | Un intermediario (balanceador, proxy, DNS+kube-proxy) |
| Código en el servicio | Librería de descubrimiento y balanceo | Ninguno: llama a un nombre |
| Políglota | Una librería por lenguaje | Independiente del lenguaje |
| Salto de red adicional | No | A veces (depende de la implementación) |
| Inteligencia del balanceo | Alta (el cliente conoce latencias, puede hacer hedging) | Media (el intermediario ve todo el tráfico, no la experiencia de cada cliente) |
| Punto de fallo | El registro (pero el cliente cachea) | El intermediario (debe replicarse) |
| Ejemplos | Eureka + Ribbon, Consul + librería cliente | AWS ELB/ALB, Kubernetes Service, NGINX/Traefik, service mesh |
| Encaje para TechCorp | Añadiría código de infraestructura a seis servicios Node.js | Kubernetes lo da de serie |
La tendencia de la industria es clara: con orquestadores y service meshes, el descubrimiento del lado del servidor ha ganado porque saca la infraestructura del código de negocio. Es coherente con "smart endpoints, dumb pipes" de 02-01: el servicio piensa en pedidos, no en IPs.
- Herramientas: Consul y Eureka
Aunque TechCorp usará el mecanismo de Kubernetes, conviene conocer las dos herramientas clásicas porque aparecen en muchos sistemas existentes y porque explican de dónde vienen los conceptos.
- Netflix Eureka. Registro REST creado por Netflix para AWS. Las instancias se registran con
POST /eureka/apps/{app}y envían un latido cada 30 s conPUT; si faltan tres latidos, se marca como caída. Los clientes descargan el registro completo y lo cachean. Muy ligado al ecosistema Spring Cloud (Java); en Node.js existen clientes, pero es raro elegirlo hoy fuera de Java. - HashiCorp Consul. Registro + almacén clave-valor + health checks + DNS. Cada nodo corre un agente local; los servicios se registran contra su agente (por API HTTP o por fichero), y Consul ejecuta las comprobaciones de salud (HTTP, TCP, script) desde el agente. Expone el registro por API y por DNS (
servicio-catalogo.service.consulresuelve a las instancias sanas), lo que ya es una forma de descubrimiento del lado del servidor. Es la opción habitual fuera de Kubernetes (máquinas virtuales, Nomad) y sigue siendo relevante.
Fragmento con el cliente consul de Node.js registrando servicio-catalogo con un health check HTTP:
// infraestructura/registroConsul.js (solo se usaría si TechCorp NO estuviera en Kubernetes)
const Consul = require('consul');
const os = require('node:os');
async function registrarEnConsul({ nombre = 'servicio-catalogo', puerto = 3001 }) {
// 1. Cliente contra el agente local (cada nodo tiene el suyo en 8500)
const consul = new Consul({ host: process.env.CONSUL_HOST ?? '127.0.0.1', port: 8500 });
// 2. Id único por instancia: nombre + host + puerto. Dos réplicas → dos ids
const instanciaId = `${nombre}-${os.hostname()}-${puerto}`;
const direccion = process.env.IP_INSTANCIA ?? obtenerIpLocal();
// 3. Registro con health check: Consul llamará a GET /health/ready cada 10 s;
// si falla 30 s seguidos, la instancia se da de baja sola (DeregisterCriticalServiceAfter)
await consul.agent.service.register({
id: instanciaId,
name: nombre,
address: direccion,
port: puerto,
tags: ['http', 'v1'],
check: {
http: `http://${direccion}:${puerto}/health/ready`,
interval: '10s',
timeout: '2s',
deregistercriticalserviceafter: '30s'
}
});
console.log(`Registrado en Consul como ${instanciaId}`);
// 4. Baja ordenada al apagar: sin esto, la instancia figura "crítica" hasta que expire el check
const darDeBaja = async () => { await consul.agent.service.deregister(instanciaId); process.exit(0); };
process.on('SIGTERM', darDeBaja);
process.on('SIGINT', darDeBaja);
}
// Cliente: obtener instancias sanas y elegir una (descubrimiento del lado del cliente)
async function resolverInstancia(consul, nombre) {
const [instancias] = await consul.health.service({ service: nombre, passing: true }); // solo las sanas
if (instancias.length === 0) throw new Error(`Sin instancias sanas de ${nombre}`);
const elegida = instancias[Math.floor(Math.random() * instancias.length)]; // balanceo aleatorio simple
return `http://${elegida.Service.Address}:${elegida.Service.Port}`;
}Fíjate en todo lo que este código añade al servicio de Catálogo que no tiene nada que ver con productos: ids de instancia, IPs, latidos, bajas. Es el coste del self-registration y del descubrimiento del lado del cliente. Con Kubernetes, nada de esto existe en el código del servicio.
- Descubrimiento nativo de Kubernetes:
Service, Endpoints y DNS
Service, Endpoints y DNSKubernetes trae registro, descubrimiento y balanceo integrados. Tres objetos bastan para entenderlo (el YAML completo de despliegue es de 05-02; aquí solo el mecanismo):
- Pod. La unidad que ejecuta un contenedor (una réplica de Catálogo). Tiene una IP efímera del clúster (
10.244.3.17). - Service. Un objeto con un nombre estable (
servicio-catalogo) y una IP virtual estable (ClusterIP, p. ej.10.96.12.5) que selecciona pods por etiquetas (app: servicio-catalogo). Vive aunque los pods mueran y nazcan. - Endpoints (o EndpointSlice en versiones actuales). La lista, mantenida automáticamente por Kubernetes, de las IPs de los pods que el
Serviceselecciona y que están listos (readiness, apartado 10). Es el registro, rellenado por un tercero (el orquestador): third-party registration sin escribir una línea.
El flujo cuando Pedidos hace fetch('http://servicio-catalogo:3001/productos?ids=...'):
flowchart LR
P[pod servicio-pedidos] -- "1. resolver servicio-catalogo" --> DNS[CoreDNS del clúster]
DNS -- "2. 10.96.12.5 (ClusterIP)" --> P
P -- "3. TCP a 10.96.12.5:3001" --> KP[kube-proxy / iptables del nodo]
KP -- "4. consulta Endpoints" --> EP[(Endpoints de servicio-catalogo: 10.244.3.17, 10.244.5.42, ...)]
KP -- "5. reescribe destino a un pod" --> C2[pod catalogo 10.244.5.42:3001]
C2 --> P
- El pod de Pedidos resuelve el nombre
servicio-catalogoen CoreDNS, el DNS interno del clúster. El nombre completo esservicio-catalogo.<namespace>.svc.cluster.local, pero desde el mismo namespace basta el nombre corto. - CoreDNS devuelve la ClusterIP del
Service, que no cambia nunca mientras elServiceexista. - Pedidos abre una conexión a esa IP virtual y al puerto 3001.
- En cada nodo, kube-proxy mantiene reglas de red (iptables o IPVS) que interceptan el tráfico a las ClusterIP.
- Esas reglas eligen uno de los pods de la lista de Endpoints y reescriben el destino a su IP real. Si un pod deja de estar listo, sale de Endpoints y deja de recibir tráfico en segundos.
Lo importante para el desarrollador: el código de Pedidos no sabe nada de esto. Llama a un nombre y un puerto fijos, exactamente el CATALOGO_URL de 03-01. Registro, salud y balanceo son de la plataforma. Es descubrimiento del lado del servidor y registro por terceros, gratis.
Fragmento del Service de Catálogo (solo para ver de dónde salen el nombre y el puerto; el Deployment que crea los pods es de 05-02):
apiVersion: v1
kind: Service
metadata:
name: servicio-catalogo # → nombre DNS estable
spec:
selector:
app: servicio-catalogo # pods con esta etiqueta forman los Endpoints
ports:
- port: 3001 # puerto del Service (el que usan los llamantes)
targetPort: 3001 # puerto del contenedorUn matiz para RabbitMQ y otras conexiones largas: el balanceo de kube-proxy ocurre al establecer la conexión TCP. Una conexión AMQP abierta durante horas se queda con el mismo pod; para HTTP con keep-alive pasa parecido (todas las peticiones de una conexión van al mismo pod). Para HTTP funciona bien porque las conexiones se renuevan; para repartir de verdad por petición hace falta balanceo de capa 7 (apartado 8), que es una de las cosas que aporta el service mesh de 05-05.
- Balanceo de carga: algoritmos y capas
Algoritmos de reparto entre N instancias:
| Algoritmo | Cómo reparte | Cuándo conviene | Limitación |
|---|---|---|---|
| Round robin | Una a cada instancia, por turnos | Instancias iguales y peticiones parecidas (el caso de Catálogo) | Ignora que una instancia esté más cargada |
| Round robin ponderado | Turnos proporcionales a un peso | Instancias con distinta capacidad; despliegues canary (05-04): 95 % a la versión vieja, 5 % a la nueva | Hay que mantener los pesos |
| Least connections (menos conexiones) | A la instancia con menos conexiones activas | Peticiones de duración muy variable (informes largos junto a consultas cortas) | Requiere que el balanceador conozca las conexiones activas |
| Hash consistente | Una función de la petición (IP, id de cliente, id de pedido) decide siempre la misma instancia | Cachés locales por instancia; afinidad de sesión; repartir consumidores por clave | Reparto desigual si las claves lo son; reasignaciones al cambiar N |
| Aleatorio | Al azar | Con muchas instancias se aproxima a round robin sin estado | Rachas |
| Latencia / carga | A la instancia que responde más rápido | Instancias heterogéneas o en zonas distintas | Necesita medir; riesgo de "manada" hacia la más rápida |
Capas en las que se balancea:
| Capa 4 (transporte, TCP/UDP) | Capa 7 (aplicación, HTTP) | |
|---|---|---|
| Qué ve | IP y puerto; decide por conexión | Método, ruta, cabeceras, cuerpo; decide por petición |
| Velocidad | Muy alta (no parsea) | Alta, pero parsea HTTP |
| Capacidades | Reparto simple, sin entender la petición | Enrutar por ruta (/api/pedidos → Pedidos), por cabecera (canary), reintentos, timeouts por petición, terminación TLS |
| Ejemplos | kube-proxy, HAProxy en modo TCP, balanceadores de red en la nube | NGINX, Traefik, Envoy (Istio), balanceadores de aplicación en la nube, el gateway de 03-04 |
kube-proxy es capa 4: reparte conexiones, no peticiones. El gateway de 03-04 es capa 7: enruta por ruta y cabecera. Un service mesh pone un proxy de capa 7 (Envoy) junto a cada pod y da balanceo por petición, reintentos y métricas entre servicios internos sin tocar el código: por eso aparece en 05-05 como evolución natural.
- Dónde ocurre el balanceo en cada modelo
| Modelo | Quién balancea | Algoritmos típicos | En TechCorp |
|---|---|---|---|
| Descubrimiento del lado del cliente | La librería del cliente (Ribbon, cliente Consul) | Round robin, aleatorio, por latencia | No (evitamos código de infraestructura) |
| Balanceador dedicado | NGINX/HAProxy/ALB delante de las instancias | Todos; capa 4 o 7 | El balanceador externo delante de las réplicas del gateway 8080 |
Kubernetes Service |
kube-proxy en cada nodo | Aleatorio/round robin por conexión (capa 4) | Sí: entre servicios internos y del gateway a los servicios |
| Ingress / gateway | Traefik, NGINX Ingress | Capa 7: por ruta, cabecera, peso | Sí: el gateway de 03-04 como Ingress (05-02) |
| Service mesh | Sidecar Envoy junto a cada pod | Capa 7 por petición, least request, hash, con reintentos y mTLS | Adelanto: 05-05 |
Para las llamadas internas (Pedidos → Catálogo) TechCorp empieza con kube-proxy: suficiente para el volumen actual y sin coste. Cuando necesite balanceo por petición, reintentos declarativos o métricas por ruta entre servicios, adoptará Istio.
- Health checks: liveness frente a readiness
Nada de lo anterior sirve si el registro no sabe qué instancias están sanas. Un contenedor con el proceso vivo no es lo mismo que un servicio capaz de atender. Por eso hay dos comprobaciones distintas, y confundirlas causa incidentes:
| Liveness ("¿estoy vivo?") | Readiness ("¿estoy listo para atender?") | |
|---|---|---|
| Pregunta | ¿El proceso funciona o está colgado/bloqueado sin remedio? | ¿Puedo atender peticiones ahora? (dependencias conectadas, caché cargada, no apagándome) |
| Qué comprueba | Lo mínimo: que el bucle de eventos responde | Conexión a la BD, al broker, configuración cargada, estado de arranque/apagado |
| Si falla | El orquestador reinicia el contenedor | El orquestador lo saca del balanceo (fuera de Endpoints) pero no lo reinicia; cuando vuelve a pasar, lo reincorpora |
| Debe depender de terceros | No (si la BD cae y liveness falla, Kubernetes reiniciaría todos los pods en bucle, empeorándolo todo) | Sí, precisamente para no recibir tráfico que no puede atender |
| Endpoint en TechCorp | GET /health/live |
GET /health/ready |
Ejemplo de ambos endpoints en Express (Catálogo, con MongoDB y RabbitMQ como dependencias):
// rutas/salud.js (patrón común a todos los servicios; se empaqueta en @techcorp/comun-http)
const express = require('express');
function crearRutasSalud({ comprobaciones = {} } = {}) {
const enrutador = express.Router();
let apagando = false;
// Liveness: si Express puede ejecutar esto, el proceso está vivo. Nada más.
enrutador.get('/health/live', (_req, res) => {
res.status(200).json({ estado: 'vivo' });
});
// Readiness: cada dependencia se comprueba con un timeout corto; una que falle → 503
enrutador.get('/health/ready', async (_req, res) => {
if (apagando) {
// Durante el apagado ordenado, decimos "no listo" para que el balanceador deje de enviarnos tráfico
return res.status(503).json({ estado: 'apagando' });
}
const resultados = {};
let todoBien = true;
for (const [nombre, comprobar] of Object.entries(comprobaciones)) {
try {
await Promise.race([
comprobar(),
new Promise((_, rechazar) => setTimeout(() => rechazar(new Error('timeout')), 1000))
]);
resultados[nombre] = 'ok';
} catch (err) {
resultados[nombre] = `fallo: ${err.message}`;
todoBien = false;
}
}
res.status(todoBien ? 200 : 503).json({ estado: todoBien ? 'listo' : 'no listo', dependencias: resultados });
});
// Al recibir SIGTERM, dejamos de estar "ready" unos segundos antes de cerrar (apagado ordenado)
process.on('SIGTERM', () => { apagando = true; });
return enrutador;
}
module.exports = { crearRutasSalud };Y su uso en Catálogo:
app.use(crearRutasSalud({
comprobaciones: {
mongodb: () => clienteMongo.db().command({ ping: 1 }), // ¿responde MongoDB?
rabbitmq: () => Promise.resolve(canalAmqp && !canalAmqp.closed ? true : Promise.reject(new Error('canal cerrado')))
}
}));Respuesta de GET /health/ready con MongoDB caído:
Con código 503. Kubernetes (05-02) llamará a estos endpoints periódicamente (livenessProbe, readinessProbe), Traefik ya los usaba en el healthCheck de 03-04, y Consul en el check del apartado 6. Por eso decimos que las comprobaciones de salud son parte del contrato de un servicio: igual que POST /pedidos responde 202, todo servicio de TechCorp responde en /health/live y /health/ready con la semántica de la tabla; el equipo de Plataforma lo da por hecho al configurar la plataforma. Y dos matices más: /health/* no requiere autenticación (lo llama la infraestructura) pero no se expone por el gateway; y no debe hacer trabajo pesado (una consulta SELECT 1, un ping), porque se ejecuta cada pocos segundos por cada réplica.
- La decisión de TechCorp
- Descubrimiento: el nativo de Kubernetes. Un
Servicepor microservicio con nombre estableservicio-catalogo,servicio-pedidos,servicio-pagos,servicio-clientes,servicio-notificaciones,servicio-inventario,bff-movil, másrabbitmqy las bases de datos. Registro por terceros (Kubernetes mantiene los Endpoints), descubrimiento del lado del servidor (DNS + kube-proxy). Ningún servicio lleva código de registro. - Balanceo: el del
Service(kube-proxy, capa 4) entre servicios internos; capa 7 en el gateway/Ingress (Traefik) para el tráfico externo; service mesh como siguiente paso cuando haga falta (05-05). - Nombres estables como configuración: cada servicio recibe las direcciones de sus dependencias por variables de entorno cuyo valor es el nombre DNS del
Service. En 04-03 se verá cómo se gestionan (ConfigMaps, ficheros.env); el contrato desde hoy es este:
| Variable | Valor en el clúster | Quién la usa |
|---|---|---|
CATALOGO_URL |
http://servicio-catalogo:3001 |
Pedidos, BFF móvil, gateway |
CLIENTES_URL |
http://servicio-clientes:3004 |
Pedidos, gateway |
PEDIDOS_URL |
http://servicio-pedidos:3002 |
BFF móvil, gateway |
INVENTARIO_GRPC |
servicio-inventario:50051 |
Pedidos (si se activa gRPC, 03-03) |
RABBITMQ_URL |
amqp://rabbitmq:5672 |
Todos los que publican o consumen |
PEDIDOS_DB_URL |
postgres://...@postgres-pedidos:5432/pedidos |
Solo Pedidos (BD por servicio, 02-04) |
- Salud como contrato:
/health/livey/health/readyen todos los servicios, con la semántica del apartado 10. - En desarrollo local (Docker Compose, 05-01) los mismos nombres funcionan porque Compose también da DNS por nombre de servicio: el código no cambia entre el portátil y el clúster, solo el valor de las variables si hiciera falta.
Errores Comunes y Consejos
- Cablear IPs en configuración "solo en desarrollo". Acaban en producción. Nombres desde el primer día.
- Cachear la resolución DNS para siempre en el cliente. Node.js no cachea DNS por defecto, pero algunas librerías o pools de conexiones sí conservan la IP; si el
Servicese recrea con otra ClusterIP, el cliente sigue apuntando a la antigua. Renovar conexiones y no fijar IPs. - Liveness que comprueba la base de datos. Cae la BD → todos los pods "no vivos" → Kubernetes los reinicia en bucle → cuando la BD vuelve, nadie está arrancado. Liveness solo mira el proceso.
- Readiness que no comprueba nada. El pod entra en el balanceo antes de conectar con RabbitMQ y falla las primeras peticiones de cada despliegue.
- No pasar a "no listo" al apagar. El pod recibe peticiones hasta el último milisegundo y las corta a medias.
SIGTERM→apagando = true→ esperar unos segundos → cerrar. - Health checks caros. Un
SELECT COUNT(*) FROM pedidoscada 5 s por réplica es una carga inventada.SELECT 1oping. - Exponer
/healthpor el gateway. Es información interna y una superficie de ataque más. Solo dentro del clúster. - Confiar en que kube-proxy reparte por petición. Reparte conexiones. Con keep-alive, un cliente muy activo puede cargar siempre el mismo pod. Si el reparto importa, capa 7 (mesh).
- Añadir un cliente Consul "por si acaso" cuando ya estás en Kubernetes. Es duplicar el registro y llenar el servicio de código de infraestructura.
Ejercicios
Ejercicio 1. El servicio de Pedidos tiene tres dependencias: PostgreSQL (pedidos), RabbitMQ y el servicio de Catálogo (síncrono, vía CATALOGO_URL). Decide, justificando cada caso, cuáles deben formar parte de /health/ready de Pedidos y cuáles no, y escribe la llamada a crearRutasSalud resultante. Pista: piensa qué pasaría con las réplicas de Pedidos si Catálogo cayera y estuviera en la comprobación.
Ejercicio 2. Explica paso a paso qué ocurre en Kubernetes cuando una réplica de Catálogo empieza a fallar su readinessProbe porque MongoDB va lento, y qué diferencia habría si en vez de la readiness fallara la liveness. Indica en qué momento Pedidos deja de recibir errores y por qué el código de Pedidos no cambia en ningún caso.
Ejercicio 3. El equipo de Plataforma propone que las réplicas de Notificaciones usen hash consistente por clienteId para que todos los correos de un mismo cliente los envíe la misma réplica y así reutilizar una conexión SMTP por cliente. Notificaciones consume eventos de RabbitMQ (03-02), no recibe HTTP. Razona si el balanceo por hash tiene sentido aquí, qué mecanismo real de RabbitMQ (o de Kubernetes) lo permitiría o no, y qué alternativa recomendarías.
Soluciones
Solución 1.
- PostgreSQL de Pedidos: sí. Sin su base de datos, Pedidos no puede atender ninguna petición (ni crear ni consultar pedidos). Mejor sacarlo del balanceo que devolver
500a todo. - RabbitMQ: sí, con matiz. Pedidos publica vía outbox (02-05):
POST /pedidosguarda pedido y evento en PostgreSQL y el relay publica después. Estrictamente, podría aceptar pedidos sin RabbitMQ; pero también consumepedidos.sagay sin él la saga no avanza. TechCorp lo incluye: un pedido aceptado que no puede avanzar es peor experiencia que un503breve. (Argumentar lo contrario es válido si se prioriza aceptar pedidos.) - Catálogo: no. Es una dependencia de otro servicio. Si Catálogo cae y estuviera en la readiness, todas las réplicas de Pedidos saldrían del balanceo a la vez y
GET /pedidos/{id}(que no necesita Catálogo) dejaría de funcionar: un fallo se convertiría en dos. Pedidos debe seguir listo y responder503 DEPENDENCIA_NO_DISPONIBLEsolo en las operaciones que necesitan Catálogo (03-01). Regla: la readiness incluye lo que este servicio necesita para funcionar en absoluto, no otros servicios (eso es cascada de fallos, tema de 06-03).
app.use(crearRutasSalud({
comprobaciones: {
postgres: () => pool.query('SELECT 1'),
rabbitmq: () => (canalAmqp && !canalAmqp.closed) ? Promise.resolve() : Promise.reject(new Error('canal cerrado'))
}
}));Solución 2.
Readiness fallando: (1) el kubelet del nodo llama a GET /health/ready cada N segundos y recibe 503 (la comprobación de MongoDB supera el timeout de 1 s); (2) tras failureThreshold fallos consecutivos (por defecto 3), Kubernetes marca el pod como not ready; (3) el controlador de Endpoints quita la IP de ese pod de los Endpoints de servicio-catalogo; (4) kube-proxy actualiza las reglas en todos los nodos y las nuevas conexiones van solo a las réplicas listas; (5) el pod sigue vivo, sigue intentando; cuando MongoDB se recupera y /health/ready da 200 (successThreshold, por defecto 1), vuelve a Endpoints. Pedidos deja de ver errores en el paso 4, unos segundos después del primer fallo, y antes solo veía errores en la fracción de peticiones que kube-proxy mandaba a ese pod.
Liveness fallando: Kubernetes mata y reinicia el contenedor. Si el problema es MongoDB lento, el reinicio no arregla nada y, si la liveness comprobara MongoDB, todas las réplicas se reiniciarían en bucle: por eso la liveness no toca dependencias.
El código de Pedidos no cambia porque llama a http://servicio-catalogo:3001, un nombre y una ClusterIP estables; quién hay detrás y si está listo lo gestiona la plataforma. Lo único que Pedidos ve son menos errores.
Solución 3.
El balanceo por hash es un concepto de llamadas entrantes (HTTP/gRPC): quien decide es un balanceador que ve la petición y elige la instancia. Notificaciones no recibe llamadas: sus réplicas compiten por mensajes de la cola notificaciones.pedidos (03-02), y RabbitMQ entrega cada mensaje a la réplica que tenga hueco en su prefetch; kube-proxy no interviene (las conexiones AMQP son de la réplica al broker, no al revés) y no puede saber nada de clienteId. Por tanto, el hash consistente por clienteId no aplica con la topología actual.
Alternativas: (a) en RabbitMQ existe el plugin consistent-hash exchange, que reparte a varias colas por hash de la routing key o de una cabecera; habría que crear una cola por réplica y perder la simetría de "una cola por servicio"; complejo y frágil al escalar. (b) Kafka resolvería esto de forma natural (partición por clave), pero cambiar de broker por una optimización SMTP no compensa. (c) La recomendación: no hacerlo. Un pool de conexiones SMTP en cada réplica (reutilizado entre clientes) da el mismo ahorro sin afinidad, y el orden entre correos de un cliente lo garantiza mejor el evento (que lleva todo lo necesario) que la topología.
Conclusión
El problema era claro: instancias efímeras con IPs dinámicas y un llamante que necesita encontrar una sana. La solución tiene tres piezas: un registro (rellenado por la propia instancia con latidos, o por un tercero como el orquestador), un mecanismo de descubrimiento (en el cliente, con una librería, o en el servidor, con una dirección estable que resuelve la plataforma) y un balanceo (round robin, ponderado, least connections, hash; en capa 4 por conexión o en capa 7 por petición). Hemos visto Consul y Eureka como referencia y hemos entendido el mecanismo nativo de Kubernetes: Service con nombre e IP estables, Endpoints como registro automático, CoreDNS para resolver servicio-catalogo y kube-proxy para repartir; y hemos fijado los health checks (/health/live para "estoy vivo", /health/ready para "puedo atender") como parte del contrato de todo servicio de TechCorp. La decisión: DNS de Kubernetes, balanceo del Service, nombres estables servicio-* como valores de configuración (CATALOGO_URL=http://servicio-catalogo:3001), y cero código de infraestructura en los servicios.
Con esto, los servicios saben hablar (REST, eventos, gRPC/GraphQL cuando toque), tienen una puerta (el gateway) y se encuentran entre sí. Queda la última pregunta del módulo, y quizá la que más incidentes evita a largo plazo: cuando el equipo de Catálogo cambia la forma de GET /productos o el de Pedidos añade un campo a pedido.creado, ¿cómo lo hacen sin romper a nadie? Qué cambios son compatibles y cuáles no, cómo se versiona una API REST, un evento, un .proto o un esquema GraphQL, cómo se retira una versión y cómo se pacta el contrato antes de escribir código. Ese es el tema de la siguiente lección: contratos y versionado de APIs.
Curso de Microservicios
Módulo 1: Introducción a los Microservicios
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
