Módulo 1 · Fundamentos que sostienen todo

Lección 02 — Redes básicas

DNS, TCP, latencia, balanceadores de carga y Nginx delante de tu Django.

Publicada
En esta lección
  1. Ejercicio 1 — DNS con
  2. Ejercicio 2 — El desglose de fases
  3. Ejercicio 3 — El 502 simulado
  4. Ejercicio 4 — Balanceo entre workers
  5. Ejercicio 5 — Cabeceras de proxy
  6. Ejercicio 6 — Health check de diseño
  7. Ejercicio 7 — Latencia de la reserva
  8. Ejercicio 8 — Trazado de ruta
  9. Resumen del profesor

No leas esto sin haber intentado los ejercicios. El diagnóstico es la habilidad; la solución es solo la confirmación.


Ejercicio 1 — DNS con dig

  1. Devuelve registros A (IPv4; si la máquina tiene IPv6, también AAAA). +short oculta el tipo; en la salida completa lo ves en la columna 4 (IN → tipo).
  2. El TTL baja con cada lectura desde caché: dig pregunta a tu resolver, y este responde desde su caché con el TTL restante de su copia. Cuando llega a 0, el resolver re-pregunta al autoritativo y el TTL vuelve a su valor original. No es que "el registro cambie": es la cuenta atrás de la copia en caché.
  3. Cadena típica: servidores root (.) → TLD (org.) → autoritativo del dominio. Cada nivel delega: nadie conoce todo, cada servidor conoce a quién preguntar después.
  4. Ambos resolvers responden lo mismo si la zona está estable, pero pueden tener copias cacheadas de distinta edad: durante una migración con TTL corto verás la IP nueva en un resolver y la vieja en otro. Implicación práctica: no esperes consistencia global instantánea; baja TTL antes, verifica después desde varios resolvers.

Ejercicio 2 — El desglose de fases

  1. Contra 127.0.0.1: DNS ≈ 0 (no hay resolución real, o la resuelve el SO al instante), TCP ≈ 0.1-0.5 ms (loopback: no hay red física), TLS no aplica (HTTP plano). Todo el tiempo es tu Django procesando.
  2. Contra sitios exteriores domina time_appconnect (TLS) y time_connect (TCP): ambos valen ~1 RTT cada uno, y ese RTT es la distancia física (Madrid→Virginia ~90 ms). La distancia "se siente" exactamente donde predice la física.
  3. Sí bajan: la 1ª petición paga DNS (caché del resolver), TCP (handshake completo) y TLS (handshake completo); la 2ª y 3ª reutilizan la caché de DNS del SO/resolver y, con curl moderno, cada invocación abre conexión nueva, pero el resolver ya tiene la respuesta en caché y el TLS puede reanudarse (session resumption) si el servidor lo permite. Para ver el keep-alive real necesitas varias URLs en el mismo curl (o HTTP/2): es el experimento que enlaza con la Lección 01.

Ejercicio 3 — El 502 simulado

1-2. Gunicorn atiende en 127.0.0.1:8000 y el curl recibe 200 OK.

  1. Con Gunicorn muerto, el cliente recibe curl: (7) Failed to connect to 127.0.0.1 port 8000: Connection refused. Nota honesta: eso sería exactamente lo que vería Nginx al hacer proxy_pass contra un Gunicorn caído; y ante eso, Nginx responde al cliente 502 Bad Gateway. La cadena mental: proxy vivo + backend muerto = 502. (Si el propio puerto no aceptara conexión y no hubiera proxy, el cliente ve "connection refused", no un código HTTP.)
  2. En error.log buscarías: connect() failed (111: Connection refused) while connecting to upstream (puerto equivocado o proceso caído) y upstream prematurely closed connection (el worker murió a mitad de petición, típico OOM). Después: systemctl status gunicorn, y revisar que el socket/puerto del proxy_pass coincide con el del servicio.

Ejercicio 4 — Balanceo entre workers

2-3. En los logs verás dos PIDs distintos atendiendo peticiones. Gunicorn con workers sync usa un patrón accept-based: el socket de escucha es compartido y cada worker acepta la conexión que "gane" (el kernel despierta a uno). No es round robin de balanceador HTTP (que decide antes de enviar la petición); es el kernel repartiendo el accept. Efecto similar, mecanismo distinto: con duraciones muy desiguales, accept-based puede dar mala suerte (un worker acumula las peticiones lentas).

  1. Con un worker bloqueado 10 s, sus 3 peticiones siguientes esperan en la cola del socket mientras el otro worker sigue libre: un balanceador HTTP con least connections las habría mandado al libre. Lección para TicketFlow: los POST de pago (lentos) y los GET (rápidos) conviven mejor con least connections o con tipos de worker asíncronos (gevent, Lección 03).

