Resumen teórico
Imagina dos panaderías. En la primera, cada pan está listo en 2 minutos, pero el sábado por la mañana la fila sale por la puerta y algunas personas esperan una hora. En la segunda, el pan tarda 3 minutos, pero nadie espera más de 5. ¿Cuál panadería es la "más rápida"? La respuesta honesta es: depende de lo que midas. El rendimiento (performance) es el estudio de qué tan rápido es un sistema informático, cuánto trabajo puede hacer y a dónde se va su tiempo.
Toda app que usas depende de esto. Una página que tarda 3 segundos en abrir hace que la gente se vaya. Una tienda que se congela durante una gran oferta pierde dinero. Un programa que es rápido en tu portátil puede arrastrarse cuando mil personas lo usan a la vez. Quien se ocupa del rendimiento no adivina. Mide primero, encuentra la parte realmente lenta, arregla solo esa parte y vuelve a medir.
En este resumen aprenderás: las palabras que se usan para hablar de velocidad (latencia, throughput, percentiles), cómo hacer una prueba justa (benchmarking), cómo descubrir qué está lento (profiling y flame graphs), cómo la memoria y la caché del computador cambian la velocidad, cómo funciona de verdad un servidor JavaScript, por qué las bases de datos se vuelven lentas, los tipos de pruebas de carga y cómo escribirlas con la herramienta k6, y las matemáticas que dicen cuántos servidores necesitas. Al final estarás listo para el quiz.
Panorama general: medir, encontrar, arreglar, medir
Arreglar el rendimiento es como ser médico. Un médico no receta antes de revisar qué anda mal. De la misma forma, un buen ingeniero sigue un ciclo: mide cómo se comporta el sistema, encuentra la parte que más tiempo desperdicia, cambia solo esa parte y vuelve a medir para ver si de verdad mejoró.
medir --> encontrar la --> cambiar una --> medir
^ parte lenta cosa de nuevo
| |
+----------------------------------------+Cuatro capas de la misma historia
| Capa | Pregunta de ejemplo | Dónde la verás más abajo |
|---|---|---|
| La CPU y su memoria | ¿Por qué recorrer una matriz por filas es más rápido que por columnas? | Caché de la CPU y localidad de memoria |
| El runtime del programa | ¿Por qué una solicitud lenta congela un servidor Node.js? | Rendimiento del runtime |
| La base de datos | ¿Por qué una página de lista ejecuta 21 consultas? | Rendimiento de bases de datos |
| El servicio completo | ¿Cuántos servidores necesitamos el día de la oferta? | Pruebas de carga, k6 y planificación de capacidad |
En todas las capas necesitas dos habilidades. Primero, buenos números: las medidas correctas, tomadas de forma justa (benchmarking). Segundo, saber a dónde se va el tiempo (profiling). El resto de este resumen construye esas habilidades paso a paso.
Mira el video: Performance Matters, Emery Berger (Strange Loop)↑ Volver al índiceLatencia, throughput y percentiles
Volvamos a la panadería. Importan dos cosas distintas. Una es cuánto tiempo espera una persona desde que entra hasta que sale con su pan. La otra es cuántas personas atiende la panadería por hora. Los computadores tienen las mismas dos medidas.
| Palabra | Significado | Panadería | Unidad habitual |
|---|---|---|---|
| Latenciacuánto tarda una solicitud, desde que se envía hasta que llega la respuesta | Tiempo de una solicitud | Tiempo de espera de una persona | milisegundos (ms) |
| Throughputcuánto trabajo termina el sistema por unidad de tiempo | Trabajo hecho por tiempo | Personas atendidas por hora | solicitudes por segundo (req/s) |
| Tasa de error | Fracción de solicitudes que fallaron | Personas que se fueron sin pan | fallos divididos por el total |
El tiempo que ve el cliente es el tiempo de respuesta. En este resumen usamos la convención: tiempo de respuesta = tiempo de espera en la cola + tiempo de servicio (el tiempo que el servidor realmente trabaja en la solicitud). Cuando el servidor está ocupado, el tiempo de servicio casi no cambia. Lo que crece es la espera.
Por qué el promedio miente
Imagina 10 solicitudes: nueve tardan 10 ms y una tarda 910 ms. El promedio (sumar todo y dividir entre 10) es 100 ms. ¡Pero ninguna solicitud tardó 100 ms! La mediana (el valor del medio tras ordenar) es 10 ms y describe mucho mejor la solicitud típica. Unos pocos valores enormes empujan el promedio hacia arriba, y las latencias casi siempre tienen unos pocos valores enormes.
Percentiles
Un percentil responde: ¿por debajo de qué valor queda el p% de las medidas? El p50 es la mediana. El p95 es el valor que 95 de cada 100 solicitudes superan, así que 1 solicitud de cada 20 es más lenta que el p95. El p99 deja 1 de cada 100 más lenta. El extremo lento de la distribución se llama cola, y los percentiles altos la describen. Esto es la latencia de cola (tail latency).
Para calcular un percentil con el método del rango más cercano (nearest-rank): ordena los valores y toma el valor en la posición ceil(p / 100 × n), contando desde 1. Ejemplo con 10 valores ordenados:
valores: 5 6 7 8 9 10 11 12 13 50 (n = 10)
p50 -> posición ceil(0,50 x 10) = 5 -> 9
p90 -> posición ceil(0,90 x 10) = 9 -> 13
p99 -> posición ceil(0,99 x 10) = 10 -> 50Throughput a partir de los números
El throughput promedio de una prueba es el total de solicitudes completadas dividido entre la duración. En k6, http_reqs cuenta solicitudes HTTP, mientras que iterations cuenta ejecuciones completas del script, y una iteración puede hacer varias solicitudes. Por ejemplo, 18.000 solicitudes en 150 segundos son 120 req/s. No mezcles los dos conteos.
En un modelo cerrado sin pausa, cada uno de los U usuarios envía una solicitud a la vez, así que el throughput es aproximadamente U / tiempo de respuesta. Con 20 usuarios y respuestas de 100 ms: 20 / 0,1 = 200 req/s. Si el servidor se vuelve más lento, los usuarios también envían menos.
La cola crece cuando hay muchas llamadas
Si una llamada tiene un 1% de probabilidad de ser lenta (su p99), una página que hace muchas llamadas es lenta con mucha más frecuencia, porque solo se salva si todas las llamadas son rápidas. La probabilidad de al menos una llamada lenta es 1 - 0,99^n.
- 1 llamada
- 1 %
- 10 llamadas
- 9.6 %
- 50 llamadas
- 39.5 %
- 100 llamadas
- 63.4 %
Dos trampas
- Los fallos son rápidos. Una solicitud que falla con un error muchas veces responde en 5 ms. Si los errores suben de 0% a 60%, la mediana puede bajar y parecer una mejora. Lee la latencia y la tasa de error juntas y mide las respuestas exitosas por separado.
- Omisión coordinada (coordinated omission). Un generador de carga que espera la respuesta lenta antes de enviar la siguiente solicitud deja de enviar justo en el momento lento, así que nunca registra lo malo que fue. Los percentiles salen demasiado optimistas. La cura es un modelo abierto: las solicitudes salen a una tasa fija, hagan lo que hagan las respuestas.
Metodología de benchmark
Un benchmark es una carrera justa entre programas. Si un corredor sale con mochila y el otro sin ella, la carrera no prueba nada. Hacer benchmarks es el arte de volver justa la carrera, para que la diferencia en los números venga realmente del código.
Warm-up, repeticiones y ruido
- Warm-up (calentamiento). Un programa recién iniciado no está en su estado normal. Los archivos no están en la caché del sistema, las cachés de la CPU están frías y los runtimes con compilador JITun compilador que traduce las partes calientes del código a código de máquina rápido mientras el programa corre aún no han optimizado. Por eso un runner ejecuta el trabajo algunas veces y descarta esos tiempos. Si lo que importa es el tiempo de arranque, mídelo por separado y dilo.
- Repite. Una ejecución prueba poco. Otro programa en la misma máquina (una videollamada, un antivirus) puede retrasar una ejecución. Esto se llama vecino ruidoso (noisy neighbour). Ejecuta varias veces en una máquina lo más ociosa posible.
- Reporta la dispersión, no solo la mejor ejecución. Di el promedio y la desviación estándarun número que dice cuánto suelen alejarse las ejecuciones de su promedio y, si puedes, el mínimo y el máximo.
La desviación estándar muestral divide entre n - 1 (no entre n), porque el promedio se estimó con las mismas ejecuciones. Ejemplo con ejecuciones de 4, 6 y 8 ms: promedio = 6; las distancias al promedio son 2, 0 y 2, así que los cuadrados son 4, 0 y 4, que suman 8; divide entre 3 - 1 = 2 y da 4; la raíz cuadrada es 2 ms.
Cómo leer un resultado: si A tiene promedio 102 ms y B tiene promedio 99 ms, pero la desviación estándar de ambos es de unos 6 ms, entonces la diferencia de 3 ms es menor que el ruido. Repetir la prueba podría invertir el orden. Reporta un empate, o mide más, en una máquina más tranquila.
Speed-up y la ley de Amdahl
El speed-up es el tiempo antiguo dividido entre el tiempo nuevo, para el mismo trabajo. Pasar de 800 ms a 200 ms es un speed-up de 4. Cuidado: reducir el tiempo un 50% es un speed-up de 2, y reducirlo un 90% es un speed-up de 10. Di qué medida usas.
Acelerar una parte tiene un límite. Si una parte ocupa la fracción p del tiempo y la haces s veces más rápida, el programa completo se vuelve 1 / ((1 - p) + p / s) veces más rápido. Esta es la ley de Amdahl. Ejemplo: un trabajo de 10 s tiene 5 s de cálculo. Hacer el cálculo 5 veces más rápido da 1 / (0,5 + 0,1) = 1,67, no 5. Incluso un cálculo infinitamente rápido dejaría los otros 5 s, así que el techo es 2.
Una comparación justa
- Cambia una cosa a la vez. Para comparar dos runtimes, usa la misma carga de trabajo, el mismo tamaño de datos, la misma concurrencia y la misma máquina.
- Mide el proceso completo (desde fuera) para saber cuánto espera el usuario. Incluye cargar el runtime y leer la entrada, un costo casi fijo que domina con entradas pequeñas. Mide solo la sección (dentro del programa) para saber el costo del algoritmo. Ambos son útiles si dices cuál muestras.
- Haz que cada variante imprima un checksum de su resultado. Si una variante es mucho más rápida pero su checksum es distinto, no es una optimización, es un bug.
- Consume el resultado. Un compilador optimizador puede borrar código cuyo resultado nadie usa, así que un microbenchmark puede imprimir "0 ms" porque midió un bucle vacío. Acumula los resultados en un checksum e imprímelo al final.
Resultados reproducibles
Un número publicado solo vale si otra persona puede repetirlo. Registra siempre la máquina (CPU, memoria, sistema), las versiones exactas de compiladores y runtimes y el comando completo con su entrada y opciones. La herramienta hyperfine mide comandos de terminal por ti, con warm-up, repeticiones y estadísticas (promedio, desviación estándar, mínimo, máximo), y exporta JSON o Markdown. Mide el proceso completo, así que el arranque está incluido.
↑ Volver al índiceProfiling y flame graphs
Sabes que el programa está lento, pero ¿dónde? Un profiler es una herramienta que observa un programa y te dice a dónde se va el tiempo. Es como una cámara de seguridad en una cocina: después puedes ver qué paso consume más minutos. Haz profiling primero, optimiza después.
Muestreo e instrumentación
| Tipo | Cómo funciona | Bueno | Malo |
|---|---|---|---|
| Profiler por muestreo (sampling) | Espía el programa muchas veces por segundo y anota la pila de llamadasla lista de funciones que se están ejecutando ahora, cada una llamada por la que está debajo | Costo bajo y estable, incluso con millones de llamadas | Una estimación estadística, sin conteo exacto de llamadas |
| Profiler por instrumentación | Agrega código a cada función para contar y medir cada llamada | Conteos y tiempos exactos | El costo crece con el número de llamadas y distorsiona funciones diminutas llamadas muchas veces |
Qué perfil para qué problema
- Perfil de CPU: ¿dónde gasta su tiempo el procesador?
- Perfil de heap: ¿quién asigna memoria y qué objetos siguen vivos? Úsalo cuando la memoria crece durante horas (una fuga) mientras la CPU está baja.
- Tiempo de reloj (wall-clock) vs tiempo de CPU: wall-clock es el tiempo real entre el inicio y el fin. El tiempo de CPU es solo el tiempo en que el procesador ejecutó tu programa (
useren tu código,sysen el kernel). La diferencia es el tiempo off-CPU: esperar disco, red, una base de datos o un lock.
$ time ./script
real 5.0 s # el reloj de la pared
user 0.3 s # CPU ejecutando tu código
sys 0.1 s # CPU ejecutando el kernel por tiCómo leer un flame graph
Un flame graph dibuja las pilas de llamadas muestreadas. Cada caja es una función. Una caja está encima de la función que la llamó, así que la altura es la profundidad de la pila. El ancho de una caja es la fracción de muestras en que esa función estaba en la pila, así que ancho significa "mucho tiempo". Las pilas idénticas se fusionan y las cajas se ordenan por nombre.
[ escape ]
[ parse ] [ render .............. ][ send ]
[ handleRequest ............................ ]
[ main ..................................... ]
(la raíz siempre tiene 100% de ancho)El tiempo total (total time) de una función incluye todo lo que ella llama. El tiempo propio (self time) es solo su código: el total menos la suma de sus hijos directos. Si renderHtml tiene un total de 600 ms y sus hijos tardan 450 + 100 ms, su tiempo propio es 50 ms. La CPU realmente trabaja donde el tiempo propio es alto, que en un flame graph es el borde libre en la parte de arriba de una caja. Busca la meseta más ancha arriba, no la caja más ancha (la raíz siempre es la más ancha) ni la torre más alta (eso es solo una pila profunda).
↑ Volver al índiceRendimiento del runtime: event loop, garbage collection, cluster
Node.js y Bun ejecutan el JavaScript de tu servidor en un solo hilo. Un hilo (thread) es como un cocinero. ¿Cómo atiende un cocinero a miles de clientes? Porque la mayor parte del tiempo no está cocinando, solo esperando.
El event loop
El event loop es una cola de tareas listas (callbacks) que ejecuta una a la vez. Cuando tu código pide una respuesta a la base de datos, la espera se entrega al sistema operativo y el bucle sigue con otras solicitudes. Esto es trabajo I/O-bound: el tiempo se va esperando, no calculando. Un hilo mantiene miles de estas solicitudes en marcha, y el límite pasa a ser la base de datos o el pool de conexiones.
I/O-bound: [inicia query][ otras solicitudes corren aquí ][respuesta]
(el event loop está libre)
CPU-bound: [ calculando 200 ms, nadie más corre ]
/ping espera detrásEl trabajo CPU-bound es lo contrario: el hilo está ocupado calculando, así que ningún otro callback corre, ningún temporizador se dispara, ningún resultado de I/O se lee. Si dos solicitudes de 300 ms de CPU síncrona llegan juntas, seguidas de un /ping diminuto, el ping se responde solo después de unos 600 ms. Un endpoint lento arruina la latencia de todo el servidor.
Usar más núcleos
| Herramienta | Qué hace | Memoria | Bueno para |
|---|---|---|---|
worker_threads | Hilos extra dentro del mismo proceso, cada uno con su propio event loop | Puede compartir memoria binaria | Sacar un cálculo pesado del hilo principal |
cluster / pm2 start app.js -i max | Varias copias del servidor completo, normalmente una por núcleo, todas escuchando en el mismo puerto | Cada proceso tiene su propia memoria, nada se comparte | Atender más solicitudes, aislar fallos |
El cluster multiplica la capacidad de CPU, así que ayuda cuando la CPU es el cuello de botella. Si una solicitud necesita 25 ms de CPU, un proceso atiende como máximo 1 / 0,025 = 40 req/s. Cuatro workers en cuatro núcleos dan hasta 160 req/s (algo menos en la vida real). Pero para trabajo I/O-bound (5 ms de CPU y 95 ms esperando), un proceso está libre la mayor parte del tiempo, así que más workers no cambian nada. Antes de añadir workers, calcula cuánto de un núcleo usa realmente el proceso: tasa de solicitudes × tiempo de CPU por solicitud.
Garbage collection
Los programas crean objetos y los olvidan. El garbage collector (GC) encuentra los objetos que nadie usa y libera su memoria. V8, el motor de Node.js, es generacional, basado en la observación de que la mayoría de los objetos mueren jóvenes. Los objetos nuevos van a un área pequeña, la generación joven, que se limpia a menudo con recolecciones menores (minor) rápidas. Los supervivientes pasan a la generación vieja, que se limpia rara vez con recolecciones mayores (major), más lentas porque el heap es mucho más grande.
Gran parte del trabajo del GC es incremental o corre en hilos auxiliares, pero algunas fases son stop-the-world: JavaScript se detiene por completo. Las recolecciones mayores sobre un heap viejo grande pueden pausar por mucho tiempo. Como son raras, casi no cambian el promedio ni la mediana, y aparecen como picos periódicos en el p99. No lo confundas con una fuga: tras una fuga, la memoria en uso después de cada recolección sigue creciendo. Si la memoria vuelve al mismo nivel después de cada recolección, no es una fuga.
↑ Volver al índiceCaché de la CPU y localidad de memoria
Big O cuenta pasos, como si cada paso costara lo mismo. Pero leer de la memoria no es igual de rápido en todas partes. Piensa en tu escritorio, tu estantería y la biblioteca de la ciudad. El lápiz sobre el escritorio tarda un segundo. Un libro en la estantería tarda un minuto. Un libro de la biblioteca tarda una tarde.
La jerarquía de memoria
registros < L1 < L2 < L3 < RAM < disco
más pequeña, más rápida, más cara -->
<-- más grande, más lenta, más barataLa memoria rápida es pequeña y cara, la memoria grande es barata y lenta. Por eso el computador mantiene los datos en uso ahora en los niveles pequeños y rápidos. En números redondos, la L1 responde en cerca de 1 nanosegundo y la memoria principal (RAM) en cerca de 100, unas cien veces más lenta. Un programa es rápido cuando la mayoría de sus lecturas las atienden los niveles de arriba. Una lectura que encuentra su dato en la caché es un hit, y una que debe ir más lejos es un miss.
Líneas de caché y localidad
La caché no copia bytes sueltos. Copia bloques llamados líneas de caché (cache lines), normalmente de 64 bytes. Si lees un byte que falta, el procesador carga la línea completa de 64 bytes que lo contiene. Los programas tienen dos costumbres, llamadas juntas localidad:
- Localidad temporal: usas la misma dirección otra vez poco después (un contador de bucle).
- Localidad espacial: usas direcciones vecinas una tras otra (los elementos de un arreglo).
Cuenta los misses de un recorrido secuencial: es el número de líneas tocadas, no el número de elementos. Un double tiene 8 bytes, así que una línea guarda 8 de ellos. Recorrer 4.096 doubles lee 4096 × 8 / 64 = 512 líneas: 1 miss por cada 8 elementos, y los siguientes 7 son hits.
Orden por filas (row-major)
C, C++ y Rust guardan una matriz fila tras fila (row-major). El elemento (i, j) está en el índice i × n + j. Avanzar a lo largo de una fila usa vecinos. Bajar por una columna salta una fila completa en cada paso.
/* A: bucle interno en j, sigue el orden de la memoria */
for (i = 0; i < N; i++)
for (j = 0; j < N; j++) sum += a[i][j];
/* B: bucle interno en i, salta N valores en cada paso */
for (j = 0; j < N; j++)
for (i = 0; i < N; i++) sum += a[i][j];La misma idea explica por qué sumar 1 millón de números en un arreglo le gana a una lista enlazada con nodos dispersos en la memoria. Ambos son O(n), pero la lista puede pagar un miss por elemento.
Multiplicación de matrices: intercambio de bucles y blocking
Multiplicar dos matrices n × n tiene tres bucles anidados y n³ multiplicaciones, sea cual sea el orden de los bucles. En el orden i, j, k, el bucle interno baja por una columna de b, el peor recorrido. El intercambio de bucles (loop interchange) a i, k, j hace que cada acceso interno sea secuencial o fijo. Mismo resultado, mismo Big O, velocidad muy distinta.
El blocking (o tiling) va más allá: hace el trabajo en pequeños bloques B × B que caben en la caché y reutiliza cada pedazo cargado muchas veces antes de seguir. Tres bloques de doubles usan 3 × B² × 8 bytes. Para una L1 de 32 KiB (32.768 bytes): B = 32 necesita 24.576 bytes y cabe, mientras que B = 64 necesita 98.304 y no cabe. El tamaño del bloque sale de la capacidad de la caché, y el blocking no cambia el número de operaciones.
Rendimiento de bases de datos: índices, N+1 y pool de conexiones
Muchas apps lentas lo son por culpa de la base de datos. Una tabla con 10 millones de filas es como una biblioteca con 10 millones de libros. Encontrar un libro recorriendo todos los estantes toma una eternidad. El bibliotecario necesita un catálogo.
Índices
Un índice es ese catálogo. Un índice B-tree guarda los valores de una columna ordenados en un árbol balanceado, así que encontrar un valor cuesta O(log n) en vez de O(n). Cada nodo guarda cientos de claves, por lo que el árbol es poco profundo incluso en tablas enormes. Sin un índice útil, PostgreSQL hace un recorrido secuencial (sequential scan): lee todas las filas y descarta las que no coinciden.
EXPLAIN muestra el plan que eligió la base de datos, sin ejecutar la consulta. Cada nodo muestra un costo estimado (unidades abstractas), las filas estimadas y el ancho de fila en bytes. Un Seq Scan con un Filter que devuelve pocas filas de una tabla enorme es la señal clásica de un índice que falta.
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
-- Seq Scan on orders (cost=0.00..23864.00 rows=95 width=64)
-- Filter: (customer_id = 42)
CREATE INDEX idx_orders_customer ON orders (customer_id);- Los índices cuestan escrituras. Cada
INSERT, y cadaUPDATEoDELETEque toca columnas indexadas, debe actualizar cada índice, y cada índice usa disco y memoria. Crea índices para consultas que existen y elimina los que nadie usa. - Selectividad. Es la fracción de filas que deja pasar un filtro. Un índice vale la pena cuando esa fracción es pequeña. En una columna con tres valores donde 'delivered' es el 95% de las filas, el planificador prefiere el recorrido secuencial, porque millones de lecturas aleatorias son peores que una lectura directa.
- Índices compuestos y el prefijo izquierdo (leftmost prefix). Un índice en
(tenant_id, status, created_at)está ordenado por la primera columna, luego por la segunda dentro de cada valor de la primera, como una guía telefónica ordenada por apellido y luego por nombre. Atiende bien los filtros que fijan un prefijo empezando por la izquierda. Un filtro que se salta la primera columna encuentra sus filas dispersas por el índice y es el peor atendido. - **
SELECT *** trae todas las columnas, incluso las que la app tira. Las columnas anchas (texto largo, JSON) cuestan lecturas de disco, memoria y red. Pide solo las columnas que necesitas.
El problema N+1
El código trae una lista con 1 consulta y luego hace más consultas por cada uno de los N elementos. Cada consulta es rápida, pero cada una paga un viaje de ida y vuelta a la base de datos. Una lista de 10 posts que carga el autor de cada post en un bucle ejecuta 1 + 10 = 11 consultas, y 100 posts ejecutarían 101. La solución es un número fijo de consultas: un JOIN, un filtro WHERE id IN (...) con todos los ids, o el eager loading del ORM.
Paginación
Con OFFSET 100000, la base de datos debe producir 100.000 filas y tirarlas antes de la página, así que las páginas más profundas cuestan más. La paginación por clave (keyset) recuerda el último valor visto: WHERE id > 100000 ORDER BY id LIMIT 20. El índice va directo a ese punto, y el costo es el mismo en cada página. La contrapartida: solo puedes ir a la página siguiente o anterior, y el orden necesita columnas indexadas sin empates.
Pool de conexiones
Abrir una conexión con la base de datos cuesta varios viajes de ida y vuelta por la red y, en PostgreSQL, un proceso nuevo en el servidor. Eso puede costar más que la consulta misma. Un pool de conexiones abre un número limitado de conexiones, las mantiene abiertas y presta una a cada solicitud, que la devuelve al terminar.
La capacidad de un pool es el número de conexiones dividido entre el tiempo que cada solicitud retiene una. Con 20 conexiones y 40 ms cada una: 20 / 0,04 = 500 req/s. Por encima de esa tasa las solicitudes se acumulan y la latencia crece sin límite, aunque cada consulta siga tardando lo mismo. En una prueba de carga esto aparece como una rodilla (knee): latencia estable hasta un punto y luego una subida empinada mientras el throughput deja de crecer. Acortar el tiempo que se retiene una conexión aumenta la capacidad tanto como agrandar el pool.
El límite max_connections de PostgreSQL pertenece a todo el servidor, pero cada instancia de la aplicación tiene su propio pool. Lo que debe caber es la suma: tamaño del pool × instancias + reserva. Con max_connections = 100, una reserva de 10 y 6 instancias: (100 - 10) / 6 = 15, así que como máximo 15 por instancia. Un pool enorme no es la respuesta: demasiadas conexiones activas pelean por CPU, memoria y disco en la base de datos.
Pruebas de carga, estrés, pico y resistencia
Una prueba de carga envía muchos usuarios falsos a un servicio y observa cómo se comporta. Es como un simulacro de incendio: quieres encontrar los problemas antes del incendio de verdad. Preguntas distintas piden formas distintas de tráfico, por eso existen distintos tipos de prueba. Se diferencian en dos cosas: cuánta carga y por cuánto tiempo.
| Tipo | Forma de la carga | Pregunta que responde |
|---|---|---|
| Smoke (humo) | Carga mínima, muy corta | ¿Funcionan el script, el entorno y la recolección de métricas? También da una línea base |
| Carga (load, carga promedio) | El tráfico normal esperado, mantenido un tiempo | ¿Cumple el servicio sus metas con la carga habitual? |
| Estrés (stress) | Sube gradualmente por encima de lo normal y se mantiene | ¿Qué pasa por encima del tráfico esperado? ¿Dónde está el punto de saturación? |
| Pico (spike) | Muy alta, muy corta, casi sin rampa | ¿Sobrevive a una ráfaga repentina y se recupera? |
| Resistencia (soak, endurance) | Carga promedio durante horas | ¿Qué se degrada lentamente con el tiempo, como una fuga de memoria? |
| Punto de ruptura (breakpoint) | Crece lenta y continuamente hasta que falla un criterio | ¿Cuál es el límite? El valor en la falla es la respuesta |
usuarios
| pico estrés
| /\ ____
| carga / \ carga /
| _____ / \ _____ /
| / \__/ \_____/ \____/
+----------------------------------> tiempo
(soak = la meseta de 'carga' durando horas)Qué revela cada uno
- Estrés y la rodilla. Mientras sobra capacidad, más usuarios dan más throughput con latencia casi igual. En el punto de saturación se agota algún recurso (CPU, conexiones del pool, hilos): el throughput se aplana y los usuarios extra solo alargan la cola, así que la latencia se dispara. En una tabla: 100 usuarios dan 800 req/s a 30 ms, 150 usuarios dan 1200 req/s a 32 ms, pero 200 usuarios dan solo 1250 req/s a 190 ms. El codo está en unos 150 usuarios.
- Pico. Observa tres momentos: antes, durante y después. Sobrevivir a la ráfaga no basta. Si el p95 sigue diez veces por encima de lo normal durante 15 minutos después de que baja la carga, hay colas sin límite, timeouts largos, tormentas de reintentos o pools lentos para liberar recursos. Una tienda que abre una oferta a una hora exacta necesita una prueba de pico.
- Soak. ¿La memoria crece al principio y luego queda plana? Eso es warm-up (cachés llenándose), no una fuga. Una fuga es un crecimiento que continúa con carga constante. Por eso una prueba de soak dura horas: en pocos minutos las dos curvas se ven iguales.
- Punto de ruptura. A diferencia del estrés, no tiene una meseta planeada. Normalmente se usa un executor de tasa de llegada, para que la presión siga subiendo incluso cuando las respuestas se vuelven lentas.
Modelos abierto y cerrado, otra vez
En un modelo cerrado el número de usuarios es fijo y cada uno espera su respuesta. Cuando el servidor se vuelve lento, el generador se vuelve lento con él, lo que alivia al servidor en el peor momento. En un modelo abierto las llegadas siguen una tasa fija, como personas reales que no saben que el sitio está lento. Por eso, para pruebas de estrés y de pico, un executor de tasa de llegada es la mejor opción.
↑ Volver al índicek6: usuarios virtuales, etapas, thresholds y checks
k6una herramienta de pruebas de carga de código abierto de Grafana, cuyos scripts de prueba se escriben en JavaScript es la herramienta de esta área. Un script de k6 es un archivo JavaScript que dice qué hace un usuario falso y cuántos usuarios falsos hay. Piensa en un teatro de títeres: escribes la obra una vez y k6 mueve muchos títeres al mismo tiempo.
Usuarios virtuales e iteraciones
Un usuario virtual (VU) es un ejecutor independiente que corre la función por defecto de tu script en un bucle. Cada pasada completa por la función es una iteración. Una iteración puede contener varias solicitudes, checks y pausas, así que k6 cuenta iteraciones y solicitudes en métricas distintas.
import http from 'k6/http';
import { sleep } from 'k6';
export const options = { vus: 10, duration: '60s' };
export default function () {
http.get('http://localhost:3000/products');
sleep(1); // una pausa, en segundos, como una persona pensando
}Una iteración dura lo que duran sus solicitudes más el sleep. Aquí, si la solicitud tarda 500 ms, una iteración dura 1,5 s, así que el número de iteraciones es VUs × duración / tiempo de la iteración, unas 10 × 60 / 1,5 = 400.
Etapas y executors
La opción stages dibuja una rampa: cada etapa tiene una duration y un target, y k6 mueve el número de VUs en línea recta desde donde está hasta el objetivo. Una etapa que repite el objetivo anterior es una meseta, y una última etapa con objetivo 0 es la rampa de bajada.
export const options = {
stages: [
{ duration: '1m', target: 20 }, // subida
{ duration: '3m', target: 20 }, // meseta
{ duration: '1m', target: 0 }, // bajada
],
};| Executor | Modelo | Qué controla | Nota |
|---|---|---|---|
constant-vus, ramping-vus | Cerrado | Número de VUs | Una nueva iteración empieza solo cuando termina la anterior, así que tasa = VUs / tiempo de la iteración |
constant-arrival-rate, ramping-arrival-rate | Abierto | Iteraciones iniciadas por unidad de tiempo | Necesita suficientes VUs: aproximadamente tasa × duración de la iteración. Si se acaban, las iteraciones se descartan y se cuentan en dropped_iterations |
Ejemplo: 50 VUs de constant-vus sin sleep. Con 100 ms por solicitud envían 50 / 0,1 = 500 req/s. Si el servidor se ralentiza a 500 ms, envían solo 100 req/s: la prueba alivia la presión justo cuando el sistema sufre. Un constant-arrival-rate de 30 por segundo con una respuesta de 1 s necesita unos 30 VUs. Si solo tiene 20, k6 descarta el resto.
Thresholds y checks
Un threshold es una regla de aprobado o fallo sobre una métrica: una agregación (avg, p(95), rate, count), un operador y un límite. http_req_duration: ['p(95)<500'] significa que el p95 de todas las duraciones de solicitud debe ser menor que 500 ms. Cuando se supera cualquier threshold, k6 termina con código de salida 99, así que un pipeline de CI ve un fallo. Por defecto k6 sigue corriendo. La forma larga { threshold: 'rate<0.05', abortOnFail: true, delayAbortEval: '30s' } detiene la prueba antes, y delayAbortEval espera primero, para que unos pocos fallos iniciales no aborten la prueba antes de que existan suficientes muestras.
Un check verifica algo sobre una respuesta, como "el cuerpo tiene elementos". No detiene la prueba ni la hace fallar: solo registra la tasa de aciertos en la métrica checks. Para que un contenido incorrecto haga fallar la prueba, añade un threshold sobre los checks, por ejemplo checks: ['rate>0.99'].
Las cuatro fases y los tipos de métrica
| Fase | Cuándo corre | Uso |
|---|---|---|
| Init (nivel superior) | Una vez por VU | Imports, lectura de archivos, definir options |
setup() | Una vez, antes de la carga | Crear datos compartidos, como una cuenta de prueba. Su valor de retorno debe ser serializable a JSON |
| función por defecto | Repetida por todos los VUs | La carga en sí |
teardown() | Una vez, al final | Limpieza |
- Counter: suma valores (cuánto).
- Gauge: guarda el último valor (cuánto ahora).
- Rate: la proporción de valores verdaderos (con qué frecuencia).
http_req_failedychecksson Rates. - Trend: estadísticas como promedio y percentiles (qué distribución). Úsalo para el tiempo de un flujo de login.
Otras piezas útiles: __ENV guarda variables de entorno (k6 run -e NOMBRE=valor), así que const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000' hace que el caso seguro y local sea el predeterminado. Y handleSummary(data) recibe todas las métricas al final y devuelve un objeto cuyas claves son destinos (un archivo, stdout) y cuyos valores son el contenido, que es como se generan informes JSON o HTML. Reemplaza el resumen por defecto, así que incluye una clave stdout si quieres ambos.
Planificación de capacidad y colas
La planificación de capacidad responde: ¿cuántos servidores necesitamos? Para responder usamos unas pocas leyes simples de la teoría de colas, que son las matemáticas de esperar en una fila. Imagina una sola taquilla de cine.
Dos leyes que siempre se cumplen
La ley de utilización: la fracción de tiempo que un servidor está ocupado es tasa de llegada × tiempo medio de servicio (con las mismas unidades de tiempo). Una taquilla que recibe 10 clientes por segundo y tarda 50 ms en cada uno está ocupada 10 × 0,05 = 0,5, o sea el 50% del tiempo. Es el primer cálculo de la planificación de capacidad: cuánto ya está comprometido.
La ley de Little: en un sistema estable, L = λ × W, donde L es el número promedio de elementos dentro, λ (lambda) la tasa de llegada y W el tiempo promedio que cada elemento pasa dentro (espera más servicio). No depende de cómo se distribuyan las llegadas ni de cuántos servidores haya. Si llegan 100 solicitudes por segundo y cada una pasa 0,05 s en el sistema, en promedio 100 × 0,05 = 5 solicitudes están dentro. Convierte entre concurrencia, throughput y latencia.
Por qué la latencia explota cerca del 100%
Las llegadas nunca están espaciadas por igual. Cuando dos clientes llegan juntos, uno debe esperar. La cola se vacía durante el tiempo ocioso. Con utilización baja hay mucho tiempo ocioso y una ráfaga se absorbe rápido. Con utilización alta, la cola de una ráfaga no está vacía cuando llega la siguiente. Para un modelo simple, la cola M/M/1 (un servidor, llegadas aleatorias de Poisson, tiempos de servicio exponenciales), el tiempo medio de respuesta es:
R = S / (1 - U)
S = tiempo medio de servicio, U = utilización (entre 0 y 1)
ejemplo: S = 20 ms, 40 req/s -> U = 0,8 -> R = 20 / 0,2 = 100 ms- U = 50%
- 2 veces S
- U = 70%
- 3.33 veces S
- U = 80%
- 5 veces S
- U = 90%
- 10 veces S
- U = 95%
- 20 veces S
- U = 99%
- 100 veces S
Mira la forma: casi plana al principio, luego muy empinada. Pasar del 50% al 90% de utilización (un tráfico que ni siquiera se duplicó) multiplica la latencia por 5. La fórmula depende del modelo, pero la forma es una buena intuición para sistemas reales. Por eso dejamos holgura (headroom): capacidad de sobra.
Dimensionar a partir de una prueba de carga
- Mide la capacidad de una instancia dentro de la meta de latencia, por ejemplo 250 req/s.
- Aplica la regla de holgura: si cada instancia puede correr como máximo al 70%, la capacidad utilizable es 175 req/s.
- Divide el pico esperado entre ella y redondea hacia arriba. Para un pico de 1.000 req/s: 1000 / 175 = 5,7, así que 6 instancias.
Esto supone que la carga se reparte por igual y que la escala es lineal. Compruébalo: una base de datos compartida puede saturarse primero.
El mismo método dimensiona un pool de conexiones. El número promedio de conexiones ocupadas es tasa de llegada × tiempo de retención. Con 100 req/s reteniendo una conexión 30 ms: 3 ocupadas en promedio. Para mantener como máximo el 60% ocupadas, divide entre 0,6: 5 conexiones. Un pool dimensionado exactamente para el promedio estaría 100% ocupado, y la cola crecería mucho antes de eso.
La ley de Amdahl para escalar
Si una fracción s del trabajo no se puede paralelizar, el speed-up con N procesadores es 1 / (s + (1 - s) / N). Con un 10% serial, nunca supera 10, sin importar cuántos procesadores añadas.
- 1 núcleo
- 1 veces
- 2 núcleos
- 1.82 veces
- 4 núcleos
- 3.08 veces
- 8 núcleos
- 4.71 veces
- 16 núcleos
- 6.4 veces
- Infinito
- 10 veces
En un cluster de procesos, la parte serial suele ser un recurso compartido: la base de datos o un lock.
↑ Volver al índiceJuntando todo
Ahora tienes una caja de herramientas. Así se conectan las piezas cuando un servicio está lento, paso a paso.
- Define rápido: elige una meta con un percentil, por ejemplo "95% de las solicitudes en menos de 300 ms".
- Produce carga con una prueba del tipo correcto (carga, estrés, pico, soak) en un modelo abierto, contra un objetivo local.
- Lee el resultado con percentiles, throughput y tasa de error juntos, y busca la rodilla.
- Encuentra el cuello de botella: CPU alta pide un perfil de CPU y un flame graph. CPU baja y latencia alta significa espera: base de datos, pool, red.
- Arregla una cosa, vuelve a medir con justicia (warm-up, repeticiones, misma máquina, checksum) y reporta la dispersión.
- Dimensiona el servicio con utilización, ley de Little y holgura.
| Síntoma | Causa probable | Herramienta o idea |
|---|---|---|
| p99 alto, p50 normal | Pocas solicitudes lentas: pausa del GC, lock, cola | Percentiles, logs del GC |
| Un endpoint lento congela todas las solicitudes (Node.js) | CPU síncrona en el hilo único | Flame graph, worker threads, cluster |
| Latencia plana y luego una subida repentina al aumentar la carga | Un recurso saturado (pool, CPU) | Prueba de estrés, ley de utilización |
| Latencia alta pero CPU baja | Espera: base de datos, pool, red | Wall-clock vs tiempo de CPU, tracing |
| Una página de lista hace decenas de consultas | N+1 | JOIN o IN, eager loading |
| Mismo Big O, velocidad muy distinta | Cache misses, mal orden de memoria | Intercambio de bucles, blocking |
| La memoria crece sin fin | Fuga | Perfil de heap, prueba de soak |
Errores comunes
- Optimizar sin hacer profiling.
- Mirar solo el promedio, o promediar percentiles de servidores distintos.
- Comparar dos programas que hacen trabajos distintos, o nunca revisar el checksum.
- Medir código que el compilador eliminó, o incluir el arranque sin querer.
- Una prueba de modelo cerrado que esconde el problema que debería encontrar (omisión coordinada).
- Apuntar una prueba de carga a un sistema que no es tuyo.
- Añadir workers o un pool mayor sin comprobar dónde está el cuello de botella.