Cuellos de botella sin histeria: Cómo medir el límite real

¿Te fue útil?

Todo está lento, los gráficos están en rojo y alguien ya ofreció la solución habitual: “Simplemente añade más CPU.” Otra persona dice “Es el almacenamiento.” Un tercero insiste “Es la red.” Así es como los equipos queman días, dinero y credibilidad: resolviendo la teoría más ruidosa en lugar del límite real.

La cura es aburrida y fiable: mide saturación, latencia, rendimiento y colas—al mismo tiempo—en la ruta crítica. Luego toma una decisión que coincida con la física. No con las sensaciones.

Qué es un cuello de botella (y qué no lo es)

Un cuello de botella es el recurso que limita el trabajo útil de tu sistema para una carga de trabajo dada. No es “lo que tiene el gráfico más grande.” No es “el componente que actualizaste el trimestre pasado.” No es “lo que está al 90%.”

Los cuellos de botella dependen de la carga. “El almacenamiento es el cuello de botella” no es un diagnóstico. Es una categoría. El diagnóstico sería: “Lecturas aleatorias 4k a QD=1 están limitadas por la latencia de lectura NVMe; estamos alcanzando p99 de 3.2ms y hay encolamiento en el thread pool de la aplicación.” Eso es accionable. Es un bisturí, no una bocina.

Tres definiciones que deberías tatuar en tu runbook

  • Capacidad: cuánto trabajo puede hacer un recurso por unidad de tiempo (por ejemplo, requests/sec, MB/sec, IOPS).
  • Saturación: qué tan cerca estás de esa capacidad bajo una carga (y si se están formando colas).
  • Latencia de cola: lo que ven tus usuarios peor afectados. P95/P99 es donde vive tu pager.

Además: los cuellos de botella se mueven. Arregla CPU y expones contención de locks. Arregla locks y expones almacenamiento. Arregla almacenamiento y expones red. Felicitaciones: tu sistema ahora es lo suficientemente rápido para encontrar una nueva manera de ponerse lento.

Una cita para mantenerte honesto. La “idea parafraseada” de Gene Kim: Mejorar el flujo significa encontrar las restricciones y elevarlas; optimizar no-restricciones sólo crea velocidad local y dolor global. — Gene Kim (idea parafraseada)

Broma #1 (corta, relevante): Un “arreglo rápido de rendimiento” es como una “regla de firewall temporal.” Estará ahí en tu fiesta de jubilación.

Las cuatro señales que importan: saturación, latencia, rendimiento, errores

Si quieres menos discusiones y llamadas de incidentes más rápidas, estandariza un vocabulario de medición. Uso cuatro señales porque obligan a la claridad:

Saturación: “¿Estamos en el límite?”

La saturación trata de colas y contención. Run queues de CPU. Colas de peticiones de disco. Colas de transmisión de la NIC. Pools de conexiones de base de datos. Programación de pods en Kubernetes. Si las colas crecen, estás saturado o mal configurado.

Latencia: “¿Cuánto tarda una unidad de trabajo?”

Mide latencia de extremo a extremo y latencia por componente. La latencia media es ficción educada; usa percentiles. La latencia de cola es donde los cuellos de botella aparecen primero porque las colas amplifican la varianza.

Rendimiento: “¿Cuánto trabajo útil completamos?”

Rendimiento no es “bytes movidos” si al negocio le importan “órdenes completadas.” Idealmente mides ambos: rendimiento a nivel de usuario y rendimiento del sistema. Un sistema puede mover muchos bytes mientras completa menos transacciones. Así es como los reintentos y los thundering herds pagan el yate nuevo de tu proveedor cloud.

Errores: “¿Fallamos rápido o fallamos lento?”

Cuando hay saturación, los sistemas a menudo fallan lentamente antes de fallar ruidosamente. Timeouts, reintentos, errores de checksum, retransmisiones TCP, errores de I/O, context deadline exceeded—eso es humo de cuello de botella. Trata los errores como señales de rendimiento de primera clase.

Qué significa “el límite real”

El límite real es el punto donde aumentar la carga ofrecida no aumenta el trabajo completado, mientras latencia y encolamiento explotan. Ese es un “knee” medible. Tu objetivo es identificar ese knee para la carga que importa, no para un benchmark sintético que favorezca tu orden de compra.

Colas: donde el rendimiento muere en silencio

La mayoría de los cuellos de botella en producción son problemas de encolamiento disfrazados de “lento”. La máquina no está necesariamente lenta; está esperando su turno detrás de otro trabajo. Por eso “utilización de recurso” puede parecer correcta mientras el sistema es miserable.

La ley de Little, pero hazla operativa

La ley de Little: L = λW (promedio de elementos en el sistema = tasa de llegada × tiempo en el sistema). No necesitas un doctorado en matemáticas para usarla. Si la latencia (W) sube y la tasa de llegada (λ) se mantiene, el número de cosas esperando (L) debe aumentar. Eso es encolamiento. Encuentra la cola.

