Módulo 7: Analizar resultados y CI
5. Encontrar el cuello de botella (app, BD, red)
Descripción
Detectaste una regresión: el p95 empeoró. La pregunta inmediata es ¿dónde se fue el tiempo?. Un endpoint que tarda 60 ms en vez de 6 gastó esos 54 ms extra en algún lado —procesando en la app, esperando a la base de datos, viajando por la red— y saber en cuál es lo que te dice qué mirar después. En esta lección aprendes a localizar el cuello de botella: las señales que separan un problema de app de uno de base de datos o de red, y cómo la propia herramienta de carga ayuda a distinguirlas (el desglose de tiempos de una petición, un Trend por paso). Es importante ser claro con el alcance: aquí se menciona cómo se investiga, no se optimiza. Arreglar el cuello de botella —poner un índice, añadir caché, reescribir la consulta— es el "después", fuera de esta guía. Lo que sí haces aquí, ejecutado, es la primera atribución: comparar la corrida rápida con la lenta para ver que el tiempo extra está en el servidor y no en la red.
Conexión con el módulo: esta lección sigue naturalmente a la 4. Detectar la regresión (lección 4) responde "¿empeoró?"; localizar el cuello de botella responde "¿dónde?". Reúsa la métrica de tendencia (lección 2) —un Trend por paso es la herramienta para aislar el paso lento— y las corridas exportadas (lección 3). Es la frontera de la guía: aquí termina el trabajo del ingeniero de carga (medir, detectar, señalar dónde) y empieza el del que optimiza (arreglarlo), que pertenece a otras guías.
El plomero que localiza la fuga antes de romper la pared
Cuando hay poca presión de agua en la ducha, el plomero no empieza a romper paredes al azar. Primero localiza el problema. Abre la llave de la cocina: si ahí también sale poca agua, la fuga está en la tubería principal (compartida); si la cocina sale bien, el problema es solo del ramal de la ducha. Cierra la llave de paso y mide la presión en distintos puntos. Con unas pocas pruebas acota dónde está la fuga —en la entrada, en un tramo concreto, en la regadera— sin haber roto una sola pared todavía. Solo cuando sabe dónde, decide cómo arreglarlo.
Localizar un cuello de botella de rendimiento es ese trabajo de plomería. No optimizas a ciegas ("pongamos caché en todo, a ver si mejora"); primero acotas dónde se va el tiempo. ¿En la app (el código que procesa la petición)? ¿En la base de datos (una consulta lenta)? ¿En la red (el viaje de los bytes)? Cada una deja una señal distinta, y la prueba de carga —con su desglose de tiempos— es tu manómetro. Esta lección te enseña a leer el manómetro y señalar el tramo. Romper la pared (optimizar) es otro oficio.
Las tres sospechas y sus señales
Cuando un endpoint está lento, el tiempo se fue en uno (o varios) de tres lugares. Cada uno tiene una firma que ayuda a distinguirlo:
- La app (CPU / código). El servidor gasta tiempo calculando: un bucle pesado, una serialización costosa, trabajo que crece con la carga. Señal: la latencia sube con la concurrencia (más peticiones compitiendo por CPU), y el tiempo se va en el procesamiento del servidor, no en conectarse ni en transferir. El
/quote_cpudel módulo 5 (trabajo de CPU serializado por el GIL) es este caso. - La base de datos. El servidor está esperando a la BD: una consulta sin índice, un lock, una consulta N+1 (una consulta por fila en un bucle). Señal: el tiempo se va en el procesamiento del servidor (igual que la app), pero no escala con la CPU —el servidor está ocioso, esperando—; y la latencia salta cuando crecen los datos o la contención en la BD. El
/quote_slowde este módulo (untime.sleepque modela una espera externa) se comporta así: el servidor no calcula, espera. - La red. Los bytes tardan en viajar: latencia de ida y vuelta alta (clientes lejanos), payloads grandes, saturación del ancho de banda. Señal: el tiempo se va en conectar y transferir, no en el procesamiento del servidor; empeora con la distancia geográfica y el tamaño de la respuesta, no con la carga del servidor. En
localhostesta señal es casi nula —por eso nuestro entorno aísla bien las otras dos—.
La clave para distinguirlas es dónde dentro de la petición se fue el tiempo. Y ahí entra el desglose que da la herramienta de carga.
El desglose de tiempos de una petición (k6, contenido)
k6 no solo mide la latencia total (http_req_duration): la desglosa en las fases de una petición HTTP, y ese desglose es tu manómetro para separar red de servidor. Las métricas (contenido rotulado, de la documentación de k6):
http_req_blocked: tiempo esperando un socket libre (antes de conectar).http_req_connecting: tiempo estableciendo la conexión TCP. Alto = problema de red/conexión.http_req_tls_handshaking: negociación TLS (HTTPS).http_req_sending: tiempo enviando el request. Alto = payload grande / red lenta.http_req_waiting: tiempo desde que se terminó de enviar hasta el primer byte de respuesta —el famoso TTFB (time to first byte)—. Es el tiempo de procesamiento del servidor (la app + la BD). Alto = problema de app o de base de datos.http_req_receiving: tiempo descargando la respuesta. Alto = respuesta grande / red lenta.
La regla de lectura es simple y potente: si el tiempo se concentra en http_req_waiting (TTFB), el problema está en el servidor (app o BD); si se concentra en connecting/sending/receiving, está en la red. Un resumen de k6 que separa así (contenido):
// CONTENIDO (no ejecutado aqui): desglose de tiempos de k6. Ver grafana.com/docs/k6
http_req_duration..............: avg=61ms p(95)=75ms
{ expected_response:true }...: avg=61ms p(95)=75ms
http_req_waiting...............: avg=60ms p(95)=74ms <- casi TODO el tiempo: el SERVIDOR
http_req_connecting............: avg=0.3ms p(95)=0.5ms <- red: despreciable
http_req_sending...............: avg=0.1ms p(95)=0.2ms
http_req_receiving.............: avg=0.2ms p(95)=0.4ms
Ese retrato —60 de 61 ms en http_req_waiting— grita "el servidor está tardando en responder": el problema es app o BD, no la red. Para bajar de "el servidor" a "app vs BD" hace falta mirar dentro del servidor (¿sube con la CPU? → app; ¿el servidor espera ocioso? → BD), y ahí ya se usan herramientas del lado del servidor: logs, un profiler, las métricas de la base de datos. La prueba de carga te lleva hasta la puerta del servidor; cruzarla es el trabajo de optimización.
El lado ejecutable: atribuir el tiempo extra
Con nuestras dos corridas exportadas podemos hacer la primera atribución de verdad. Las dos golpean el mismo localhost (misma red, despreciable), así que cualquier diferencia de latencia entre /quote y /quote_slow está en el servidor, no en la red. Este pequeño script lo hace explícito:
"""Localiza DONDE crecio la latencia comparando dos corridas.
No optimiza nada: solo atribuye el aumento del p50/p95. Como las dos corridas
golpean el MISMO localhost (misma red), un aumento en el tiempo de respuesta
apunta al procesamiento del servidor (la app o su base de datos), no a la red.
Uso: python3.14 where_time_went.py baseline.json actual.json
"""
import json
import sys
with open(sys.argv[1]) as f:
base = json.load(f)
with open(sys.argv[2]) as f:
now = json.load(f)
d_p50 = now["latency_ms"]["p50"] - base["latency_ms"]["p50"]
d_p95 = now["latency_ms"]["p95"] - base["latency_ms"]["p95"]
print(f"baseline {base['endpoint']:>12}: p50={base['latency_ms']['p50']}ms "
f"p95={base['latency_ms']['p95']}ms")
print(f"actual {now['endpoint']:>12}: p50={now['latency_ms']['p50']}ms "
f"p95={now['latency_ms']['p95']}ms")
print(f"delta : p50 {d_p50:+.2f}ms p95 {d_p95:+.2f}ms")
print(f"misma red (localhost) en ambas -> el tiempo extra esta en el SERVIDOR")
print(f"proximo paso (el 'despues', fuera de esta guia): perfilar ese endpoint")
Salida real:
Qué esperar — el endpoint lento añadió ~50 ms al p50 y ~55 ms al p95; como la red es la misma, ese tiempo está en el servidor:
$ python3.14 where_time_went.py results_baseline.json results_actual.json
baseline /quote: p50=4.7ms p95=6.66ms
actual /quote_slow: p50=54.74ms p95=61.27ms
delta : p50 +50.04ms p95 +54.61ms
misma red (localhost) en ambas -> el tiempo extra esta en el SERVIDOR
proximo paso (el 'despues', fuera de esta guia): perfilar ese endpoint
La atribución está hecha: los ~50 ms extra están en el servidor, y como /quote_slow no calcula (solo hace time.sleep), la firma es la de una espera externa —el modelo de una consulta lenta a la base de datos—. Un http_req_waiting alto con el servidor ocioso apunta a la BD; si en cambio la latencia hubiera subido con la concurrencia por CPU, apuntaría a la app. Nosotros llegamos hasta aquí: "el tiempo está en el servidor, con firma de espera". El siguiente paso —abrir el endpoint, encontrar la consulta lenta y ponerle un índice— es optimización, y es el "después".
Dónde termina esta guía y empieza el "después"
Conviene ser explícito con la frontera, porque es una tentación fácil de cruzar. Una prueba de carga detecta y localiza: te dice que el p95 empeoró (lección 4) y te señala dónde se fue el tiempo (esta lección, hasta "el servidor / con esta firma"). Ahí termina su trabajo. Optimizar —bajar ese tiempo— es una disciplina distinta con sus propias herramientas: perfiladores de código, planes de ejecución de consultas (EXPLAIN), índices, cachés, colas. Esta guía no la enseña, y mezclarla aquí sería salirse del alcance.
La razón de mantener la frontera limpia es práctica: el ciclo sano es medir → detectar → localizar → optimizar → volver a medir. Las primeras tres son testing de carga (esta guía); la cuarta es ingeniería de rendimiento (otra); y la quinta te devuelve aquí, a comprobar con una nueva corrida que la optimización de verdad bajó el p95 y no rompió nada. La prueba de carga es el juez que abre y cierra el ciclo; la optimización es lo que pasa en medio, en otro lado. Cuando localices el cuello de botella, el enlace natural es a las guías de optimización de app y de base de datos del ecosistema —y luego de vuelta a una corrida para verificar—.
Errores comunes
Optimizar sin localizar. Qué pasa: se ve un p95 alto y se empieza a "mejorar" cosas al azar —añadir caché, subir workers— sin saber dónde está el tiempo. Por qué pasa: la urgencia empuja a actuar antes de diagnosticar. Cómo detectarlo: cambios de rendimiento que no mueven el p95, porque atacaron el lugar equivocado. Cómo corregirlo: localiza primero (¿http_req_waiting? → servidor; ¿connecting? → red), y solo entonces optimiza el tramo correcto. Como el plomero: no rompas la pared hasta saber dónde está la fuga.
Confundir latencia de servidor con latencia de red. Qué pasa: un p95 alto se atribuye a "la red está lenta" cuando en realidad el servidor tarda en responder. Por qué pasa: no se mira el desglose de tiempos. Cómo detectarlo: si http_req_waiting (TTFB) es casi toda la latencia, el problema es el servidor, no la red —da igual cuánto "mejores la red"—. Cómo corregirlo: lee el desglose; waiting alto = servidor (app/BD), connecting/receiving alto = red.
Creer que esta guía optimiza. Qué pasa: alguien espera que el módulo enseñe a arreglar la consulta lenta o a poner el índice. Por qué pasa: detectar y arreglar se sienten como un solo trabajo. Cómo detectarlo: si buscas "cómo hago el endpoint más rápido", estás pidiendo el "después", que no vive aquí. Cómo corregirlo: entiende la frontera —esta guía mide, detecta y localiza; la optimización es otra disciplina— y sigue el enlace a las guías de optimización cuando toque arreglar.
Ejercicios
Ejercicio 1 — ¿App, BD o red? Para cada síntoma, di cuál es la sospecha más probable. (a) Casi toda la latencia está en http_req_waiting, y sube cuando crece la concurrencia (más CPU). (b) Casi toda en http_req_waiting, pero el servidor está ocioso (poca CPU) y empeoró al crecer la tabla. (c) Casi toda en http_req_connecting y http_req_receiving, con respuestas grandes y clientes lejanos.
Ver solución
- (a) La app (CPU/código). El tiempo está en el servidor (
waiting) y escala con la concurrencia por CPU: el código hace trabajo pesado que compite por el procesador. (El/quote_cpudel módulo 5.) - (b) La base de datos. El tiempo está en el servidor (
waiting) pero no lo consume la CPU —el servidor espera ocioso a la BD— y empeora con el tamaño de los datos: firma de una consulta lenta o sin índice. (El/quote_slowde este módulo.) - (c) La red. El tiempo se va en conectar y transferir (
connecting/receiving), no en el procesamiento del servidor; los payloads grandes y la distancia geográfica lo confirman.
Ejercicio 2 — Lee el desglose. Un resumen dice: http_req_duration: avg=120ms; http_req_waiting: avg=118ms; http_req_connecting: avg=0.5ms; http_req_receiving: avg=1ms. (a) ¿El cuello de botella está en la red o en el servidor? (b) ¿Qué mirarías después para separar app de BD? (c) ¿Esta guía te dice cómo arreglarlo?
Ver solución
- (a) En el servidor.
http_req_waiting(118 ms) es casi toda la latencia (120 ms); la parte de red (connecting+receiving≈ 1.5 ms) es despreciable. La red no es el problema. - (b) Miraría, del lado del servidor: si la latencia sube con la concurrencia por CPU (el servidor está al 100% de CPU) → app; si el servidor está ocioso esperando y la latencia crece con los datos → base de datos. Herramientas: un profiler de código, los logs, las métricas y el
EXPLAINde la BD. - (c) No. Esta guía te lleva hasta "el problema está en el servidor, con esta firma". Arreglarlo (índice, caché, reescribir la consulta) es el "después", en las guías de optimización.
Ejercicio 3 — El ciclo completo. Ordena estos cinco pasos en el ciclo sano de rendimiento y di cuáles son de esta guía y cuáles del "después": optimizar, medir, volver a medir, localizar, detectar.
Ver solución
Orden: medir → detectar → localizar → optimizar → volver a medir.
- medir (correr la prueba de carga) — esta guía.
- detectar (el p95 empeoró: regresión) — esta guía (lección 4).
- localizar (¿dónde se fue el tiempo? servidor/red) — esta guía (esta lección).
- optimizar (arreglar la consulta, poner el índice) — el "después", otra disciplina.
- volver a medir (confirmar que el p95 bajó y nada se rompió) — esta guía otra vez.
La prueba de carga abre y cierra el ciclo (medir, detectar, localizar, y verificar al final); la optimización pasa en medio, fuera de esta guía. Por eso importa la frontera: el juez y el que arregla son oficios distintos.
Resumen y siguiente paso
En esta lección aprendiste a localizar un cuello de botella tras detectar una regresión: las tres sospechas —app (CPU/código), base de datos (espera), red (viaje de los bytes)— y sus firmas. La herramienta clave es el desglose de tiempos de la petición: si el tiempo se concentra en http_req_waiting (TTFB), el problema está en el servidor (app o BD); si en connecting/sending/receiving, está en la red. Con las dos corridas exportadas hiciste la primera atribución real: los ~50 ms extra de /quote_slow están en el servidor (misma red en ambas), con firma de espera externa —el modelo de una consulta lenta a la BD—.
Y trazaste la frontera: esta guía mide, detecta y localiza; optimizar —bajar el tiempo— es el "después", otra disciplina con sus propias herramientas. El ciclo sano es medir → detectar → localizar → optimizar → volver a medir, y la prueba de carga abre y cierra ese ciclo.
Antes de avanzar deberías poder: nombrar las tres sospechas y sus señales; usar el desglose de tiempos para separar servidor de red; y explicar dónde termina esta guía y empieza la optimización. Lo que sigue, en la lección 6, es la segunda mitad del módulo —automatizar—: poner la prueba de carga en un pipeline de CI, con el .github/workflows/load.yml y el threshold como gate que bloquea el deploy.
Recursos
- k6 — HTTP request timings — el desglose de tiempos de una petición (
http_req_waiting,connecting,sending,receiving) con el que se separa un cuello de botella de servidor de uno de red. La fuente del contenido de esta lección. - Google SRE Book — Monitoring Distributed Systems — cómo se correlacionan las métricas del cliente (la prueba de carga) con las del servidor para localizar dónde se va el tiempo; el fundamento de "cruzar la puerta del servidor".
- k6 —
Trend(custom metric) — unTrendpor paso del flujo, la herramienta para aislar cuál de varias peticiones de una iteración es la lenta (complementa el desglose de tiempos de una sola petición). e2e-testing-with-playwright-guide— la guía hermana del ecosistema Testing; junto con las guías de optimización de app y base de datos, es a dónde sigue el "después" cuando toca arreglar el cuello de botella que aquí localizaste.