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
- Devuelve registros A (IPv4; si la máquina tiene IPv6, también AAAA).
+shortoculta el tipo; en la salida completa lo ves en la columna 4 (IN → tipo). - El TTL baja con cada lectura desde caché:
digpregunta 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é. - 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. - 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
- 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. - Contra sitios exteriores domina
time_appconnect(TLS) ytime_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. - 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
curlmoderno, 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 mismocurl(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.
- 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 hacerproxy_passcontra 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.) - En
error.logbuscarías:connect() failed (111: Connection refused) while connecting to upstream(puerto equivocado o proceso caído) yupstream 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 delproxy_passcoincide 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).
- 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
- Con
SECURE_PROXY_SSL_HEADERconfigurada y una petición sin la cabeceraX-Forwarded-Proto: https, Django interpreta el esquema según lo que llegue; con la petición HTTP plana y sin cabecera,request.schemeeshttpy 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íeX-Forwarded-Proto: httpspor 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. - Sin
X-Forwarded-For, Django vería como IP del cliente127.0.0.1(la del propio Nginx): todos tus logs de auditoría y rate limits apuntarían a la misma "IP". SinX-Forwarded-Proto,request.schemeseríahttpaunque el cliente hable HTTPS. - En Nginx:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;yproxy_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_HEADERmás firewall que impida peticiones directas al puerto de Gunicorn).
Ejercicio 6 — Health check de diseño
- 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 conSELECT 1(no con queries de negocio). - 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).
- 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
- 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. - 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
- 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á. - 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 -wte 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.