Tipos de colas que encontrarás a las 2 a.m.

  • Run queue de CPU: hilos listos para ejecutarse pero no programados (observa load y longitud de run queue).
  • Cola de I/O de bloque: peticiones esperando por discos/NVMe (observa avgqu-sz, await, utilización del dispositivo).
  • Colas de mutex/locks: hilos esperando por un lock (perf, eBPF, o métricas de la app).
  • Pools de conexiones: pools DB/HTTP que limitan la concurrencia (observa tiempo de espera en el pool).
  • Colas de red del kernel: drops en qdisc, sobrecargas de ring buffers (observa drops, retransmits).
  • Stalls de GC y del asignador: encolamiento dentro de runtimes (JVM, Go, Python).

Las colas no siempre son malas. Se vuelven malas cuando son incontroladas, están ocultas o están acopladas a reintentos. Los reintentos multiplican las colas. Un pequeño aumento de latencia se convierte en un DDoS autoinfligido.

Guion de diagnóstico rápido (primero/segundo/tercero)

Esta es la secuencia de “entrar a una sala en llamas sin convertirte en el incendio”. Está diseñada para la realidad on-call: tienes visibilidad parcial, stakeholders enfadados y exactamente una oportunidad de no empeorar las cosas.

Primero: confirma el síntoma en términos de usuario

  • ¿Qué está lento? ¿Una carga de página? ¿Un endpoint API? ¿Un job por lotes? ¿Una consulta DB?
  • ¿Es latencia, rendimiento o ambos?
  • ¿Es global o aislado (una AZ, un pool de nodos, un tenant)?
  • ¿Qué cambió recientemente (deploys, config, forma del tráfico, crecimiento de datos)?

Segundo: encuentra la cola más cercana al dolor

Empieza en el servicio orientado al usuario y camina hacia abajo en la pila:

  1. Métricas de la app: percentiles de latencia de requests, concurrencia, timeouts, tasa de reintentos.
  2. Thread/worker pools: longitud de cola, saturación, trabajo rechazado.
  3. DB: espera en pools de conexión, consultas lentas, esperas por locks.
  4. OS: run queue de CPU, iowait, presión de memoria, context switches.
  5. Almacenamiento: latencia/queue depth del dispositivo, stalls del filesystem, ZFS txg sync, writeback de páginas sucias.
  6. Red: retransmits, drops, saturación de interfaces, latencia de DNS.

Tercero: mide saturación y margen, y deja de adivinar

Elige 2–3 candidatos y recoge evidencia sólida en 10 minutos:

  • CPU: run queue, steal time, throttling, hotspots por core.
  • Memoria: fallos mayores, swap, reclaim, riesgo de OOM.
  • Disco/NVMe: await, svctm (con cuidado), avgqu-sz, util, stalls de discard/writeback.
  • Red: retransmits, drops, qdisc, errores NIC, RTT.

Si no puedes explicar la ralentización con una de esas categorías, busca cuellos de botella de coordinación: locks, elecciones de líder, servicios centralizados, cuotas, límites de API, o un shard caliente único.

Tareas prácticas: comandos, salidas, decisiones (12+)

Estas tareas están pensadas para ejecutarse en un host Linux en la ruta crítica. Cada una incluye: un comando, qué significa la salida y qué decisión tomar. Ejecútalas durante un incidente, pero también en tiempos tranquilos para saber qué es “normal”.

Tarea 1: Identificar si la CPU es realmente el límite (run queue + iowait)

cr0x@server:~$ uptime
 14:02:11 up 37 days,  3:19,  2 users,  load average: 18.24, 17.90, 16.88

Significado: El load average cuenta tareas ejecutables y sleep no interrumpible (a menudo I/O wait). “18” en una máquina de 32 cores puede estar bien o ser terrible, dependiendo de qué estén haciendo esas tareas.

Decisión: No declares cuello de botella de CPU sólo por el load. Sigue con vmstat y una vista por core.

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
12  7      0  82124  51200 842120    0    0   120  9820  820 2400 22  8 41 29  0
15  8      0  81560  51200 841980    0    0   140 10120  900 2600 21  9 39 31  0

Significado: r son tareas ejecutables; b bloqueadas (a menudo por I/O). Alto wa con alto b apunta a stalls de I/O, no a falta de CPU.

Decisión: Si b y wa son altos, cambia la atención a I/O de almacenamiento/red antes de escalar CPU.

Tarea 2: Comprobar saturación por core y steal/throttling (dolor de virtualización)

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.1.0 (server) 	01/10/2026 	_x86_64_	(32 CPU)

