Pruebas de carga
Curso de escalabilidad v3, día 2
Programa
Uso de herramientas
Herramientas a medida
Variabilidad y percentiles
Leyes de Pareto y Amdahl
Incertidumbre y error
Apache ab
$ ab -n 1000 -c 10 http://service.pinchito.es/a
Opciones:
-
-n [N]
: número total de peticiones
-
-c [C]
: concurrencia, número de hilos
-
-t [T]
: tiempo de prueba
wrk
Diseño multi-hilo
$ wrk -t10 -c400 -d30s http://service.pinchito.es/a
Opciones:
-
-t[T]
: usa T hilos (threads)
-
-c[C]
: número de conexiones abiertas
-
-d[D]
: duración de la prueba (con unidades)
wrk2
Añade una nueva opción --rate
o -R
$ wrk -t10 -c400 -d30s --rate 2000 http://service.pinchito.es/a
Opciones:
-
--rate [R]
: tasa de peticiones por segundo
-
--latency
: muestra latencia con percentiles
Modos de carga
Modo "amable": espera a que termine una petición
antes de lanzar otra
Simula sistemas batch
Modo "agresivo": lanza R peticiones por segundo
incluso aunque no hayan terminado las anteriores
Simula sistemas online
Un servicio mal diseñado aguantará mucho menos
en el modo agresivo que en el amable
loadtest
Contribuciones de 41 devs
Modo agresivo: --rps
$ loadtest -n 1000 -c 10 --rps 2000 --keepalive http://service.pinchito.es/a
Opciones:
-
-n N
: número total de peticiones
-
-c C
: concurrencia
-
-t T
: tiempo en segundos
-
--rps R
: peticiones sostenidas por segundo
autocannon
Inspirado por wrk, wrk2
Modo "agresivo": -R
$ autocannon -c 400 -d 10 -R 2000 http://service.pinchito.es/a
Opciones:
-c C
: conexiones
-d S
: duración en segundos
-R R
: peticiones por segundo
Ejercicio: Pruebas de carga
Crea una instancia en AWS EC2
Región eu-south-2, Europe (Spain)
Tipo t3-small (o superior)
Imagen service.pinchito.es-2023-10-16
Usa ab, wrk, loadtest, autocannon
Comprueba si la frecuencia "natural" es la misma
⮯
Ejercicio +
Lanza pruebas de carga a tasa de RPS constante
$ autocannon -R [R] ...
$ loadtest --rps [R] ...
Usa una tasa algo por encima y por debajo de la frecuencia natural
Comprueba si los resultados son consistentes
Si varían, ¿por qué lo hacen?
⮯
Ejercicio +
¿Cambian los resultados?
¿Por qué?
⮯
¡Crea tu propio loadtester!
Lanza peticiones concurrentes http
Recoge las respuestas:
- 200: OK
- 204: sin respuesta
- 301: moved permanently
- 302: moved
- 4xx: bad request
- 5xx: server error
Modo amable: cada hilo envía peticiones seguidas
Proyecto lambda-loadtester
Pruebas de AWS Lambda
Tasa de RPS fija
Modo agresivo: R peticiones por segundo
Una petición cada 1000/R milisegundos
Se puede usar por ejemplo setInterval() en Node.js
¿Qué pasa cuando 1000/R es una fracción?
Ejercicio: Diseña un temporizador
Modo "agresivo" de peticiones fijas
Primero intenta con una tasa redonda: 10 rps
Estudia cómo poner un intervalo
¿Qué pasa si una petición tarda demasiado?
¿Qué pasa cuando hay un retraso del sistema?
⮯
Ejercicio +
Ahora intenta que funcione con tasas fraccionales, e.g. 400 rps
¿Cómo gestionas fracciones de milisegundo?
⮯
Ejercicio +
Ahora piensa que tienes que lanzar más de 1000 rps
¿Se puede garantizar que la tasa sea ± constante?
¿Se puede garantizar al menos que el retraso no se acumule?
⮯
Variabilidad y percentiles
Ejemplo: altura
Distribución de alturas en USA,
fuente
Fuente inagotable de preocupación entre padres novatos
Distribución de alturas
Distribución "normal"
Distribuciones importantes
Gaussiana (normal)
Poisson: llegada aleatoria
Binomial: monedas aleatorias
Zipf: frecuencia de palabras
Pareto: riqueza
Percentiles y servicio
La media da una idea sesgada de la experiencia
Los percentiles cuentan una historia más completa
Si hablamos del tiempo por petición:
- Percentil 50: la mitad de las peticiones están por encima
- Percentil 90: 1 de cada 10 peticiones está por encima
- Percentil 99: 1 de cada 100 peticiones por encima
Mal percentil 99, a 100 peticiones por segundo:
Estamos cometiendo 1 pifia cada segundo
Ejercicio: Tiempo total
Una consulta utiliza 10 servidores en paralelo
El percentil 50 es 50 ms
El percentil 90 es de 200 ms
Estima una cota mínima para el tiempo medio por consulta
⮯
Ejercicio +
xm = 28 ms
α = 1.16
Como función U puedes usar Math.random()
⮯
Ejercicio +
Simula 100k muestras de Pareto
Calcula la media, mínimo y máximo
Calcula los percentiles 5, 50, 90, 95, 99, 99.9
Nota para calcular percentiles:
- Ordena el array (¡como números!)
- Para el percentil 50%, vete a la mitad del array
- Para el percentil 90% vete a la posición 90k
- ...
⮯
Ejercicio +
Ahora simula peticiones con 10 llamadas a servidores en paralelo
= el máximo de 10 muestras de Pareto
Calcula mínimo, media, percentiles 50 y 90
¿Son iguales que antes?
⮯
Ejercicio +
Por fin, simula peticiones con 10 llamadas a servidores en serie
Será el resultado de sumar 10 muestras de Pareto
Calcula el percentil 50
¿Es 10 veces el valor de antes?
⮯
Ejercicio +
Para notísima: simula una petición a 10 servidores en serie,
cada una de las cuales hace 10 en paralelo
¿Cuáles son media, mínima, percentil 50?
⮯
Ejercicio +
Esta última parte no es ciencia ficción
Tiempo total 200 ms
¿Se te ocurre alguna forma de mejorar la respuesta?
⮯
Paquete pareto-simulator
Instala pareto-simulator:
$ npm i -g pareto-simulator
Ahora prueba con unos cuantos comandos:
$ pareto --xm 28
$ pareto --xm 28 --parallel 10
$ pareto --xm 28 --series 10
$ pareto --xm 28 --series 10 --parallel 10
$ pareto --xm 1 -n 1000 --parallel 30 --series 30 --timeout 10 --linear
¿A qué distribuciones te recuerdan?
Regla del 80/20
Un 20% de las causas genera el 80% de los efectos
Aplica a un amplio espectro de fenómenos:
Trabajo de optimización
Toca el duro trabajo de optimizar un servicio
Consume demasiado/a:
- CPU
- memoria
- descriptores de fichero
- búffers de entrada/salida
- ...
¿Por dónde empezamos?
Localizamos los 🔥hot spots🔥
¡Qué suerte!
Los 🔥hot spots🔥 siguen el principio de Pareto
Un 20% del código ocupa el 80% del tiempo de proceso
La ley aplica recursivamente...
Un 4% del código ocupa el 64% del tiempo de proceso
Una ley en apariencia complicada
Ejercicio: Límites de optimización
Petición get de nodecached: 37250 rps
¿Cuánto podemos acelerar el servidor nodecached?
⮯
Ejercicio +
Nos centramos en la operación get:
- 2 µs: conversión a string
- 6 µs: procesamiento interno
- 7 µs: input
- 12 µs: output
Suponemos que optimizamos string y procesamiento (0µs)
¿Cuánto es el máximo teórico de peticiones por segundo?
⮯
Ejercicio +
Fórmula: R [rps] = 1000000 / t [µs]
Ejercicio de imaginación:
¿Qué estrategias podemos seguir para optimizar más?
⮯
Ley de retornos decrecientes
Según avanzamos en la optimización el retorno de inversión disminuye
Ley poco rigurosa (pero útil)
Cuidado con la micro-optimización
Error sistemático o aleatorio
Error sistemático
Error aleatorio
Ejercicio: Distribución real
Queremos medir la distribución de tiempos
⮯
Ejercicio +
Bájate a local el proyecto loadtest
(Lo tienes preparado en la imagen pinchito-loadtest)
Trúcalo para que pinte el tiempo de cada petición
(También en la imagen)
Lanza las pruebas:
node bin/loadtest.js http://service.pinchito.es/a -n 15000 -c 100 --rps 300
Usa rps por encima y por debajo del valor sin --rps (e.g. 300 y 500)
⮯
Ejercicio +
Saca el resultado a fichero y extrae los valores numéricos
$ comando | grep -v INFO > service-times-300.csv
Dibuja un histograma con los valores
Dibuja un histograma log-log
¿Qué te dicen las gráficas?
⮯
Perfilado con microprofiler
Permite ver en qué se va el tiempo de proceso
Se instrumentan secciones de código:
const start = microprofiler.start()
Empieza a medir
microprofiler.measureFrom(start, 'label', 10000)
Mide el tiempo entre start()
y measureFrom()
Pinta un resumen para label
cada 10000 llamadas
Perfilado nativo
Node.js incluye también un
profiler majo:
Esto genera un fichero como isolate-0x440c2f0-28473-v8.log
Se puede interpretar con el comando
node --prof-process isolate-0x440c2f0-28473-v8.log
La salida tiene tres partes:
-
[Summary]
: Resumen de tiempos
-
[JavaScript]
, [C++ entry points]
: Secciones desglosadas
-
[Bottom up (heavy) profile]
: Perfil jerárquico
Paquetes de flamegraphs
Demo en directo
Ejercicio: Ahorrando microsegundos
¿Dónde se nos va el tiempo?
Vamos a ver dos enfoques alternativos
⮯
Ejercicio +
Clona el paquete pareto-simulator
$ git clone https://github.com/alexfernandez/pareto-simulator.git
$ cd pareto-simulator
Léete las instrucciones
⮯
Ejercicio +
const microprofiler = require('microprofiler');
...
computeSamples() {
for (let i = 0; i < options.number; i++) {
const start = microprofiler.start()
const sample = this.computeSample()
microprofiler.measureFrom(start, 'sample', 10000)
this.samples.push(sample)
this.sum += sample
if (sample > this.max) this.max = sample
if (sample < this.min) this.min = sample
microprofiler.measureFrom(start, 'stats', 10000)
}
}
Ejecuta y mira los resultados:
$ node index.js --xm 1 -n 1000 --parallel 30 --series 30 --timeout 10 --linear
⮯
Ejercicio +
Ahora instrumenta la función computeSample()
Mira a ver si hay alguna sorpresa
Intenta buscar alguna optimización...
Y vuelve a medir
⮯
Ejercicio+
En este último paso vamos a ejecutar el profiler de Node.js:
$ node --prof index.js --xm 28 -n 10000000
Y ahora interpretamos la salida:
$ node --prof-process isolate-0x...-v8.log
¿Te llama algo la atención?
¿Se te ocurre cómo optimizarlo?
¿Es este profiler igual de ágil que microprofiler?
⮯