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

  1. El problema: instancias efímeras con IPs dinámicas
  2. Registro de servicios: quién anota que una instancia existe
  3. Descubrimiento del lado del cliente
  4. Descubrimiento del lado del servidor
  5. Comparativa de los dos modelos
  6. Herramientas: Consul y Eureka
  7. Descubrimiento nativo de Kubernetes: Service, Endpoints y DNS
  8. Balanceo de carga: algoritmos y capas
  9. Dónde ocurre el balanceo en cada modelo
  10. Health checks: liveness frente a readiness
  11. La decisión de TechCorp

  1. 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).

  1. 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ó.

  1. 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.

  1. 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.

  1. 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.

  1. 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 con PUT; 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.consul resuelve 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:

npm install consul
// 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.

  1. Descubrimiento nativo de Kubernetes: Service, Endpoints y DNS

Kubernetes 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 Service selecciona 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
  1. El pod de Pedidos resuelve el nombre servicio-catalogo en CoreDNS, el DNS interno del clúster. El nombre completo es servicio-catalogo.<namespace>.svc.cluster.local, pero desde el mismo namespace basta el nombre corto.
  2. CoreDNS devuelve la ClusterIP del Service, que no cambia nunca mientras el Service exista.
  3. Pedidos abre una conexión a esa IP virtual y al puerto 3001.
  4. En cada nodo, kube-proxy mantiene reglas de red (iptables o IPVS) que interceptan el tráfico a las ClusterIP.
  5. 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 contenedor

Un 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.

  1. 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.

  1. 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) : entre servicios internos y del gateway a los servicios
Ingress / gateway Traefik, NGINX Ingress Capa 7: por ruta, cabecera, peso : 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.

  1. 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) , 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:

{ "estado": "no listo", "dependencias": { "mongodb": "fallo: timeout", "rabbitmq": "ok" } }

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.

  1. La decisión de TechCorp

  • Descubrimiento: el nativo de Kubernetes. Un Service por microservicio con nombre estable servicio-catalogo, servicio-pedidos, servicio-pagos, servicio-clientes, servicio-notificaciones, servicio-inventario, bff-movil, más rabbitmq y 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/live y /health/ready en 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 Service se 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. SIGTERMapagando = true → esperar unos segundos → cerrar.
  • Health checks caros. Un SELECT COUNT(*) FROM pedidos cada 5 s por réplica es una carga inventada. SELECT 1 o ping.
  • Exponer /health por 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 500 a todo.
  • RabbitMQ: sí, con matiz. Pedidos publica vía outbox (02-05): POST /pedidos guarda pedido y evento en PostgreSQL y el relay publica después. Estrictamente, podría aceptar pedidos sin RabbitMQ; pero también consume pedidos.saga y sin él la saga no avanza. TechCorp lo incluye: un pedido aceptado que no puede avanzar es peor experiencia que un 503 breve. (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 responder 503 DEPENDENCIA_NO_DISPONIBLE solo 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

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

Módulo 8: Casos de Estudio y Ejemplos Prácticos

© Copyright 2026. Todos los derechos reservados