13:58:01     CPU   %usr   %sys   %iowait   %irq   %soft  %steal  %idle
13:58:02     all   24.10  10.20    18.50   0.00    0.90    0.00  46.30
13:58:02       7   89.00   9.00     0.00   0.00    0.00    0.00   2.00
13:58:02      12   10.00   5.00    70.00   0.00    1.00    0.00  14.00

Significado: Un CPU pegado puede indicar un cuello de botella mono-hilo o desbalance de IRQ; alto %steal indica contención del hypervisor; alto %iowait en CPUs específicas puede correlacionar con afinidad de IRQ y colas de dispositivo.

Decisión: Si unos pocos cores están saturados, investiga hotspots de un solo hilo, distribución de interrupciones o ajustes de afinidad antes de añadir más cores.

Tarea 3: Identificar los principales encolados: CPU vs I/O vs locks (triage de alto nivel)

cr0x@server:~$ top -b -n 1 | head -n 20
top - 13:59:41 up 37 days,  3:17,  2 users,  load average: 18.24, 17.90, 16.88
Tasks: 412 total,  11 running, 401 sleeping,   0 stopped,   0 zombie
%Cpu(s): 22.4 us,  8.9 sy,  0.0 ni, 46.1 id, 22.1 wa,  0.0 hi,  0.5 si,  0.0 st
MiB Mem :  64000.0 total,   1100.2 free,   4200.1 used,  58700.0 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.  59600.0 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
23120 app       20   0 4120.5m  520.2m  152.0m D  35.0   0.8  12:20.11 java

Significado: Un proceso en estado D está en sleep no interrumpible, normalmente esperando I/O. Un alto wa a nivel sistema refuerza I/O como candidato a cuello de botella.

Decisión: Sigue los procesos en D para ver qué están esperando (dispositivos de bloque, NFS, etc.).

Tarea 4: Mapear un proceso atascado a sus ficheros abiertos y al filesystem de respaldo

cr0x@server:~$ lsof -p 23120 | head
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF  NODE NAME
java    23120  app  cwd    DIR  253,0     4096     2 /srv/app
java    23120  app   12u   REG  253,0  1048576 98342 /srv/app/data/index.bin

Significado: Muestra qué rutas usa activamente el proceso. Estás intentando conectar “la app está lenta” con “este filesystem/dispositivo está lento”.

Decisión: Identifica el mount y el dispositivo que respalda la ruta crítica; enfoca las herramientas de I/O allí.

cr0x@server:~$ findmnt -T /srv/app/data/index.bin
TARGET SOURCE         FSTYPE OPTIONS
/srv   /dev/nvme0n1p2 ext4   rw,relatime

Decisión: Ahora sabes qué dispositivo inspeccionar (nvme0n1 aquí).

Tarea 5: Latencia y encolamiento a nivel de dispositivo (el suero de la verdad)

cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (server) 	01/10/2026 	_x86_64_	(32 CPU)

Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz  %util
nvme0n1         820.0  2100.0  3280.0  26800.0    0.0    0.0   0.00   0.00    1.2    18.9   42.10  98.00

Significado: Alto %util con alto aqu-sz significa que el dispositivo está saturado y las peticiones se están encolando. El await de escritura es alto, así que las escrituras esperan mucho.

Decisión: Deja de fingir que la CPU lo arreglará. O reduces la presión de escritura, cambias el patrón de I/O, añades dispositivos o mueves la carga. También comprueba si la latencia viene del dispositivo o del comportamiento de flush del filesystem.

Tarea 6: Contadores de salud y errores específicos NVMe

cr0x@server:~$ sudo nvme smart-log /dev/nvme0
Smart Log for NVME device:nvme0 namespace-id:ffffffff
critical_warning                    : 0
temperature                         : 41 C
available_spare                     : 100%
percentage_used                     : 7%
media_errors                        : 0
num_err_log_entries                 : 0

Significado: Si ves media errors o un porcentaje used alto, el rendimiento puede degradarse y tu “cuello de botella” se convierte en “unidad fallando”.

Decisión: Si aparecen errores, cambia del modo rendimiento al modo fiabilidad: planifica reemplazo y reduce la carga.

Tarea 7: Presión de writeback del filesystem y del kernel (páginas sucias pueden ser tu cola)

cr0x@server:~$ cat /proc/meminfo | egrep 'Dirty|Writeback|MemFree|MemAvailable'
MemFree:           80124 kB
MemAvailable:   61045232 kB
Dirty:           1248204 kB
Writeback:         92160 kB
WritebackTmp:          0 kB

Significado: Un Dirty grande y persistente puede significar que el sistema está bufferizando escrituras y luego las vacía en ráfagas dolorosas, creando picos de latencia.

Decisión: Si los datos sucios crecen sin drenarse, investiga throttling de writeback, journaling del filesystem y patrones de flush de la aplicación.

Tarea 8: Detectar presión de memoria (el cuello de botella sigiloso que se hace pasar por CPU)

