Módulo 4: Perfiles de carga y stages
6. constant-arrival-rate: carga por tasa de llegada
Descripción
Los dos executors de la lección 5 tienen algo en común: fijan cuántos usuarios hay (los VUs). Este executor cambia la pregunta de raíz: en vez de fijar los usuarios, fija cuántas peticiones por segundo llegan —la tasa de llegada, los RPS— las termine el servidor o no. Esa diferencia, que suena técnica, es en realidad profunda, porque son dos modelos del mundo distintos. El modelo por VUs es cerrado: cada usuario virtual espera su respuesta antes de mandar la siguiente, así que si el servidor se pone lento, los VUs se frenan solos —la carga se auto-limita—. El modelo por tasa de llegada es abierto: las peticiones entran al ritmo que fijaste, sin esperar a que salgan las anteriores, así que si el servidor se pone lento, las peticiones se acumulan —la carga no se auto-limita—. En esta lección entiendes esa diferencia, conoces el executor constant-arrival-rate (contenido), y —lo más revelador— lo ejecutas contra Reservo a dos tasas: una por debajo de su capacidad y otra por encima, para ver con números cómo el modelo abierto destapa una sobrecarga que el modelo por VUs habría escondido.
Conexión con el módulo: la lección 5 cubrió los executors por VUs (el modelo cerrado); esta cubre el executor por tasa de llegada (el modelo abierto). Juntas completan el tema de los executors. Esta es, además, la lección conceptualmente más densa del módulo, porque el salto de "fijar usuarios" a "fijar demanda" reordena cómo piensas una prueba de carga. Reutilizamos la noción de capacidad/throughput del módulo 3 (los RPS que el servidor puede sostener) y la de p95. No entramos en thresholds (módulo 5) ni en el flujo cotizar→reservar (módulo 6): aquí el foco es un solo executor y el modelo que representa.
La analogía: la fila del banco vs la cinta transportadora
Imagina dos maneras de darle trabajo a una ventanilla de banco.
Modelo cerrado (por VUs): hay 20 clientes y una sola fila. Cuando un cliente es atendido, se va y entonces el siguiente de la fila avanza. Si el cajero se pone lento, la fila simplemente avanza más despacio: nunca hay más de 20 personas en el banco, porque cada una espera su turno antes de que entre trabajo nuevo. El ritmo de trabajo se auto-regula con la velocidad del cajero. Esto son los VUs: cada uno espera su respuesta antes de pedir de nuevo.
Modelo abierto (por tasa de llegada): hay una cinta transportadora que deja caer un formulario nuevo en el mostrador cada 3 segundos, pase lo que pase, sin importar si el cajero terminó el anterior. Si el cajero mantiene el ritmo, perfecto: procesa uno, llega otro, el mostrador está limpio. Pero si el cajero se pone lento —tarda 5 segundos por formulario cuando llega uno cada 3—, los formularios se apilan: la pila crece y crece, y cada formulario nuevo espera detrás de una montaña cada vez más alta. La cinta no se entera de que el cajero va lento; sigue soltando formularios al mismo ritmo. Esto es la tasa de llegada: las peticiones entran a un ritmo fijo, las procese el servidor o no.
¿Cuál modela mejor el tráfico real? La cinta. Los usuarios de tu API no se coordinan en una fila educada esperando a que el anterior termine; llegan cuando llegan, a un ritmo que depende de cuánta gente hay, no de qué tan rápido responde tu servidor. Si tu API se pone lenta un viernes por la noche, los usuarios no dejan de llegar por cortesía —siguen entrando al mismo ritmo, y se apilan—. Por eso el modelo abierto (tasa de llegada) captura un peligro que el cerrado (VUs) esconde: la posibilidad de que la demanda supere la capacidad y la cola crezca sin freno.
El peligro que el modelo por VUs esconde
Aquí está la consecuencia práctica, y es importante. Con un constant-vus de 20, nunca puedes tener más de 20 peticiones en vuelo a la vez, porque cada VU espera su respuesta antes de mandar otra. Si el servidor se satura, los 20 VUs simplemente esperan más, y tu prueba mide una carga que se auto-limitó a 20. Es decir: el modelo por VUs no puede sobrecargar el servidor más allá de N. Si tu servidor colapsaría con una demanda de 500 RPS pero tú lo pruebas con 20 VUs que se auto-frenan, jamás verás el colapso —tu prueba fue amable con el servidor sin querer—.
Con constant-arrival-rate fijas la demanda (digamos 500 RPS) y k6 se encarga de sostenerla lanzando tantas iteraciones como haga falta, aunque el servidor no dé abasto. Si el servidor solo puede con 337 RPS y tú exiges 500, la diferencia (163 peticiones por segundo) se acumula: la cola crece, la latencia se dispara, y eventualmente aparecen errores. Eso es exactamente lo que pasa en producción cuando el tráfico supera la capacidad, y solo el modelo abierto lo reproduce en una prueba.
El executor constant-arrival-rate (contenido)
Así se configura, como contenido rotulado (k6 no está instalado):
// CONTENIDO (no ejecutado aquí). Ref: grafana.com/docs/k6 (constant-arrival-rate).
export const options = {
scenarios: {
fixed_demand: {
executor: 'constant-arrival-rate',
rate: 200, // 200 iteraciones...
timeUnit: '1s', // ...por segundo => 200 RPS objetivo
duration: '1m', // sostener esa tasa 1 minuto
preAllocatedVUs: 50, // VUs reservados de entrada para servir la tasa
maxVUs: 300, // techo de VUs si hace falta más para sostener la tasa
},
},
};
Fíjate en cómo la carga se define distinto que en los executors por VUs. No dices "cuántos usuarios", dices "cuántas iteraciones por unidad de tiempo": rate: 200 + timeUnit: '1s' = 200 RPS. Los VUs pasan a ser un recurso que k6 usa para sostener esa tasa, no la variable de control: preAllocatedVUs es cuántos reserva de entrada (crearlos cuesta, así que se reservan antes de empezar) y maxVUs es el techo hasta el que puede crecer si el servidor se pone lento y necesita más VUs para mantener los 200 RPS. Esa inversión —los VUs al servicio de la tasa, no al revés— es la esencia del modelo abierto.
Un detalle que se te puede escapar: si el servidor se satura y k6 llega a maxVUs sin poder sostener la tasa, k6 lo reporta como iteraciones que se quedaron cortas (dropped iterations). Ese número —cuántas peticiones no se pudieron ni lanzar— es en sí una señal de que superaste la capacidad, algo que el modelo por VUs nunca te diría.
El experimento ejecutado: por debajo y por encima de la capacidad
Vamos a los números, que es donde esto se vuelve nítido. Primero necesitamos saber la capacidad de Reservo: cuántos RPS puede sostener. Midiéndolo (24 trabajadores a tope contra /quote):
Qué esperar — el throughput máximo sostenible es la capacidad; por encima de él, la demanda se acumula. Salida real:
capacidad (24 VUs): 1369 peticiones en 4.1s = 337 req/s
La capacidad de este Reservo de laboratorio es ~337 req/s. Ahora aplicamos carga por tasa de llegada a dos niveles: 120 RPS (cómodo, por debajo de 337) y 450 RPS (por encima de 337). El generador mide la latencia experimentada por cada petición: desde el instante en que debía salir hasta que terminó, incluyendo la espera en cola si el servidor no da abasto —justo lo que sentiría un usuario real—.
Qué esperar — a 120 RPS el servidor mantiene el ritmo: latencia baja y estable, todo entregado. A 450 RPS la demanda supera la capacidad: la cola crece, la latencia se dispara a segundos, y algunas peticiones ni siquiera se completan. Salida real:
$ python3.14 arrival_rate.py http://127.0.0.1:PORT
carga por TASA DE LLEGADA contra http://127.0.0.1:PORT/quote
RPS objetivo entregadas p50 (ms) p95 (ms) max (ms)
----------------------------------------------------------
120 480/480 6.0 7.2 7.9
450 1470/1800 100.9 1915.2 6719.5
----------------------------------------------------------
Lee las dos filas, porque cuentan historias opuestas:
-
120 RPS (por debajo de la capacidad):
480/480peticiones entregadas —todas—, con un p50 de6.0 ms, un p95 de7.2 msy un máximo de7.9 ms. Fíjate en lo planos que están esos números: p50, p95 y máximo casi coinciden. Eso es un sistema que mantiene el ritmo: cada petición llega a un mostrador limpio, no hay cola, la latencia es la del servicio puro. La cinta suelta 120 formularios por segundo y el cajero procesa 120; el mostrador nunca se apila. -
450 RPS (por encima de la capacidad): el desastre. Solo
1470/1800entregadas —330 peticiones ni se completaron—, y de las que sí, el p50 ya es100.9 ms(17 veces peor que a 120 RPS), el p95 explota a1915.2 ms(¡casi dos segundos!) y el máximo llega a6719.5 ms(casi siete segundos). Aquí la cinta suelta 450 formularios por segundo pero el cajero solo procesa ~337: los ~113 sobrantes por segundo se apilan, la pila crece durante toda la prueba, y las últimas peticiones esperan detrás de una montaña que llevaba segundos formándose. Por eso el máximo es de segundos: no es una petición lenta, es una petición que esperó su turno detrás de miles.
Con carga por tasa de llegada, cruzar la capacidad no degrada un poco: rompe. Por debajo (120 < 337 RPS) todo es plano y perfecto; por encima (450 > 337 RPS) la cola crece sin freno, el p95 salta de 7 ms a ~1900 ms, y peticiones enteras se pierden. El modelo por VUs jamás habría mostrado esto, porque se habría auto-limitado antes de sobrecargar.
Por qué esto importa: la demanda no espera
La diferencia entre las dos filas es la lección entera del modelo abierto. A 120 RPS el sistema está sano; a 450 RPS está roto —y el punto donde cambia es la capacidad, ~337 RPS—. Un constant-vus nunca te habría dejado ver ese muro, porque los VUs se frenan solos: con 24 VUs, aunque el servidor se ponga lento, nunca le exiges más de lo que puede procesar de forma sostenida (los VUs esperan). El modelo abierto, en cambio, fija la demanda y deja que el servidor sufra las consecuencias —exactamente como el tráfico real, que no reduce su ritmo porque tu servidor esté lento—.
Por eso constant-arrival-rate es el executor correcto cuando tu pregunta es sobre capacidad en términos de tasa: "¿aguanta mi API 500 pedidos por segundo?", "¿a partir de cuántos RPS se cae?". Esas preguntas están en RPS, no en usuarios, y RPS es lo que este executor controla. Cuando piensas en el tráfico de un pico —"esperamos 500 solicitudes por segundo el día de la campaña"—, estás pensando en tasa de llegada, y debes probar con tasa de llegada.
Errores comunes
Probar con VUs una pregunta que es sobre RPS. Qué pasa: alguien quiere saber si la API aguanta 500 RPS y lo prueba con 50 VUs, viendo que "no se cae". Por qué pasa: se confunde concurrencia (usuarios) con tasa (peticiones por segundo). Cómo detectarlo: con VUs, la carga se auto-limita —nunca exiges más de lo que el servidor sostiene—, así que "no se cae" no prueba que aguante 500 RPS; solo que aguanta 50 VUs. Cómo corregirlo: si la pregunta está en RPS, usa constant-arrival-rate con ese rate; es el único modelo que fija la demanda sin auto-limitarse.
Poner maxVUs demasiado bajo y no notar las iteraciones perdidas. Qué pasa: se configura constant-arrival-rate con un maxVUs corto; el servidor se pone lento, k6 necesita más VUs para sostener la tasa, se queda sin ellos y empieza a soltar iteraciones —pero nadie mira ese número—. Por qué pasa: se asume que k6 siempre logra la tasa pedida. Cómo detectarlo: si k6 reporta dropped iterations, no alcanzó la tasa objetivo; tu prueba aplicó menos carga de la que creías (y esa caída es la señal de sobrecarga). Cómo corregirlo: dale a maxVUs un techo generoso y lee las iteraciones perdidas: son parte del resultado, no un detalle técnico.
Creer que el modelo abierto es "mejor" y el cerrado "peor". Qué pasa: alguien concluye que siempre hay que usar constant-arrival-rate. Por qué pasa: es más realista para el tráfico web. Cómo detectarlo: no todo se modela por tasa —un lote de 20 trabajadores fijos procesando una cola es un sistema cerrado real, y ahí los VUs son el modelo correcto—. Cómo corregirlo: usa el modelo que corresponde a tu realidad: tráfico de usuarios que llegan sin coordinarse → tasa de llegada (abierto); un número fijo de agentes/hilos/dispositivos → VUs (cerrado).
Ejercicios
Ejercicio 1 — ¿Abierto o cerrado? Para cada situación, di si la modela mejor el modelo abierto (tasa de llegada) o el cerrado (VUs). (a) "Miles de usuarios web llegan a cotizar sin coordinarse entre sí." (b) "Una flota de 10 dispositivos IoT, cada uno manda una lectura, espera la confirmación, y manda la siguiente." (c) "Esperamos 800 pedidos por segundo el día del lanzamiento." (d) "20 trabajadores de un pool consumen tareas de una cola, uno tras otro."
Ver solución
- (a) Abierto (tasa de llegada). Usuarios que no se coordinan llegan a un ritmo propio, independiente de la velocidad del servidor: es la cinta transportadora.
- (b) Cerrado (VUs). Cada dispositivo espera su confirmación antes de la siguiente lectura: se auto-limita, hay exactamente 10 "usuarios". Es una fila.
- (c) Abierto (tasa de llegada). La pregunta está en RPS (800 pedidos/segundo): fijas demanda, no usuarios.
- (d) Cerrado (VUs). 20 trabajadores que procesan uno tras otro son un sistema cerrado real; nunca hay más de 20 tareas en vuelo.
Ejercicio 2 — Lee el experimento. Con la salida ejecutada, responde: (a) ¿Por qué a 120 RPS el p50, el p95 y el máximo están tan juntos (6.0 / 7.2 / 7.9 ms)? (b) A 450 RPS se entregaron 1470 de 1800: ¿qué pasó con las 330 restantes y por qué? (c) ¿Qué número marca la frontera entre "sano" y "roto", y de dónde salió?
Ver solución
- (a) Porque a 120 RPS (por debajo de la capacidad de ~337) el servidor mantiene el ritmo: no hay cola, cada petición ve solo el tiempo de servicio puro. Sin cola, la distribución de latencia es estrecha, así que p50, p95 y máximo casi coinciden.
- (b) Las 330 peticiones que faltan no se completaron: a 450 RPS contra una capacidad de ~337, la demanda superó lo que el servidor podía procesar, la cola creció sin freno y esas peticiones fallaron o no llegaron a terminar dentro de la prueba. Es la firma de la sobrecarga: no solo latencia alta, sino trabajo perdido.
- (c) La capacidad, ~337 RPS, medida como el throughput máximo sostenible (1369 peticiones en 4.1 s). Por debajo (120) todo va bien; por encima (450) se rompe. La frontera es la capacidad.
Ejercicio 3 — Configura la prueba. Quieres verificar si Reservo aguanta la demanda esperada de un lanzamiento: 300 peticiones por segundo durante 2 minutos. Escribe el scenarios con constant-arrival-rate y explica por qué elegiste ese executor y no constant-vus.
Ver solución
export const options = {
scenarios: {
launch_demand: {
executor: 'constant-arrival-rate',
rate: 300,
timeUnit: '1s', // 300 RPS
duration: '2m',
preAllocatedVUs: 100,
maxVUs: 500, // techo generoso por si el servidor se pone lento
},
},
};
Se elige constant-arrival-rate porque la pregunta está en RPS (300 peticiones por segundo), que es la demanda real esperada, y este executor fija esa demanda sin auto-limitarse. Con constant-vus no podrías: los VUs se frenarían si el servidor se satura, y nunca sabrías si aguanta la demanda —solo si aguanta ese número de usuarios—. Como 300 RPS está por debajo de la capacidad medida (~337), lo esperable es que aguante, pero con poco margen; valdría la pena probar también 350 o 400 para ver dónde se rompe.
Resumen y siguiente paso
En esta lección conociste el executor que cambia la pregunta de raíz. Los executors por VUs (lección 5) fijan cuántos usuarios hay; constant-arrival-rate fija cuántas peticiones por segundo llegan —la tasa de llegada—. Detrás hay dos modelos del mundo: el cerrado (VUs, donde cada usuario espera su respuesta y la carga se auto-limita, como una fila) y el abierto (tasa de llegada, donde las peticiones entran a ritmo fijo y se acumulan si el servidor no da abasto, como una cinta transportadora). El modelo abierto modela mejor el tráfico real, porque los usuarios no dejan de llegar porque tu servidor esté lento.
Lo viste con números que no dejan lugar a dudas: contra un Reservo de capacidad ~337 RPS, una carga de 120 RPS dio latencias planas y perfectas (p95 7.2 ms, todo entregado), pero una de 450 RPS —por encima de la capacidad— rompió el sistema: p95 de 1915 ms, máximo de casi siete segundos, y 330 peticiones perdidas. El modelo por VUs jamás habría mostrado ese muro, porque se habría auto-limitado antes de sobrecargar. Por eso, cuando tu pregunta está en RPS ("¿aguanta 500 pedidos por segundo?"), debes probar con tasa de llegada.
Antes de avanzar deberías poder: explicar la diferencia entre el modelo abierto y el cerrado con la analogía de la cinta y la fila; decir por qué el modelo por VUs no puede sobrecargar más allá de N; leer los campos de constant-arrival-rate (rate, timeUnit, preAllocatedVUs, maxVUs); e interpretar por qué cruzar la capacidad rompe en vez de degradar suave.
Lo que sigue es juntar todo lo del módulo en una sola decisión práctica. Ya conoces las formas (lección 4) y los executors (5 y 6); en la lección 7 aprendes a elegir el perfil según la pregunta: qué forma y qué executor usar para un load test, un stress test, un spike test o un soak, y cómo se combinan.
Recursos
- k6 — Executor
constant-arrival-rate— el executor de esta lección, conrate,timeUnit,preAllocatedVUsymaxVUs. - k6 — Open vs closed models (modelos abierto y cerrado) — la explicación oficial de la diferencia entre fijar VUs y fijar tasa de llegada; el corazón conceptual de esta lección.
- k6 — Dropped iterations — qué son las iteraciones perdidas y por qué son una señal de sobrecapacidad.
concurrent.futures.ThreadPoolExecutor— documentación de Python — el pool con el que el generador sostiene una tasa de llegada dejando crecer la cola cuando el servidor se satura.