Ejercicio 5 — Cabeceras de proxy

  1. Con SECURE_PROXY_SSL_HEADER configurada y una petición sin la cabecera X-Forwarded-Proto: https, Django interpreta el esquema según lo que llegue; con la petición HTTP plana y sin cabecera, request.scheme es http y no pasa nada raro. El veneno real ocurre al revés: si Nginx sí envía la cabecera (o un cliente la falsifica) y tú no filtras cabeceras del cliente, un atacante que envíe X-Forwarded-Proto: https por HTTP plano engaña a Django sobre la seguridad de la conexión. Por eso la config exige que el proxy siempre ponga o quite esa cabecera.
  2. Sin X-Forwarded-For, Django vería como IP del cliente 127.0.0.1 (la del propio Nginx): todos tus logs de auditoría y rate limits apuntarían a la misma "IP". Sin X-Forwarded-Proto, request.scheme sería http aunque el cliente hable HTTPS.
  3. En Nginx: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; y proxy_set_header X-Forwarded-Proto $scheme; — y en Django confiar solo en esas cabeceras si el tráfico llega de tu proxy (SECURE_PROXY_SSL_HEADER más firewall que impida peticiones directas al puerto de Gunicorn).

Ejercicio 6 — Health check de diseño

  1. Niveles: liveness (¿el proceso responde? barato, siempre) y readiness (¿puedo atender de verdad? BD/Redis). Comprobar la BD en cada check cada 5 s son 17.280 queries/día por instancia: aceptable en una SELECT 1, carísimo si el check consulta tablas reales. Recomendación: liveness barato siempre; readiness con SELECT 1 (no con queries de negocio).
  2. Si la BD responde lenta pero responde: 200 en liveness; readiness según política. Devolver 503 hace que el balanceador saque la instancia del pool: si la lentitud es puntual, has amplificado la caída (menos capacidad justo cuando va mal). Si es estructural, sacarla es lo correcto. Decisión con criterio: umbrales de latencia y flapping (varios fallos seguidos antes de sacar).
  3. Versión del despliegue (¿a qué build hablo?), arranque (¿se reinició en bucle?) y PID (¿corresponde al proceso que veo en top?): son las tres pistas que convierten "la API va mal" en "la v1.4.2 desplegada a las 10:03 va mal". Coste: cero. Valor en incidente: enorme.

Ejercicio 7 — Latencia de la reserva

  1. Con la BD a 90 ms de RTT, 5 viajes = ~450 ms añadidos. En una transacción única los viajes no desaparecen, pero se planifican mejor: con transaction.atomic() sigues teniendo 5 statements pero puedes reducir idas y venidas (batch de INSERTs, RETURNING id) y, sobre todo, el bloqueo se sostiene 450 ms: más contención. La solución senior es minimize-RTT (BD en la misma región que la app) — la física no se negocia; la Lección 10 se ocupa del resto.
  2. Es consistencia eventual entre caché y origen: el CDN sirvió una copia de hasta 60 s de antigüedad. Opciones de negocio: TTL bajo en disponibilidad (5-10 s) + invalidación al confirmar; o asumir la contradicción y resolverla al reservar (el 409 de la Lección 01) con un mensaje claro. La segunda es más robusta: la caché acelera; la BD decide.

Ejercicio 8 — Trazado de ruta

  1. Los saltos son routers intermedios; los * son saltos que no responden ICMP con TTL expirado (o lo descartan). No significa "caído": significa "no contesta a traceroute", y la ruta continúa más allá.
  2. El primero es tu router (siempre responde: es tuyo). El último puede no responder ICMP por política de seguridad (rate-limit o drop de ICMP hacia su IP pública), aunque atienda tu servicio perfectamente en TCP 443.

Resumen del profesor

  • El viaje es DNS → TCP → TLS → HTTP: cada fase se paga y curl -w te lo desglosa. Diagnostica por fases, no por intuición.
  • La latencia es física (RTT) y el ancho de banda casi nunca es el problema de una API JSON.
  • El balanceador reparte y vigila (health checks); tu app debe ser sin estado para poder réplicas.
  • Nginx delante de Gunicorn: estáticos, TLS y cabeceras X-Forwarded-* — sin ellas, Django miente sobre el mundo que ve.

Cuando entregues, corregimos y pasamos a la Lección 03 — Concurrencia y asincronía: el event loop, los hilos y la carrera que la Lección 01 te prometió demostrar.