cr0x@server:~$ vmstat 1 5 | tail -n +3
 6  0      0  10240  45000 1200000    0    0     0   2000  1100 3800 30 12 48 10  0
 5  0      0   8120  45000 1180000    0    0     0   2200  1200 4200 29 11 44 16  0

Significado: Observa si/so (swap in/out) y memoria libre consistentemente baja con alto cs (context switches). Incluso sin swap, las tormentas de reclaim pueden disparar la latencia.

Decisión: Si el reclaim es intenso (revisa sar -B o PSI), reduce la huella de memoria o añade RAM; no persigas CPUs fantasmas.

Tarea 9: Pressure Stall Information (PSI): demuestra tiempo perdido por contención

cr0x@server:~$ cat /proc/pressure/io
some avg10=12.34 avg60=10.21 avg300=8.90 total=98324212
full avg10=4.12 avg60=3.20 avg300=2.75 total=31244211

Significado: PSI cuantifica cuánto tiempo las tareas están paradas por presión de I/O. full significa que en momentos ninguna tarea pudo progresar por I/O—esto es dolor real, no teórico.

Decisión: Si PSI es alto, prioriza reducir la contención de I/O (batching, caching, límites de cola) por encima de micro-optimizaciones de CPU.

Tarea 10: Red: encontrar retransmisiones y drops (latencia que parece “lenta la app”)

cr0x@server:~$ ss -s
Total: 1252 (kernel 0)
TCP:   1023 (estab 812, closed 141, orphaned 0, timewait 141)

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  12        9         3
TCP	  882       740       142
INET	  894       749       145
FRAG	  0         0         0

Significado: Alta rotación de conexiones puede indicar reintentos/timeouts. Esta salida es sólo el aperitivo.

cr0x@server:~$ netstat -s | egrep 'retransmit|segments retransmited|packet receive errors|dropped'
    18432 segments retransmited
    92 packet receive errors
    1184 packets received, dropped

Significado: Retransmits y drops matan el rendimiento y son fábricas de latencia de cola.

Decisión: Si los retransmits suben durante la lentitud, trata la ruta de red como candidato: revisa estadísticas de NIC, colas y dispositivos upstream.

Tarea 11: Contadores a nivel NIC (demuéstralo: no es “la app”)

cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 0a:1b:2c:3d:4e:5f brd ff:ff:ff:ff:ff:ff
    RX:  bytes  packets  errors  dropped  missed   mcast
    9823412231  9123421       0      812       0       0
    TX:  bytes  packets  errors  dropped  carrier collsns
    11233412211 10233421      0      221       0       0

Significado: Drops en el host son inequívocos. Pueden deberse a overruns de ring buffer, problemas de qdisc, o simplemente demasiado tráfico.

Decisión: Si los drops suben, reduce la ráfaga (pacing), aumenta buffers con cuidado o arregla la congestión upstream; no sólo “añadas hilos”.

Tarea 12: Comprobar picos de latencia del filesystem por workloads con many syncs (ejemplo ext4)

cr0x@server:~$ sudo dmesg -T | egrep -i 'blocked for more than|I/O error|nvme|EXT4-fs' | tail -n 10
[Fri Jan 10 13:55:12 2026] INFO: task java:23120 blocked for more than 120 seconds.
[Fri Jan 10 13:55:12 2026] EXT4-fs (nvme0n1p2): Delayed block allocation failed for inode 98342 at logical offset 152343 with max blocks 4

Significado: El kernel te dice que procesos están bloqueados y a veces por qué. Esto puede implicar presión de allocation, writeback o stalls del dispositivo.

Decisión: Trata “blocked for more than” como un stall serio de I/O. Deja de hacer restarts a ciegas; encuentra la causa de I/O.

Tarea 13: ZFS: detectar presión de txg sync (culpa común de “por qué las escrituras son espasmódicas”)

cr0x@server:~$ sudo zpool iostat -v 1 3
              capacity     operations     bandwidth
pool        alloc   free   read  write   read  write
tank        1.20T  2.40T    820   2400  3.2M  26.8M
  mirror    1.20T  2.40T    820   2400  3.2M  26.8M
    nvme0n1     -      -    410   1200  1.6M  13.4M
    nvme1n1     -      -    410   1200  1.6M  13.4M

Significado: Bueno para ancho de banda y operaciones, pero no para latencia por sí solo. Si las escrituras parecen bien pero la latencia de la app es terrible, puedes estar atascado en escrituras sync, contención del SLOG, o ráfagas de txg sync.

Decisión: Si sospechas comportamiento de sync, revisa la carga y las propiedades del dataset a continuación.

cr0x@server:~$ sudo zfs get sync,logbias,compression,recordsize tank/app
NAME      PROPERTY     VALUE     SOURCE
tank/app  sync         standard  local
tank/app  logbias      latency   local
tank/app  compression  lz4       local
tank/app  recordsize   128K      local

