Ya lo has visto: el servicio recibió “más CPU” y de alguna manera se volvió más lento. O el proveedor prometió “el doble de núcleos”,
y tu latencia p99 se encogió de hombros como si tuviera sueldo fijo. Mientras tanto, tus paneles muestran suficiente margen—
excepto que los usuarios están actualizando la página, el canal de incidentes está activo y el on-call tiene esa mirada distante de mil yardas.
El punto de inflexión no fue el día en que las CPU ganaron núcleos extra. Fue el día en que aprendimos colectivamente—muchas veces por las malas—que
los relojes dejaron de salvarnos. A partir de ese momento, el rendimiento pasó a ser una disciplina de ingeniería en lugar de una compra.
El punto de inflexión: de relojes a núcleos
Durante mucho tiempo, el trabajo de rendimiento fue básicamente un flujo de compras. Comprabas la siguiente generación de CPU, funcionaba a
una frecuencia más alta y tu aplicación mejoraba mágicamente—aun si tu código era una pieza de museo. Esa era terminó cuando el calor y
la energía se convirtieron en el factor limitante, no el conteo de transistores.
Una vez que el crecimiento de frecuencia se frenó, “más rápido” se convirtió en “más paralelo”. Pero el paralelismo no es una comida gratis; es una factura que
llega mensualmente, detallada en contención de locks, fallos de caché, ancho de banda de memoria, sobrecarga del planificador, latencias de cola y
“¿por qué este hilo está al 100%?”.
Aquí está la verdad incómoda: los núcleos no vencen a los relojes por defecto. Vencen a los relojes solo cuando tu software y tu
modelo operativo pueden explotar la concurrencia sin ahogarse en costes de coordinación.
Broma seca #1: Añadir núcleos a una app con muchas locks es como abrir más cajas de pago manteniendo un solo cajero que insiste en validar cada cupón personalmente.
El “verdadero punto de inflexión” es cuando dejas de tratar la CPU como un escalar y comienzas a tratar la máquina como un sistema:
CPU y memoria y almacenamiento y red y planificación del kernel. En producción, esos
subsistemas no esperan su turno educadamente.
Hechos y contexto que vale la pena recordar
Estos son puntos cortos y concretos que te evitan repetir la historia. No son trivialidades. Son anclas.
-
La escalada de frecuencia chocó contra un muro a mediados de los 2000 cuando la densidad de potencia y la disipación de calor hicieron de “subir el reloj”
un problema de fiabilidad y empaquetado, no solo un reto de ingeniería. -
“Dennard scaling” dejó de ser tu amigo: al reducirse los transistores, el voltaje no siguió bajando al mismo ritmo,
así que la potencia por área aumentó y forzó elecciones conservadoras de frecuencia. -
Multi-core no fue un lujo; fue una solución alternativa. Si no puedes aumentar la frecuencia de forma segura, añades unidades de ejecución paralelas.
Entonces transfieres la complejidad al software. -
La Ley de Amdahl se volvió una realidad operacional: la fracción serial de tu carga determina tu techo de escalado,
y el tráfico de producción ama encontrar el camino serial. -
La jerarquía de caché se volvió un factor de primer orden. El comportamiento L1/L2/L3, el tráfico de coherencia de caché y los efectos NUMA
dominan rutinariamente los gráficos de “uso de CPU”. -
La ejecución especulativa y las técnicas out-of-order compraron rendimiento sin frecuencias más altas, pero también aumentaron la complejidad y
la superficie de vulnerabilidad; las mitigaciones posteriores cambiaron perfiles de rendimiento de forma medible. -
La virtualización y los contenedores cambiaron el significado de “un núcleo”. El scheduling de vCPU, steal time, throttling de CPU y vecinos ruidosos
pueden hacer que un nodo de 32 núcleos se sienta como un portátil cansado. -
El almacenamiento se volvió más rápido, pero la latencia siguió siendo testaruda. NVMe mejoró mucho, sin embargo la diferencia entre “rápido” y “lento” suele ser el encolamiento,
el comportamiento del sistema de archivos y la semántica de sync—no las especificaciones brutas del dispositivo.
Qué cambió realmente en los sistemas de producción
1) “CPU” dejó de ser el cuello de botella y se volvió el mensajero
En la era del escalado por reloj, la utilización de CPU era un proxy razonable para “estamos ocupados”. En la era de los núcleos, la CPU suele ser solo donde
notas el síntoma: un hilo girando en un lock, una pausa de GC, una ruta del kernel que hace demasiado trabajo por paquete, o una tormenta de syscalls
causada por I/O pequeño.
Si tratas la CPU alta como el problema, afinarás lo incorrecto. Si tratas la CPU como el mensajero, preguntarás:
¿qué trabajo se está haciendo, en nombre de quién y por qué ahora?
2) La latencia de cola se volvió la métrica que importa
Los sistemas paralelos son excelentes produciendo medias que se ven bien mientras los usuarios sufren. Cuando ejecutas muchas solicitudes concurrentes,
las más lentas—poseedores de locks, rezagados, cachés frías, misses NUMA, picos en colas de disco—definen la experiencia del usuario y los timeouts.
Los núcleos amplifican la concurrencia; la concurrencia amplifica el encolamiento; el encolamiento amplifica el p99.
Puedes entregar un sistema “más rápido” que sea peor, si tus optimizaciones aumentan la varianza. En otras palabras: el rendimiento gana demostraciones; la estabilidad evita paginazos.
3) El planificador del kernel se volvió parte de tu arquitectura de aplicación
Con más núcleos, el scheduler tiene más opciones—y más oportunidades para dañarte. La migración de hilos puede destrozar cachés.
Una mala colocación de IRQ puede robar ciclos a tus hilos calientes. Cgroups y cuotas de CPU pueden introducir throttling
que parece “latencia misteriosa”.
4) La memoria y NUMA dejaron de ser “temas avanzados”
Cuando tienes múltiples sockets, la memoria no es solo RAM; es RAM local frente a RAM remota.
Un hilo en el socket 0 leyendo memoria asignada en el socket 1 puede sufrir una penalización medible, y esa penalización se compone cuando estás
saturando el ancho de banda de memoria. Tu código podría ser “limitado por CPU” hasta que se vuelve “limitado por memoria”, y no lo notarás
mirando solo la utilización de CPU.
5) El rendimiento del almacenamiento pasó a ser más sobre coordinación que sobre dispositivos
Los sistemas de almacenamiento son paralelos también: colas, merges, readahead, writeback, journaling, copy-on-write, checksums.
El dispositivo puede ser rápido mientras el sistema es lento porque creaste la tormenta perfecta de escrituras sincrónicas pequeñas,
contención de metadata o amplificación de escritura.
Como ingeniero de almacenamiento, diré la parte silenciosa en voz alta: para muchas cargas, el sistema de archivos es la primera dependencia de rendimiento de tu base de datos.
Trátalo con el mismo respeto que tu planificador de consultas.
Una cita que se mantiene en operaciones:
La esperanza no es una estrategia.
— General Gordon R. Sullivan
Cuellos de botella: dónde se esfuma “más núcleos”
Trabajo serial: Amdahl cobra su renta
Todo sistema tiene una fracción serial: locks globales, líder único, un hilo de compactación, un escritor de WAL, un coordinador de shard,
un mutex del kernel en una ruta caliente. Tu recuento de núcleos brillante mayormente aumenta el número de hilos esperando su turno.
Regla de decisión: si añadir concurrencia mejora el throughput pero empeora la latencia, probablemente chocaste contra un punto de estrangulamiento serial más encolamiento.
No necesitas más núcleos. Necesitas reducir la contención o fragmentar el recurso serial.
Contención de locks y estado compartido
El estado compartido es el impuesto clásico de la concurrencia: mutexes, rwlocks, atómicos, allocators globales, conteo de referencias, pools de conexiones,
y “solo un pequeño lock de métricas”. A veces el lock no está en tu código; está en el runtime, el allocator de libc, el kernel
o el sistema de archivos.
Ancho de banda de memoria y coherencia de caché
Las CPU modernas son rápidas en aritmética. Son más lentas esperando memoria. Añade núcleos y aumentas el número de bocas hambrientas
compitiendo por el ancho de banda de memoria. Entonces aparece el tráfico de coherencia de caché: los núcleos pasan tiempo acordando el significado de una línea de caché
en lugar de hacer trabajo útil.
NUMA: cuando la “RAM” tiene geografía
Los problemas NUMA a menudo parecen aleatorios: misma solicitud, distinta latencia, dependiendo de qué núcleo la ejecutó y dónde vive su memoria.
Si no haces pinning, asignas localmente o eliges una configuración consciente de la topología, obtendrás “deriva de rendimiento” que viene y va.
Tiempo del kernel: syscalls, switches de contexto e interrupciones
Tasas altas de cambios de contexto pueden borrar las ganancias del paralelismo. Muchas syscalls por I/O pequeño o logging parlanchín pueden hacer que un servicio
consuma CPU sin hacer trabajo de negocio. Interrupciones mal colocadas pueden fijar la carga IRQ de una cola NIC entera en los mismos núcleos que ejecutan tus
hilos sensibles a la latencia.
I/O de almacenamiento: encolamiento y amplificación de escrituras
Los núcleos no ayudan si estás atascado en patrones fsync sincrónicos, escrituras aleatorias pequeñas, o una pila de almacenamiento que amplifica escrituras
mediante copy-on-write y actualizaciones de metadata. Peor aún: más núcleos pueden emitir más I/O concurrente, aumentando la profundidad de cola y la latencia.
Redes: procesamiento de paquetes y tiempo de softirq
Si estás manejando PPS alto, el cuello de botella puede ser el procesamiento softirq, conntrack, reglas de iptables o TLS. La CPU no está “ocupada”
con tu código; está ocupada siendo la asistente de la tarjeta de red.
Broma seca #2: Lo único que escala linealmente en mi carrera es el número de paneles que afirman que todo está bien.
Manual de diagnóstico rápido
Este es el orden que te lleva al cuello de botella rápidamente sin una semana de danza interpretativa en Grafana. El objetivo no es ser
ingenioso; es ser rápido y correcto.
Primero: establece qué está saturando (CPU vs memoria vs I/O vs red)
- Comprueba load average frente a hilos ejecutables y espera de I/O.
- Comprueba el desglose de CPU (user/system/iowait/steal) y el throttling.
- Comprueba la latencia de disco y la profundidad de cola; confirma si las esperas se correlacionan con p99.
- Comprueba pérdidas/retransmisiones en NIC y tiempo de CPU en softirq si el servicio depende de la red.
Segundo: determina si el límite es serial, compartido o externo
- Un hilo al 100% mientras otros están inactivos: trabajo serial o un lock caliente.
- Todos los núcleos moderadamente ocupados pero p99 malo: encolamiento, contención o stalls de memoria.
- CPU baja pero latencia alta: I/O o dependencia downstream.
- CPU alta en kernel: redes, syscalls, sistema de archivos o interrupciones.
Tercero: valida con perfilado dirigido (no con intuiciones)
- Usa
perf top/perf recordpara rutas calientes de CPU. - Usa flame graphs si puedes, pero hasta trazas de pila en el momento correcto ayudan.
- Usa
pidstatpara CPU por hilo y cambios de contexto. - Usa
iostaty estadísticas del sistema de archivos para la distribución de latencia de I/O y saturación.
Cuarto: cambia una variable, mide, revierte rápido
- Reduce la concurrencia y observa si la cola mejora (diagnóstico de encolamiento).
- Haz pin de hilos / ajusta la afinidad de IRQ si dominan efectos de caché y scheduler.
- Cambia la estrategia sync con cuidado (batch fsync, group commit) si es seguro.
- Escala horizontalmente cuando hayas demostrado que no es un límite de coordinación en un solo nodo.
Tareas prácticas con comandos: medir, interpretar, decidir
Estas son tareas deliberadamente “ejecutables a las 03:00”. Cada una incluye: comando, salida de ejemplo, qué significa y la decisión que tomas.
Úsalas en orden cuando estés perdido.
Task 1: Check CPU saturation and iowait quickly
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (db01) 01/09/2026 _x86_64_ (32 CPU)
01:12:01 PM CPU %usr %nice %sys %iowait %irq %soft %steal %idle
01:12:02 PM all 42.10 0.00 12.40 8.60 0.00 2.10 0.20 34.60
01:12:02 PM 7 98.00 0.00 1.00 0.00 0.00 0.00 0.00 1.00
01:12:02 PM 12 10.00 0.00 40.00 30.00 0.00 5.00 0.00 15.00
Significado: La CPU 7 está esencialmente saturada en espacio de usuario (probablemente un hilo caliente). La CPU 12 pasa mucho tiempo en sys + iowait (kernel + esperas de almacenamiento).
Decisión: Si una CPU está al máximo, busca un cuello de botella de un solo hilo o un lock. Si iowait es alto, pasa a chequear la latencia de disco antes de afinar la CPU.
Task 2: Separate runnable load from I/O load
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
3 1 0 421312 91240 812340 0 0 1024 2048 5400 9800 41 12 35 9 3
9 0 0 418900 91300 811900 0 0 120 250 6200 21000 55 15 25 2 3
Significado: r muestra hilos ejecutables; b muestra bloqueados (a menudo I/O). La primera línea indica algo de bloqueo, la segunda indica presión de CPU (r=9).
Un cs alto (cambios de contexto) apunta a contención o concurrencia excesivamente habladora.
Decisión: Si r está consistentemente > número de CPUs, estás limitado por CPU o thrashing. Si b aumenta, prioriza investigación de I/O.
Task 3: Spot CPU throttling in containers (cgroups)
cr0x@server:~$ cat /sys/fs/cgroup/cpu.stat
usage_usec 9034211123
user_usec 7011200456
system_usec 2023010667
nr_periods 120934
nr_throttled 23910
throttled_usec 882134223
Significado: La carga ha sido throttled en 23,910 periodos; se acumularon casi 882 segundos de tiempo throttled.
Esto puede parecer “latencia aleatoria” y “CPU ociosa” simultáneamente.
Decisión: Si el throttling es notable durante incidentes, aumenta límites/requests de CPU, arregla vecinos ruidosos o deja de asumir que “ocioso significa disponible.”
Task 4: Check steal time on virtual machines
cr0x@server:~$ sar -u 1 3
Linux 6.5.0 (app03) 01/09/2026 _x86_64_ (8 CPU)
01:18:01 PM CPU %user %system %iowait %steal %idle
01:18:02 PM all 22.10 7.30 1.20 18.40 51.00
01:18:03 PM all 24.00 8.10 1.10 19.20 47.60
Significado: ~19% de steal es el hipervisor quitando tiempo; no puedes optimizar eso desde dentro del guest.
Decisión: Si el steal es alto, migra hosts, cambia el tipo de instancia o reduce la contención en la capa de virtualización.
Task 5: Identify per-thread CPU hogs and context switch storms
cr0x@server:~$ pidstat -t -p 2147 1 3
Linux 6.5.0 (api01) 01/09/2026 _x86_64_ (32 CPU)
01:21:10 PM UID TGID TID %usr %system %CPU cswch/s nvcswch/s Command
01:21:11 PM 1001 2147 2159 98.00 1.00 99.00 0.00 12.00 java
01:21:11 PM 1001 2147 2164 5.00 18.00 23.00 12000.00 8000.00 java
Significado: Un hilo está limitado por CPU (probablemente un bucle caliente o cuello de botella serial). Otro está cargado en tiempo de sistema y switching—a menudo contención de locks, syscalls o churn del scheduler.
Decisión: Hilo limitado por CPU: perfila con perf. Switching alto: inspecciona locks, comportamiento del allocator, logging y hotspots del kernel.
Task 6: Find CPU hotspots with perf (fast triage)
cr0x@server:~$ sudo perf top -p 2147
Samples: 2K of event 'cycles', Event count (approx.): 2289012345
38.12% libpthread-2.35.so [.] pthread_mutex_lock
14.55% libc-2.35.so [.] __memmove_avx_unaligned_erms
10.09% [kernel] [k] tcp_recvmsg
7.44% [kernel] [k] ext4_da_write_end
Significado: Una gran porción en pthread_mutex_lock es una firma de contención. Hotspots del kernel sugieren que la recepción de red y las rutas de escritura del sistema de archivos también son relevantes.
Decisión: Si el lock domina, reduce el estado compartido, aumenta el sharding o cambia el modelo de concurrencia. Si la red del kernel domina, revisa softirq/IRQ y tasas de paquetes.
Task 7: Confirm disk latency and queue depth
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (db01) 01/09/2026 _x86_64_ (32 CPU)
Device r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
nvme0n1 120.0 950.0 4800 81200 154.0 9.80 11.2 3.1 12.3 0.7 82.0
Significado: Alto avgqu-sz y await elevado indican encolamiento. %util en 82% sugiere que el dispositivo está ocupado; la latencia subirá bajo ráfagas.
Decisión: Si await crece con la carga, reduce la presión de escrituras sincrónicas, agrupa escrituras, afina el sistema de archivos/ZFS o mueve datos/logs calientes a dispositivos más rápidos o dedicados.
Task 8: See which processes are generating I/O
cr0x@server:~$ sudo iotop -o -b -n 3
Total DISK READ: 0.00 B/s | Total DISK WRITE: 58.23 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
2147 be/4 app 0.00 B/s 22.10 M/s 0.00 % 12.50 % java -jar api.jar
1762 be/4 postgres 0.00 B/s 31.90 M/s 0.00 % 18.00 % postgres: wal writer
Significado: El WAL writer está generando escrituras significativas; tu aplicación también escribe mucho. Si los problemas de latencia se correlacionan, puedes estar limitado por sync o journaling.
Decisión: Valida los patrones de fsync y la configuración de almacenamiento. Considera mover WAL/logs a un dispositivo separado, o afinar ajustes de commit según requisitos de durabilidad.
Task 9: Check filesystem and mount options (the boring truth)
cr0x@server:~$ findmnt -no SOURCE,TARGET,FSTYPE,OPTIONS /var/lib/postgresql
/dev/nvme0n1p2 /var/lib/postgresql ext4 rw,noatime,data=ordered
Significado: noatime evita escrituras extra de metadata. Ext4 en modo ordered es generalmente sensato para bases de datos en Linux.
Decisión: Si ves opciones sorprendentes (como sync o barreras extrañas deshabilitadas sin razón), arréglalas. No sigas a ciegas flags de rendimiento.
Task 10: Check ZFS pool health and latency pressure (if you run it)
cr0x@server:~$ sudo zpool iostat -v tank 1 3
capacity operations bandwidth
pool alloc free read write read write
---------- ----- ----- ----- ----- ----- -----
tank 1.20T 2.30T 210 1800 8.20M 95.1M
mirror 1.20T 2.30T 210 1800 8.20M 95.1M
nvme1n1 - - 110 920 4.10M 47.6M
nvme2n1 - - 100 880 4.10M 47.5M
Significado: Muchas operaciones de escritura en relación con el ancho de banda implican escrituras pequeñas. Los mirrors pueden manejarlo, pero la latencia depende del comportamiento de sync y de la presencia de SLOG.
Decisión: Si las escrituras sincrónicas pequeñas dominan, evalúa SLOG separado, recordsize y agrupamiento de fsync—sin comprometer la durabilidad.
Task 11: Detect memory pressure and reclaim thrash
cr0x@server:~$ sar -B 1 3
Linux 6.5.0 (cache01) 01/09/2026 _x86_64_ (16 CPU)
01:33:20 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
01:33:21 PM 0.0 81234.0 120000.0 12.0 90000.0 0.0 54000.0 39000.0 72.2
Significado: Escaneo intensivo de páginas y pgpgout alto sugieren presión de reclaim; fallos mayores indican paging basado en disco.
Decisión: Si el reclaim está activo durante picos de latencia, reduce la huella de memoria, ajusta el tamaño de caches o muévete a nodos con más RAM. Más núcleos no ayudarán.
Task 12: Check NUMA locality problems
cr0x@server:~$ numastat -p 2147
Per-node process memory usage (in MBs) for PID 2147 (java)
Node 0 18240.3
Node 1 2240.8
Total 20481.1
Significado: La memoria está fuertemente concentrada en el Nodo 0; si los hilos se ejecutan en ambos sockets, los hilos del Nodo 1 accederán con frecuencia a memoria remota.
Decisión: Si el desbalance NUMA se correlaciona con latencia, considera fijar el proceso a un socket, habilitar asignación consciente de NUMA o afinar la colocación de hilos.
Task 13: Check interrupt distribution and softirq load
cr0x@server:~$ cat /proc/interrupts | head -n 8
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
24: 9123401 102332 99321 88210 90111 93321 92011 88712 PCI-MSI 524288-edge eth0-TxRx-0
25: 10231 8231201 99221 88120 90211 93411 92101 88602 PCI-MSI 524289-edge eth0-TxRx-1
NMI: 2012 1998 2001 2003 1999 2002 1997 2004 Non-maskable interrupts
Significado: Los IRQs están concentrados en CPU0 y CPU1 para colas separadas. Eso no siempre es malo, pero si tus hilos calientes comparten esas CPUs, tendrás jitter.
Decisión: Si hilos sensibles a latencia y CPUs con mucha carga IRQ se solapan, ajusta la afinidad de IRQ para aislarlos, o mueve hilos de la app lejos de los núcleos IRQ.
Task 14: Confirm TCP retransmits and drops (network-induced latency)
cr0x@server:~$ netstat -s | egrep -i 'retrans|segments retransmited|listen drops|RTO' | head
124567 segments retransmited
98 timeouts after RTO
1428 SYNs to LISTEN sockets dropped
Significado: Retransmisiones y RTOs crean latencia de cola que parece “la app se volvió más lenta”. Los SYN drops pueden parecer fallos de conexión aleatorios bajo carga.
Decisión: Si las retransmisiones aumentan durante incidentes, revisa saturación de NIC, configuraciones de cola, salud del load balancer, conntrack y pérdida de paquetes upstream.
Task 15: Measure file descriptor pressure (hidden serialization)
cr0x@server:~$ cat /proc/sys/fs/file-nr
24576 0 9223372036854775807
Significado: El primer número son manejadores de archivos asignados; cerca de los límites verás fallos y reintentos que crean patrones extraños de contención.
Decisión: Si te acercas a límites, aumenta y arregla fugas. No dejes que la escasez de descriptores de archivo se disfrace de problema de escalabilidad de CPU.
Task 16: Spot one-core bottlenecks in application metrics using top
cr0x@server:~$ top -H -p 2147 -b -n 1 | head -n 12
top - 13:41:01 up 12 days, 3:22, 1 user, load average: 6.20, 5.90, 5.10
Threads: 98 total, 2 running, 96 sleeping, 0 stopped, 0 zombie
%Cpu(s): 45.0 us, 12.0 sy, 0.0 ni, 35.0 id, 8.0 wa, 0.0 hi, 0.0 si, 0.0 st
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2159 app 20 0 9856.2m 3.1g 41224 R 100.0 9.8 32:11.20 java
2164 app 20 0 9856.2m 3.1g 41224 S 23.0 9.8 10:05.88 java
Significado: Un hilo ocupando un núcleo al máximo es el ejemplo perfecto de “los núcleos no ayudan”. Ese hilo es tu techo de throughput y tu acantilado de latencia.
Decisión: Perfílalo y luego rediseña: divide el trabajo, reduce el alcance del lock, fragmenta el estado o mueve el trabajo fuera del camino de la solicitud.
Tres mini-historias corporativas (anonimizadas, plausibles y técnicamente precisas)
Mini-historia #1: Un incidente causado por una suposición equivocada
Una compañía SaaS mediana movió su capa API de instancias antiguas de 8 núcleos a instancias nuevas de 32 núcleos. Misma memoria. Misma clase de almacenamiento.
El plan de migración fue simple: reemplazar nodos, mantener los mismos umbrales de autoescalado, disfrutar del menor coste por núcleo.
El primer día laborable después del corte, las tasas de error aumentaron lentamente. No catastróficamente—lo suficiente para activar reintentos de clientes.
La latencia subió, luego se estabilizó, luego subió de nuevo. Los paneles decían que la CPU estaba “bien”: 40–55% en la flota.
El comandante del incidente hizo la pregunta clásica: “¿Por qué estamos lentos si la CPU está solo a la mitad?”
La suposición equivocada fue pensar que el porcentaje de CPU mapea linealmente a capacidad. Lo que pasó fue más sutil: la aplicación tenía un
lock global protegiendo una estructura de caché usada en cada solicitud. En cajas de 8 núcleos el lock era molesto. En cajas de 32 núcleos se volvió
un festival de contención. Más núcleos significaron más contendientes simultáneos, mayores tasas de cambios de contexto y más tiempo de espera por solicitud.
El throughput no subió; la latencia de cola sí.
La solución no fue “añadir más CPU.” La solución fue fragmentar la caché por keyspace y usar striping de locks. También redujeron temporalmente el
límite de concurrencia en el ingreso—menos throughput en papel, mejor p99 en realidad—hasta que se desplegó el cambio de código.
La lección que quedó: los núcleos amplifican tanto tu paralelismo como tus costes de coordinación. Si no mides la contención, estás adivinando.
Y adivinar es cómo te llaman de noche.
Mini-historia #2: Una optimización que salió mal
Un equipo de plataforma de datos quería reducir la latencia de escritura para un servicio de ingestión. Notaron que su base de datos pasaba tiempo en flush y sync.
Alguien propuso un “win rápido”: mover logs y WAL al mismo volumen NVMe rápido usado para datos, y aumentar hilos de trabajadores para “usar todos los núcleos.”
El cambio se desplegó gradualmente, con una pequeña prueba que se vio bien: mayor throughput, menor latencia media.
Dos semanas después, durante un pico de tráfico predecible, el sistema empezó a hacer timeouts. No de forma uniforme—suficiente para doler. Los gráficos del nodo mostraban
%util de disco subiendo. iostat mostró la profundidad de cola aumentando. La CPU permaneció disponible. Los ingenieros aumentaron los pools de hilos para “empujar más”.
Eso lo empeoró.
El contragolpe fue un colapso por encolamiento: más hilos generaron más escrituras sincrónicas pequeñas, lo que aumentó la profundidad de cola del dispositivo, lo que aumentó
la latencia por operación, lo que aumentó el número de operaciones concurrentes en vuelo, lo que volvió a aumentar la profundidad de cola. Un bucle de retroalimentación.
El throughput medio se veía aceptable, pero el p99 explotó porque algunas solicitudes quedaron detrás de largas colas de I/O.
La solución fue deliberadamente aburrida: limitar la concurrencia, separar dispositivos WAL/log, e implementar batching para agrupar llamadas fsync.
También añadieron alertas en await y profundidad de cola, no solo en throughput. Después de eso, el sistema manejó picos sin drama.
La lección: “usar todos los núcleos” no es un objetivo. Es un riesgo. La concurrencia es un dial, y el ajuste correcto depende del recurso compartido más lento.
Mini-historia #3: Una práctica aburrida pero correcta que salvó el día
Una fintech ejecutaba un servicio de liquidaciones con muchas lecturas, escrituras moderadas y durabilidad estricta para un subconjunto de operaciones.
Tenían la costumbre que no gana premios en hackathons: cada trimestre ejecutaban un ensayo de capacidad y modos de fallo con carga parecida a producción,
con runbooks estrictos y una regla de “no heroísmos”.
Durante uno de los ensayos, notaron algo poco sexy: la latencia p99 iba en aumento al acercarse al throughput pico, aun cuando la CPU parecía bien.
Recogieron pidstat, iostat y perfiles de perf y encontraron contención leve de locks más un aumento de profundidad de cola de almacenamiento
durante ráfagas periódicas tipo checkpoint. Nada “roto”, solo cerca del acantilado.
Hicieron dos cambios: (1) fijaron pools de trabajadores específicos a CPUs lejos de los núcleos IRQ de la NIC, y (2) ajustaron el layout de almacenamiento para que el log durable
viviera en un dispositivo separado con latencia predecible. También establecieron SLOs explícitos en p99 y añadieron alertas en throttling y steal time.
Meses después, un pico real de tráfico golpeó durante un evento de vecino ruidoso en la capa de virtualización. Sus sistemas aún degradaron,
pero se mantuvieron dentro del SLO lo suficiente para descargar carga de forma ordenada. Otros equipos tuvieron incidentes; este equipo tuvo un hilo en Slack
y un informe postmortem sin adrenalina.
La lección: las prácticas aburridas son las que hacen que los núcleos sean utilizables. Ensaya, mide las cosas correctas y verás el acantilado antes de que te caigas.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: CPU está “solo al 50%” pero la latencia es terrible
Causa raíz: cuello de botella de un solo hilo, contención de locks o throttling de cgroup que enmascara una saturación real.
Solución: Usa top -H/pidstat -t para encontrar el hilo caliente; usa perf top para hallar locks o bucles calientes.
Revisa /sys/fs/cgroup/cpu.stat por throttling. Rediseña el camino serial; no solo añadas instancias.
2) Síntoma: El throughput aumenta con más hilos, luego de repente colapsa
Causa raíz: colapso por encolamiento en I/O o dependencia downstream; la concurrencia sobrepasa la región estable de operación del servicio.
Solución: Limita la concurrencia, añade backpressure y mide profundidad de cola/await. Ajusta los pools de hilos hacia abajo hasta que p99 se estabilice.
3) Síntoma: Picos aleatorios de p99 después de mover a máquinas multi-socket más grandes
Causa raíz: efectos NUMA y problemas de localidad de caché; los hilos migran y acceden a memoria remota.
Solución: Revisa numastat. Fija procesos o usa allocators conscientes de NUMA. Mantén servicios críticos de latencia dentro de un socket cuando sea posible.
4) Síntoma: El tiempo de sistema de la CPU sube con el tráfico, pero el código de la app no cambió
Causa raíz: syscalls y sobrecarga del kernel por networking, I/O pequeño, logging o churn de metadata del sistema de archivos.
Solución: Usa perf top para ver símbolos del kernel, revisa la distribución de interrupciones, reduce la tasa de syscalls (batch, buffer, async),
y reevalúa el volumen de logging y políticas de flush.
5) Síntoma: Alto iowait y “discos rápidos”
Causa raíz: encolamiento del dispositivo, patrones de escritura sincrónica, amplificación de escritura (copy-on-write, bloques pequeños) o compartir un dispositivo con otra carga.
Solución: Confirma con iostat -x y iotop. Separa WAL/logs, agrupa fsync, ajusta record size del sistema de archivos,
y asegura que el dispositivo subyacente no esté sobresuscrito.
6) Síntoma: Escalar horizontalmente añade nodos pero no capacidad
Causa raíz: dependencia centralizada (escritor DB único, hotspot de elección de líder, caché compartida, downstream con límite).
Solución: Identifica la dependencia compartida, fragmenta o replica correctamente y asegúrate de que los clientes distribuyan la carga equitativamente.
“Más pods estateless” no arreglarán un cuello de botella stateful.
7) Síntoma: El rendimiento empeora después de “hacerlo más concurrente”
Causa raíz: aumento de contención y tráfico de coherencia de caché; más hilos causan más escrituras compartidas y false sharing.
Solución: Reduce el estado compartido, evita contadores atómicos calientes en el camino de la solicitud, usa batching por núcleo/hilo y perfila la contención.
Listas de verificación / plan paso a paso
Paso a paso: probar si los núcleos ayudarán
-
Mide la latencia de cola bajo carga (p95/p99) y correlaciónala con el desglose de CPU (usr/sys/iowait/steal).
Si p99 empeora mientras la CPU está “disponible”, sospecha contención o esperas externas. -
Encuentra el hilo o lock limitante:
ejecutatop -Hypidstat -tpara localizar hilos calientes y tormentas de switching. -
Perfila antes de afinar:
usaperf toppara identificar funciones principales (locks, memcpy, syscalls, rutas del kernel). -
Comprueba latencia de I/O y profundidad de cola:
iostat -xyiotoppara confirmar si el almacenamiento es el elemento que marca el ritmo. -
Revisa límites artificiales de CPU:
el throttling de cgroup, steal time y restricciones del scheduler pueden imitar “mal código”. -
Valida NUMA y colocación de IRQ:
confirma la localidad connumastat, confirma la distribución de interrupciones con/proc/interrupts. -
Sólo entonces decide:
- Si la CPU está realmente saturada en user time a través de núcleos: más núcleos (o núcleos más rápidos) pueden ayudar.
- Si la contención domina: rediseña la concurrencia; núcleos extra pueden empeorarla.
- Si I/O domina: arregla la ruta de almacenamiento; más núcleos solo generarán más esperas.
- Si la red/kernel domina: ajusta IRQ, offloads y la ruta de paquetes; considera núcleos más rápidos.
Lista operativa: hacer el comportamiento multi-core predecible
- Establece límites de concurrencia explícitos (por instancia) y trátalos como controles de capacidad, no como “parches temporales”.
- Alerta sobre throttling de CPU y steal time; son asesinos silenciosos de capacidad.
- Controla
awaitde disco y profundidad de cola; el throughput por sí solo miente. - Mide cambios de contexto y longitud de la cola de ejecución; valores altos suelen preceder dolor en p99.
- Mantén el estado caliente fragmentado; no centralices contadores y mapas en el camino de la solicitud.
- Separa logs sensibles a durabilidad de datos masivos cuando sea posible.
- Valida la colocación NUMA en máquinas multi-socket; fija si necesitas determinismo.
- Ensaya carga pico con datos parecidos a producción; el camino serial se mostrará.
Preguntas frecuentes
1) ¿Debería preferir mayor frecuencia por núcleo o más núcleos para servicios sensibles a la latencia?
Prefiere mayor rendimiento por núcleo cuando tienes un componente serial conocido, procesamiento intenso en kernel/red, o código sensible a locks.
Más núcleos ayudan cuando la carga es embarazosamente paralela y el estado compartido es mínimo.
2) ¿Por qué la utilización de CPU se ve baja cuando el servicio está en timeout?
Porque el servicio puede estar esperando: locks, I/O, llamadas downstream, o siendo throttled por cgroups. Además, “CPU baja promedio”
puede ocultar un núcleo saturado. Siempre mira vistas por núcleo y por hilo.
3) ¿Cuál es la forma más rápida de detectar contención de locks?
En Linux, perf top mostrando pthread_mutex_lock (o rutas futex en el kernel) es una señal fuerte.
Combínalo con pidstat -t para cambios de contexto y con CPU por hilo para encontrar al culpable.
4) ¿Cómo sé si estoy limitado por I/O o por CPU?
Si iostat -x muestra await y profundidad de cola en aumento durante picos de latencia, probablemente estás limitado por I/O.
Si vmstat muestra hilos ejecutables altos y bajo iowait, probablemente estás limitado por CPU o por contención.
5) ¿Puede añadir más hilos reducir la latencia?
A veces, para cargas I/O-heavy donde la concurrencia oculta tiempo de espera. Pero una vez que alcanzas un cuello de botella compartido, más hilos aumentan encolamiento
y varianza. El movimiento correcto suele ser limitar concurrencia con backpressure.
6) ¿Cuál es la trampa más común “núcleos vs relojes” en Kubernetes?
Límites de CPU causando throttling. El pod puede mostrar “uso de CPU por debajo del límite”, pero aún así ser throttled en ráfagas, creando picos de latencia.
Revisa /sys/fs/cgroup/cpu.stat dentro del contenedor y correlaciónalo con la latencia de solicitudes.
7) ¿Por qué máquinas más grandes a veces rinden peor que las más pequeñas?
Efectos NUMA, migración del scheduler y localidad de caché. Además, las máquinas más grandes a menudo atraen cargas co-localizadas, aumentando la contención
en recursos compartidos como ancho de banda de memoria y I/O.
8) ¿El almacenamiento sigue siendo relevante si uso NVMe?
Muy relevante. NVMe mejora latencia base y throughput, pero el encolamiento sigue existiendo, las semánticas de sync siguen existiendo y los sistemas de archivos siguen haciendo trabajo.
Si generas muchas escrituras sincrónicas pequeñas, NVMe simplemente te dejará llegar más rápido al muro de cola.
9) ¿Qué métricas debo poner en un panel para reflejar la realidad de la “era de los núcleos”?
CPU por núcleo, CPU steal, throttling de CPU, cambios de contexto, longitud de cola de ejecución, await y profundidad de cola de disco, retransmisiones de red, y latencia p95/p99.
Las medias están bien, pero solo como actores de apoyo.
Próximos pasos que puedes hacer esta semana
Si quieres sistemas que se beneficien de más núcleos en lugar de avergonzarse por ellos, haz lo siguiente—práctico, no aspiracional.
-
Añade un panel que muestre CPU por núcleo y los hilos principales (o exporta CPU por hilo para el proceso principal).
Detecta el techo de un solo núcleo temprano. - Alerta sobre throttling de CPU y steal time. Si estás en contenedores o VMs y no alertas sobre eso, estás eligiendo sorpresa.
-
Haz seguimiento de disk
awaity profundidad de cola junto al p99. Si solo rastreas throughput, estás optimizando el tipo equivocado de éxito. -
Ejecuta un “drill de cuellos de botella” de una hora: bajo carga controlada, captura
mpstat,vmstat,iostat,pidstaty una muestra corta deperf.
Anota los tres principales factores limitantes. Repite trimestralmente. - Establece límites de concurrencia explícitos para tus servicios más ocupados. Trata el límite como un control de estabilidad; ajústalo como ajustas un disyuntor.
El verdadero punto de inflexión no fueron las CPU multicore. Fue el momento en que tuvimos que dejar de confiar en los relojes para cubrir nuestros pecados.
Si mides contención, encolamiento y localidad—y estás dispuesto a bajar la concurrencia cuando ayuda—los núcleos vencerán a los relojes.
De lo contrario, solo te vencerán a ti.