Decisión: Si la app hace muchas escrituras sync pequeñas (bases de datos, journaling), valida el diseño de SLOG y si realmente se requieren semánticas sync. No pongas sync=disabled en producción a menos que disfrutes explicarle la pérdida de datos al equipo legal.

Tarea 14: Benchmarks seguros con fio (no te mientas)

cr0x@server:~$ sudo fio --name=randread --filename=/srv/app/.fiotest --size=2G --rw=randread --bs=4k --iodepth=1 --numjobs=1 --direct=1 --time_based --runtime=30 --group_reporting
randread: (groupid=0, jobs=1): err= 0: pid=31022: Fri Jan 10 14:01:02 2026
  read: IOPS=18.2k, BW=71.0MiB/s (74.4MB/s)(2131MiB/30001msec)
    lat (usec): min=55, max=3280, avg=68.11, stdev=22.50
    clat percentiles (usec):
     |  1.00th=[   58], 50.00th=[   66], 95.00th=[   82], 99.00th=[  120]

Significado: Esto mide latencia de lectura aleatoria 4k a QD=1, que a menudo coincide mejor con patrones reales de aplicaciones que “lectura secuencial grande”. Los percentiles muestran comportamiento de cola.

Decisión: Si tu app es sensible a latencia y fio muestra p99 subiendo bajo carga, has identificado un techo de latencia de almacenamiento. Planifica mitigación (cache, sharding, medios más rápidos, batching mejor) en lugar de tunear capas no relacionadas.

Tarea 15: Identificar la profundidad de cola del bloque y el scheduler

cr0x@server:~$ cat /sys/block/nvme0n1/queue/nr_requests
1024
cr0x@server:~$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq

Significado: nr_requests influye en cuán profundas pueden ser las colas. La elección de scheduler puede afectar la latencia de cola, la justicia y el rendimiento.

Decisión: Si la latencia de cola es el dolor, considera un scheduler que se comporte mejor bajo contención y reduce el encolamiento donde proceda. No copies sin probar; testea con tu carga.

Tarea 16: Detectar throttling (containers y cgroups: el cuello de botella invisible)

cr0x@server:~$ cat /sys/fs/cgroup/cpu.stat
usage_usec 92833423211
user_usec  81223311200
system_usec 11610112011
nr_periods  124112
nr_throttled 32110
throttled_usec 8823122210

Significado: Si nr_throttled y throttled_usec suben, tu carga está siendo CPU-throttled por cgroups. Parecerá “necesitamos más CPU” pero en realidad es “pusimos un límite”.

Decisión: Ajusta límites/requests de CPU, reduce la ráfaga o mueve la carga. No actualices hardware para compensar tus propias cuotas.

Broma #2 (corta, relevante): Si tu cuello de botella es “la base de datos”, felicidades—has descubierto la gravedad.

Hechos interesantes y un poco de historia (para que dejes de repetir errores viejos)

  • La ley de Amdahl (1967) formalizó por qué acelerar una parte del sistema tiene retornos decrecientes si el resto sigue en serie. Es la matemática detrás de “duplicamos CPU y ganamos 12%.”
  • La ley de Little (publicada 1961) se volvió una de las herramientas más prácticas para razonar sobre latencia y concurrencia en sistemas mucho antes de que tuviéramos observabilidad moderna.
  • La curva “utilización vs latencia” se enseñó en teoría de colas hace décadas; los incidentes modernos de “p99 se fue vertical” son esa curva, no un misterio.
  • La confusión con IOwait es antigua: Linux cuenta tareas en sleep no interrumpible en el load average. La gente aún se manda mensajes sobre “alto load” que en realidad es espera de disco.
  • Ethernet ha sido más rápida que muchas pilas de almacenamiento por años; en la práctica, la sobrecarga software (paths del kernel, TLS, serialización) a menudo bloquea antes que la línea.
  • NVMe mejoró la paralelización dramáticamente al ofrecer múltiples colas y baja sobrecarga respecto a SATA/AHCI, pero también facilitó ocultar latencia detrás de colas profundas—hasta que la latencia de cola te muerde.
  • “The Free Lunch Is Over” (mediados de 2000) no fue sólo sobre CPUs; obligó al software a confrontar cuellos de botella paralelos: locks, contención de caché y costes de coordinación.
  • Caches de escritura y barriers tienen larga historia de intercambiar durabilidad por velocidad. Muchas “victorias” catastróficas de rendimiento fueron solo ajustes de configuración que causaron pérdida de datos inadvertida.
  • La latencia de cola se volvió mainstream en grandes servicios web porque las medias no explicaban el dolor del usuario. El cambio de la industria a p95/p99 es una de las pocas mejoras culturales que podemos medir.

Tres mini-historias corporativas desde el terreno

Mini-historia 1: El incidente causado por una suposición errónea

El síntoma fue clásico: la latencia de la API se duplicó, luego se triplicó, y el error budget empezó a gritar. El equipo on-call vio altos load averages en los nodos de la app y declaró “CPU está saturada.” Escalaron el deployment y vieron… que nada mejoraba. Más pods, mismo dolor.

La suposición errónea fue sutil y común: trataron el load average como “uso de CPU”. En realidad, los nodos tenían CPU ociosa. El load estaba inflado por hilos atascados en sleep no interrumpible. Esperaban en disco.

La causa raíz vivía en otra capa: un cambio en la estrategia de logging de la aplicación. Una actualización “inofensiva” cambió logging buffered a escrituras síncronas por “correctitud de auditoría”. Los filesystems estaban en volúmenes respaldados por red con buen throughput pero latencia sync mediocre. Bajo pico de tráfico, cada request hacía al menos un fsync. La latencia se fue vertical porque el cuello de botella no era el ancho de banda; era la latencia de fsync y el encolamiento.

La solución no fue “más CPU.” Fue mover los logs de auditoría a una canalización asíncrona con buffering duradero y aislar I/O de logs de los threads de request. También añadieron un presupuesto duro en operaciones sync por ruta de request. El postmortem incluyó una nueva regla: si tocas fsync, debes anexar mediciones p99 de latencia en almacenamiento similar a producción.

Mini-historia 2: La optimización que salió mal

Un equipo de plataforma quería procesamientos por lotes más rápidos. Vieron que su job de ingest a object storage “subutilizaba la red” y decidieron aumentar la concurrencia: más workers, buffers más grandes, colas más profundas. El throughput subió en staging. Gente celebró. El cambio se desplegó un viernes por la tarde, porque claro que sí.

En producción, el job alcanzó mayor throughput—por un tiempo. Luego el resto de la flota empezó a sufrir. La latencia de cola de la API se disparó en servicios no relacionados. Se acumularon conexiones a la DB. Surgieron reintentos. El canal de incidentes se convirtió en un festival de capturas de pantalla.

Lo que pasó fue un ejemplo perfecto de optimizar una no-restricción hasta convertirla en restricción para todos. El job por lotes no sólo consumía más red; consumía recursos compartidos: buffers top-of-rack, colas NIC de host y, lo más importante, el presupuesto de peticiones del backend de almacenamiento. Servicios sensibles a latencia competían ahora con un job de fondo sin presión de retorno ni controles de equidad.

La solución final no fue “limitar el job a un hilo.” Fue implementar scheduling e aislamiento explícitos: shaping de red con cgroups, pools de nodos separados y rate limiting hacia el backend. También introdujeron concurrencia consciente de SLO: el job retrocede cuando el p99 de tráfico de clientes empeora. No es elegante. Es madurez.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una compañía con carga mixta—DBs, colas y un clúster de búsqueda—tenía la costumbre que parecía poco cool en reviews de arquitectura: cada trimestre, corrían tests controlados de carga en producción en ventana off-peak y registraban “puntos de knee” base para servicios clave. Mismos scripts. Mismas ventanas de medición. Mismos dashboards. Sin heroísmos.

Un martes, la latencia orientada al cliente subió. No fue un outage total, solo una hemorragia lenta: p99 pasó de “bien” a “la gente se queja”. El primer respondedor sacó el informe base de hace dos semanas y notó algo: el sistema alcanzaba el throughput knee con un 20% menos de tráfico que antes. Eso significaba que la capacidad había encogido efectivamente.

Porque tenían baselines, no perdieron tiempo debatiendo. Compararon iostat y PSI contra la base y vieron un patrón nuevo: mayor latencia de escritura y colas más profundas en un subconjunto específico de nodos. Esos nodos compartían la misma revisión de firmware en sus NVMe—actualizada recientemente como parte del “mantenimiento rutinario”. La actualización cambió el comportamiento de gestión de energía e introdujo picos de latencia bajo escrituras sostenidas.

Revirtieron el firmware en la flota afectada y el punto knee volvió. No hubo nueva arquitectura. No hubo olimpiadas de culpas. Solo una reversión calmada respaldada por evidencia. La acción del postmortem fue deliciosamente aburrida: fijar versiones de firmware y añadir una comprobación de regresión de latencia a los procedimientos de mantenimiento.

Errores comunes: síntoma → causa raíz → solución

1) Alto load average → “Necesitamos más CPU” → escalas y nada cambia

Síntoma: Load average alto, usuarios se quejan, CPU parece “ocupada” a primera vista.

Causa raíz: Tareas atascadas en I/O wait (sleep no interrumpible). El load las incluye.

Solución: Confirma con vmstat (b, wa), iostat -x, PSI I/O. Resuelve latencia de almacenamiento o reduce I/O sync en la ruta de request.

2) Disco %util al 100% → “El disco está al máximo” → compras discos más rápidos y aún ves picos de latencia

Síntoma: %util fijo, await alto, profundidad de cola grande.

Causa raíz: El disco puede estar bien; el patrón de carga es patológico (escrituras sync pequeñas, fsync storms, contención de journaling), o la cola es demasiado profunda y amplifica la latencia de cola.

Solución: Caracteriza tamaño de I/O, frecuencia de sync y concurrencia. Introduce batching, mueve fsync fuera de la ruta crítica, ajusta scheduler/profundidad de cola y valida con fio usando el patrón de I/O de la app.

3) “La red está bien, tenemos ancho de banda” → la latencia de cola sigue siendo terrible

Síntoma: Mbps de interfaz por debajo de la línea, pero timeouts y p99 largos.

Causa raíz: Drops/retransmits, bufferbloat o microbursts. Los gráficos de throughput mienten cuando paquetes se retransmiten.

Solución: Revisa netstat -s, ip -s link, stats de qdisc. Aplica pacing, fair queuing o reduce concurrencia en ráfaga.

4) “Optimizamos la cache hit rate” → la presión de memoria mata el rendimiento en silencio

Síntoma: Mayor uso de cache, pero picos de latencia y tiempo de sistema CPU aumenta.

Causa raíz: El crecimiento de cache provoca reclaim, page faults o stalls de compactación; el sistema pasa tiempo gestionando memoria en vez de servir requests.

Solución: Mide PSI de memoria, major faults y reclaim. Limita caches, redimensiónalos intencionalmente y deja margen para el kernel y el working set.

5) “Más hilos = más throughput” → el throughput estanca y la latencia explota

Síntoma: Aumenta la concurrencia, p99 empeora, CPU no se usa por completo.

Causa raíz: Saturaste un downstream compartido (DB, disco, lock o pool). Más concurrencia solo profundiza las colas.

Solución: Encuentra el recurso limitante, añade backpressure y reduce concurrencia hasta el punto knee. Usa shedding en lugar de espera infinita.

6) “Activamos compresión” → CPU está bien pero el throughput cae

Síntoma: Menos escrituras en disco, pero throughput de requests disminuye y la latencia de cola sube.

Causa raíz: La compresión desplaza el cuello de botella a CPU, ancho de banda de memoria o etapas mono-hilo de compresión; también cambia el patrón de I/O.

Solución: Haz benchmarks con datos representativos. Observa hotspots por core. Usa algoritmos más rápidos, ajusta tamaños de bloque/registro o comprime sólo caminos fríos.

7) “Desactivamos fsync para velocidad” → todo va rápido hasta que deja de hacerlo

Síntoma: Gran “mejora” de rendimiento tras un cambio de config; más tarde, corrupción o transacciones perdidas tras un crash.

Causa raíz: Se comerciaron las semánticas de durabilidad por latencia sin una decisión de negocio.

Solución: Restaura la durabilidad correcta, diseña buffering/logging adecuados y documenta requisitos de durabilidad. Si debes relajar durabilidad, hazlo explícita y visiblemente.

Listas de verificación / plan paso a paso

Paso a paso: establecer el límite real (tiempo tranquilo)

  1. Elige una carga de trabajo que importe (endpoint, job, consulta) y define éxito (p95/p99, throughput, tasa de errores).
  2. Traza la ruta crítica: cliente → servicio → downstreams (DB, cache, almacenamiento, red).
  3. Instrumenta colas: profundidad de worker pool, espera en DB pool, PSI del kernel, profundidad de cola de disco.
  4. Ejecuta una rampa de carga controlada: incrementa la carga ofrecida gradualmente; mantén estable en cada paso.
  5. Encuentra el knee: donde el throughput deja de escalar y la latencia de cola se acelera.
  6. Registra baselines: punto knee, p99, métricas de saturación y principales contribuyentes.
  7. Define guardrails: límites de concurrencia, timeouts, presupuestos de reintentos y backpressure.
  8. Reprueba después de cambios: deploys, actualizaciones de kernel/firmware, cambios de tipo de instancia, migraciones de almacenamiento.

Checklist de incidentes: evita thrash y conjeturas

  1. Detén la hemorragia: si la latencia explota, limita la concurrencia o habilita shedding. Protege el sistema primero.
  2. Confirma alcance: qué servicios, qué nodos, qué tenants, qué regiones.
  3. Revisa señales de error: timeouts, reintentos, retransmisiones TCP, errores de I/O.
  4. Revisa colas: longitud de colas de la app, espera en DB pool, run queue, profundidad de cola de disco, PSI.
  5. Elige una hipótesis de cuello de botella y recoge 2–3 mediciones que puedan falsarla.
  6. Cambia una cosa a la vez salvo que estés en contención de emergencia.
  7. Escribe la línea temporal mientras sucede. El tú del futuro es un testigo poco fiable.

Reglas de decisión (opinadas, porque estás ocupado)

  • Si p99 sube mientras el throughput se mantiene plano, estás encolando. Encuentra la cola, no añadas más trabajo.
  • Si los errores suben (timeouts/reintentos), trata el rendimiento como fiabilidad. Reduce carga; arregla la restricción después.
  • Si la latencia del dispositivo sube pero el ancho de banda es moderado, sospecha escrituras sync, writeback o firmware/gestión de energía.
  • Si la CPU está baja pero la latencia alta, sospecha esperas: I/O, locks, pools, retransmisiones de red, GC.
  • Si un core está pegado, deja de escalar horizontalmente y encuentra la sección serial o punto de contención.

Preguntas frecuentes

1) ¿Cómo digo rápido si estoy limitado por CPU o por I/O?

Usa vmstat 1 y iostat -x 1. Alto wa y tareas bloqueadas (b) apuntan a esperas por I/O. Alto r con bajo wa sugiere saturación de CPU o contención de ejecutables.

2) ¿Un alto %util de disco siempre es malo?

No. Puede significar que el dispositivo está ocupado (bien) o saturado con colas profundas (malo). Mira aqu-sz y await. Un disco ocupado con await bajo es saludable; uno ocupado con await alto es un cuello de botella.

3) ¿Por qué añadir hilos empeora la latencia aunque la CPU esté ociosa?

Porque probablemente estás saturando un downstream compartido (conexiones DB, cola de disco, lock o pool) y creas colas más largas. CPU ociosa no significa margen; puede significar que estás bloqueado en otro lugar.

4) ¿Cuál es la diferencia entre cuellos de botella de throughput y de latencia de cola?

Los cuellos de botella de throughput limitan el trabajo completado; los de latencia de cola arruinan la experiencia del usuario primero y pueden ocurrir mucho antes del throughput máximo. Los problemas de cola suelen ser causados por encolamiento, GC, jitter o picos de contención.

5) ¿Puede la cache “arreglar” un cuello de botella permanentemente?

La cache puede mover el cuello de botella al reducir trabajo, pero crea nuevos modos de falla: cache stampedes, presión de memoria y complejidad de consistencia. Trata la cache como un sistema de ingeniería con límites y backpressure, no como una manta mágica.

6) ¿Cómo mido el “punto knee” de forma segura en producción?

Haz una rampa de carga gradual durante una ventana de bajo riesgo, aisla tráfico de prueba si es posible y limita la concurrencia. Observa p95/p99, errores y métricas de saturación. Detente cuando los errores o la latencia de cola empiecen a acelerarse.

7) ¿Por qué mis benchmarks se ven geniales pero producción es lenta?

Tu benchmark probablemente mide lo equivocado: I/O secuencial en lugar de aleatorio, QD=32 en lugar de QD=1, cache caliente vs cache fría, o ignora semánticas de sync. Además, producción tiene contención y noisy neighbors. Benchmaquea la carga que realmente ejecutas.

8) ¿Cuándo escalo vs optimizo?

Escala cuando tienes saturación clara de un recurso que escala linealmente con capacidad (CPU para cómputo paralelo, ancho de banda para streaming). Optimiza cuando el cuello de botella es coordinación, latencia de cola o contención; el hardware rara vez arregla eso.

9) ¿Cuál es la forma más rápida de evitar la histeria por cuellos de botella en un equipo?

Pónganse de acuerdo en un pequeño conjunto de mediciones estándar y un orden de triage. Haz que la gente traiga evidencia: “iostat muestra 20ms write await y cola de 40” vence a “se siente como disco.” Escríbelo en la plantilla de incidentes.

Próximos pasos que realmente puedes hacer

Si quieres menos fuegos de artificio por rendimiento, deja de tratar los cuellos de botella como folclore. Trátalos como restricciones que puedes medir.

  1. Elige un viaje de usuario crítico y instrumenta percentiles de latencia, errores y concurrencia.
  2. Añade métricas de colas (colas de la app, pools, PSI, colas de dispositivos). Los cuellos de botella se esconden en la espera, no en el trabajo.
  3. Captura un punto knee base bajo carga controlada. Guárdalo en el runbook. Actualízalo tras cambios significativos.
  4. Durante incidentes, ejecuta el guion de diagnóstico rápido y recoge evidencia falsable antes de cambiar cosas.
  5. Implementa backpressure (límites de concurrencia, presupuestos de reintentos, timeouts). Un sistema con backpressure falla de forma predecible en vez de teatral.

El límite real nunca es un misterio. Sólo se esconde detrás de tus suposiciones, esperando que lo midas.

← Anterior
IPC > GHz: la explicación más simple que realmente recordarás
Siguiente →
Contextos Docker: Gestiona varios hosts como un profesional

Deja un comentario