EN
Artículo26 oct 202624 min de lecturaopinión

Cuando el test de carga es "ya veremos en producción"

Por qué no hacer pruebas de carga serias es la forma más cara de aprender: consultas N+1, bloqueos, GC y picos que nadie simuló.

Contenido

Hay una especie muy particular de optimista en tecnología: esa persona que, mirando un gráfico de usuarios concurrentes, te dice con toda la calma del mundo: “bah, si va mal, escalamos”. Y por “escalamos” se refiere a reiniciar el pod a mano mientras le tiembla el ratón, con el Slack ardiendo y el director de negocio preguntando cuánto dinero se está perdiendo por minuto.

El problema es que, cuando llegan días como Black Friday, Prime Day o el lanzamiento de “la nueva campaña que va a revolucionar el sector”, no hay Excel motivacional que compense no haber hecho pruebas de rendimiento en serio. Lo que no has probado antes, te lo va a enseñar producción, pero con intereses.

Vamos a hablar de eso: de no definir SLO, de no usar datos reales, de no simular picos y de pensar que el ORM es un mago que convierte consultas horribles en algo óptimo. Y lo voy a mezclar con ejemplos reales de caídas sonadas, y con tripas: consultas N+1, bloqueos, GC desmadrado y demás fauna.

Black Friday: cuando “ya veremos” sale en la portada

Black Friday se ha convertido en una prueba de estrés mundial… pero en producción. Año tras año pasa lo mismo: muchas webs caen, y casi nunca por errores funcionales: caen porque nadie se ha molestado en validar cómo se comporta aquello con tráfico de verdad.

Quien se dedica a analizar estas caídas lo dice sin rodeos: casi todos los cuellos de botella están en el software, no en el hardware, así que no se arreglan echando más servidores a un sistema que no está hecho para escalar así. No es “un fallo raro que no se podía prever”.

Por ejemplo, en el fin de semana de Black Friday de 2018 se cayeron o se arrastraron las webs de Walmart, J.Crew o Lululemon, y también hubo quejas con Lowe’s y Ulta.

En el caso de J.Crew, según las estimaciones de LovetheSales.com, la caída le costó unos 775.000 dólares en ventas solo en las primeras cinco horas. Los problemas de Walmart, que empezaron la víspera de Acción de Gracias, afectaron a unos 3,6 millones de compradores, con unos 9 millones de dólares en ventas perdidas. Eso es quemar dinero en directo.

Ni siquiera los gigantes se libran. RedLine13 incluyó a Macy’s entre los grandes patinazos de 2016 en pruebas de carga: la web y la aplicación de una de las mayores cadenas de grandes almacenes del mundo se cayeron justo el día más importante del año, con las campañas preparadas y los anuncios en marcha. Nadie sabe exactamente cuánto dinero se perdió, pero podemos suponer que el suficiente como para que más de uno tuviera una conversación… intensa.

Y lo que más escuece es que no apareció ningún fallo raro. Lo que había eran pruebas de carga poco realistas, una infraestructura dimensionada a ojo, y un código y unas dependencias que iban justitos el resto del año y que se hundieron con el pico. Si tú no diseñas tu test de carga, la realidad te va a diseñar uno mucho más creativo… y mucho más caro.

En el mundo hispano también se empieza a ver la misma película: consultoras y proveedores que dan servicios de pruebas de rendimiento relatan casos reales de tiendas online que se desploman el día de la campaña porque “nunca se habían probado con tráfico concurrente alto”, confiando en que el hosting “debería aguantar”.

En uno de estos casos, nadie había probado una tienda online con más de 100 usuarios concurrentes antes de un evento tipo Black Friday; el día señalado, el sitio dejó de responder justo en el momento de máxima demanda. No hace falta ver el registro de errores para saber que el problema no era un NullPointerException perdido. Si tu única métrica es que “con cien usuarios va bien”, lo que tienes es un deseo.

Y, aun así, el patrón se repite: no se definen objetivos claros de rendimiento, no se simula el tráfico real de los usuarios, no se prueban los picos y se confía en que “la nube escala sola”.

Spoiler: la nube no escala sola si tu código hace cosas absurdas.

El pecado original: no tener SLO ni preguntas incómodas

La raíz del problema suele estar mucho antes del Black Friday. Empieza el día en que nadie se atreve a hacer las preguntas feas al principio del proyecto. Cosas como:

  • ¿Qué tiempo de respuesta máximo aceptamos en el pago bajo carga normal?
  • ¿Y bajo pico?
  • ¿Cuántos usuarios concurrentes esperamos? ¿Cuál es el escenario extremo?
  • ¿Cuál es la tasa de errores tolerable?

Las buenas prácticas de pruebas de rendimiento serias, como las que describen IBM o AWS, empiezan exactamente por ahí: identificar objetivos de la aplicación, las métricas que importan, cargas objetivo y umbrales aceptables. Pero en muchos equipos se prefiere hablar de funcionalidades nuevas antes que de estos detalles “aburridos”.

Las respuestas a esas preguntas son los SLO (Service Level Objectives, objetivos de nivel de servicio). Sin SLO, cualquier cosa “funciona”.

Si nadie ha dicho que el pago tiene que ir por debajo de 500 ms en el 95 % de las peticiones con 1.000 usuarios concurrentes, entonces ese pago que tarda 4 segundos con 300 usuarios “bueno, va justito, pero vale”. Hasta que llega el pico de tráfico, se ponen 10 veces más usuarios y de repente todo está por encima de los 10 segundos y empiezas a ver tiempos de espera agotados, reintentos y efectos en cascada.

La ironía es que las propias guías de pruebas de carga insisten en que hay que definir métricas de tiempo de respuesta, utilización de recursos, capacidad máxima y puntos de ruptura antes de empezar a lanzar scripts. Pero claro, eso implica sentarse a pensar, y pensar es menos vistoso que enseñar un tablero de Kanban lleno de post-its.

Una regla sencilla: si no eres capaz de escribir tus objetivos de rendimiento en un papel, lo que estás haciendo es rezar.

Datos de mentira, resultados de mentira

Otro clásico: las pruebas se hacen con datos de juguete. Ese entorno que tiene 300 usuarios ficticios, 20 pedidos y tres productos de catálogo, y con eso se valida el rendimiento de un sistema que en producción va a tener millones de registros.

El problema está más que documentado: muchas patologías de rendimiento (como el de las consultas N+1) no se ven hasta que el volumen de datos crece. Cuando tienes 10 filas, puedes permitirte casi cualquier burrada. Cuando tienes 10 millones, cada consulta tonta es una bomba de relojería.

Los artículos técnicos sobre las consultas N+1 lo señalan: con pocos datos, la degradación es casi imperceptible, pero cuando el número de objetos relacionados crece, el coste se dispara y el rendimiento se hunde. Y, por supuesto, eso no lo vas a ver si tu entorno de preproducción está vacío o poblado con datos irrelevantes que nada tienen que ver con los patrones reales de producción.

Las guías serias sobre pruebas de carga recomiendan construir escenarios realistas basados tanto en datos de usuarios (navegadores, dispositivos, recorridos) como de sistema (TTFB, uso de CPU, I/O, etc.), usando datos representativos o anonimizados de producción. Pero muchas veces se rellena la base de datos con un script improvisado que repite el mismo nombre una y otra vez y se da por buena la simulación.

Si tu catálogo real tiene miles de combinaciones raras, históricos largos, carritos abandonados y datos sucios, y tu entorno de pruebas es un “Hello, World” con cuatro registros limpios, el test de carga te va a mentir con una sonrisa.

Consultas N+1: el ORM no es tu abogado

Hablemos del clásico: las consultas N+1. Ese patrón entrañable en el que haces una consulta para obtener una lista de entidades, y luego, dentro de un bucle, accedes a una relación perezosa… que dispara una consulta nueva por cada elemento.

La literatura sobre ORM lo repite hasta la saciedad: el problema de N+1 es una ineficiencia habitual que genera 1 consulta principal más N consultas adicionales para cargar las relaciones, y que castiga de forma brutal el rendimiento.

En artículos de gente que trabaja con Doctrine, Entity Framework o similares se ve el mismo patrón: por priorizar la simplicidad del código (“ya lo hará el ORM”) se acaba lanzando una lluvia de consultas que, con datos pequeños, pasa desapercibida, pero con volúmenes grandes convierte el sistema en una factoría de I/O absurdo.

El problema se agrava cuando combinas N+1 con días de pico. Durante un Black Friday, por ejemplo, el número de peticiones concurrentes sube mucho. Si cada petición arrastra N+1 consultas, los recursos de la base de datos se agotan, la latencia sube, empiezan los tiempos de espera y, de propina, el pool de conexiones se queda sin oxígeno.

Algunas consultoras, como Deloitte, han publicado artículos sobre cómo optimizar los ORM en los que señalan el N+1 como un problema crítico de rendimiento y explican cómo el uso de lazy loading sin control acaba generando un número enorme de conexiones y consultas en escenarios con muchas relaciones. Sí, la misma Deloitte que tuvo que devolver parte del dinero al Gobierno australiano por un informe con citas que se había inventado la IA. Tranquilidad: lo del N+1 lo he comprobado yo.

Otros autores, en blogs técnicos, ilustran casos reales en los que este patrón llevaba a crecimientos casi explosivos de consultas y tiempos de respuesta, hasta que se sustituyó por consultas con JOIN bien diseñadas.

Ahora imagínate esto en un pico de tráfico: cada petición multiplica el coste en la base de datos, la saturas y provocas un efecto dominó que ya no se arregla “añadiendo un nodo más”. Eso es SQL mal planteado, y la infraestructura no lo arregla.

¿Y cómo se arregla? Para empezar, detectándolo en las pruebas de carga: mide cuántas consultas lanza cada petición, porque ese número delata un N+1 mucho antes que la latencia. Después, usa JOIN bien pensados, pide solo los datos que hacen falta o carga las relaciones por adelantado, pero con cabeza. Y, sobre todo, reescribe las consultas para traer lo que necesitas y no el universo entero. Es muchísimo más barato arreglarlo en la primera prueba de carga que enterarte con la gente gritando en las redes.

Bloqueos por todas partes: tus transacciones también hacen cola

Otra fuente maravillosa de sorpresas en producción son los bloqueos. Si has trabajado con bases de datos relacionales, sabes que las transacciones y los bloqueos son una especie de juego de sillas: mientras haya pocas personas, nadie se pelea; cuando llega Black Friday y entra medio país, se reparten hostias.

Las guías de pruebas de carga y resiliencia (por ejemplo, las de AWS Well-Architected) recomiendan validar cómo escala tu arquitectura de datos, detectar cuellos de botella en la base de datos y observar métricas como tiempos de bloqueo, esperas y contención antes de ir a producción. Pero esa parte suele ignorarse porque “en local va rápido”.

El antipatrón de siempre es una tabla de inventario con una única fila por producto, actualizaciones con transacciones largas y un SELECT ... FOR UPDATE alegre, y un pico de peticiones que intentan restar stock a la vez.

Si no has simulado esa concurrencia, lo que vas a descubrir el día de la campaña es que las peticiones atendidas por segundo se desploman, las transacciones empiezan a esperar, la API se queda sin hilos libres, y toda la web parece colgada. Aunque lo único que hace es esperar. Todo esto se podría haber visto en pruebas de carga que introdujeran patrones de uso real, midiendo tiempos de bloqueo y latencias bajo alta concurrencia.

Claro que eso implica saber qué hace realmente tu ORM y cómo se traduce en SQL. Mucho más cómodo asumir que nunca van a pulsar a la vez el botón de pagar miles de personas que quieren la misma oferta.

Lo que se debería haber hecho tiene tres pasos. Primero, pruebas de carga que actualicen el inventario como lo hacen los usuarios de verdad, todos a por la misma oferta. Segundo, medir cuánto duran los bloqueos y cuánto tardan las respuestas cuando hay mucha gente a la vez. Y tercero, cambiar el modelo de datos, con reservas, colas o eventos, para que no compita todo el mundo por la misma fila. No puedes poner a medio país a bloquear la misma fila y esperar que aquello vaya fluido.

GC descontrolado: cuando el recolector decide hacer huelga

En lenguajes con recolección de basura, otra fuente de sustos de “ya veremos en producción” es la gestión de memoria.

Un escenario habitual: una aplicación Java o similar, con objetos grandes, cachés en memoria sin control y serializaciones innecesarias. Con poca carga, la JVM lo lleva bien y el GC apenas molesta. Con mucha carga, la memoria se llena, aumenta la presión y el recolector entra en pausa de “a ver, todos quietos que voy a recoger”.

Lo que dice la teoría de pruebas de rendimiento es claro: hay que monitorizar el uso de memoria, el comportamiento del GC y sus pausas, y hacerlo con cargas crecientes, no solo con tráfico de juguete. Esos mismos textos recomiendan pruebas de estrés que lleven el sistema a sus límites para observar cómo se comporta, dónde aparecen las colas y qué subsistemas caen primero.

Pero en muchos sitios, las pruebas de rendimiento consisten en “lanzamos un JMeter/Gatling 5 minutos, parece que va bien, siguiente historia”. Eso, más que una prueba, es un masaje. Mientras tanto, la realidad en producción es que el GC entra en ciclos cada vez más largos (por fugas de memoria, por cachés mal dimensionadas, por estructuras que nunca se liberan) y, en el momento del pico de tráfico, las pausas del GC son tan brutales que las peticiones empiezan a agotar su tiempo de espera.

Aquí la gracia es que el problema no es visible con 100 usuarios, pero sí con 5.000. Y si no has hecho pruebas a varias veces el tráfico esperado, te vas a enterar cuando intentes comprar en tu propia tienda para ver si el pago funciona y ni siquiera te cargue la página.

Picos no simulados: el Black Friday no es “tráfico normal un poco más alto”

Otra trampa mental: creer que Black Friday es “lo de siempre, pero multiplicado por dos”.

Las plataformas de rendimiento y los especialistas en comercio electrónico llevan años explicando que esos días el patrón de tráfico cambia por completo: ráfagas intensas en ventanas concretas, campañas que disparan miles de usuarios a la vez, una forma distinta de navegar. Las empresas que analizan el tráfico de eventos como Prime Day muestran cómo las curvas de tráfico y ventas tienen picos muy marcados, muy por encima de la media de los días normales.

No puedes simular eso lanzando un test en el que subes usuarios poco a poco hasta un nivel “razonable”. Black Friday no es una rampa, es un muro.

Los manuales de pruebas de rendimiento hablan de pruebas de carga con progresión escalonada, pero también de pruebas de estrés y de escenarios que lleven el sistema más allá de los niveles normales para encontrar el punto de rotura y el comportamiento en saturación. Y para eventos como Black Friday recomiendan empezar cuanto antes, simular picos realistas y repetir las pruebas cada vez que se corrige un cuello de botella, porque prepararse para ese día es un proceso iterativo.

Y, sin embargo, muchas organizaciones se quedan en el “hemos probado con el doble de usuarios y parece estable”. Luego llega el gran día y reciben entre 5 y 10 veces el tráfico normal en un intervalo corto… y adivina qué pasa.

Y no solo cuenta el volumen. La mezcla de dispositivos, rutas y casos de uso también cambia esos días, y si tus pruebas no la reproducen, la saturación aparecerá en el servicio que nadie había mirado.

Yo tampoco puedo tirar la piedra

Antes de que parezca que miro todo esto desde arriba, te confieso una cosa. Dicen que el que esté libre de pecado tire la primera piedra. Yo esa piedra no la puedo tirar, porque también he hecho alguna prueba de carga en producción. Sin querer, eso sí, que es como se hacen casi todas.

Fue en un proyecto en el que participé como arquitecto. No voy a decir cuál, pero es bastante probable que lo conozcas. Esperábamos picos de cientos de miles de usuarios en una ventana de diez segundos. Hicimos pruebas de carga, muchas y muy bien hechas, o eso creíamos. Eran de las optimistas, de esas en las que los usuarios van subiendo poco a poco, con su rampa preciosa. Justo lo que acabo de decir que no se hace.

Encontramos fallos, ojo. Los corregimos, ajustamos la infraestructura y llegamos al día del lanzamiento bastante tranquilos. Y durante medio minuto, aquello se nos fue de las manos.

Lo que no habíamos tenido en cuenta es que la infraestructura de nuestro proveedor, por cómo estaba diseñada, escalaba deprisa, pero no tan deprisa como necesitábamos. Y que conste que escalaba de maravilla: aquel día aguantó dos millones de peticiones en un minuto. Pero nuestro muro llegaba en diez segundos, y el escalado necesitaba algo más.

Lo más doloroso, visto con distancia, es que el problema sí había salido en las pruebas. Nada más empezar cada prueba veíamos unos cuantos errores, y luego todo en verde. ¿Por qué? Porque, después de escalar, la infraestructura se queda un rato con esa capacidad extra antes de volver a encogerse: es el tiempo de enfriamiento, o cooldown.

Cuando lanzábamos la siguiente tanda, las máquinas seguían ahí, bien calentitas, esperándonos, y nosotros, encantados de la vida, convencidos de que aquello ya estaba arreglado. Estábamos probando el muro con la infraestructura ya caliente. Y el día del lanzamiento llegó fría.

Medio minuto parece poco. Ahora imagínate ese medio minuto con cientos de miles de personas intentando entrar a la vez, y con todo el equipo delante de los paneles tirándose de los pelos, yo el primero.

La lección me la llevé marcada a fuego: el pico hay que probarlo en frío. Hay que dejar que la infraestructura vuelva a su tamaño normal entre prueba y prueba, y no fiarse nunca de una segunda tanda que sale bien. Tus usuarios no van a esperar a que tu sistema entre en calor.

Casos reales en los que la falta de pruebas de rendimiento se cobró su peaje

A veces, la falta de pruebas de rendimiento aparece directamente como la causa principal del desastre.

Por ejemplo, el de Ello. Si no te suena, tranquilo, que a mí tampoco me sonaba hasta que me puse a preparar este artículo. Fue una red social que, en 2014, se vendía como el anti-Facebook: sin anuncios y sin vender los datos de nadie. Solo se podía entrar con invitación, y en su mejor momento llegó a recibir más de treinta mil peticiones de invitación por hora. Se puso de moda durante un rato. Un rato cortito, para qué nos vamos a engañar.

Se ha documentado que sus servidores no pudieron soportar la carga porque no se habían hecho pruebas a la altura del crecimiento que esperaban. El resultado: caídas frecuentes, rendimiento pobre y una fuga masiva de usuarios, atribuida en buena parte a no haber identificado cuellos de botella antes del lanzamiento mediante pruebas de carga en serio. Si tu producto tiene éxito y tu infraestructura no, el usuario se va con el producto que funcione mejor.

La moraleja es directa: lo caro no fue hacer las pruebas, lo caro fue no hacerlas.

Lo que sí funciona: tratar el rendimiento como requisito, no como decoración

Si te vas a la literatura más seria (IBM, AWS, guías de pruebas de rendimiento, blogs especializados en rendimiento), hay un consenso bastante claro sobre lo que hay que hacer si no quieres jugar a la ruleta rusa en producción.

El rendimiento es un requisito más, tan exigible como los funcionales, con objetivos definidos, y se valida continuamente. Eso implica, por ejemplo:

  • Definir métricas y SLO: tiempos de respuesta, capacidad de proceso, tasas de error, utilización de recursos, colas, etc., distinguiendo entre la carga normal y los picos esperados.
  • Construir escenarios realistas: con datos representativos, patrones de navegación reales y dispositivos/plataformas similares a producción.
  • Realizar pruebas de carga, estrés y resiliencia: empezando por cargas inferiores a las habituales, subiendo progresivamente, luego yendo más allá de lo esperado para detectar puntos de fallo y comportamientos bajo saturación.
  • Monitorizar durante las pruebas: métricas de sistema (CPU, memoria, disco, GC), de aplicación (latencias por endpoint, colas, bloqueos), y de experiencia de usuario (tiempo hasta el primer byte, tiempos de carga percibidos).
  • Revisar y optimizar el código crítico: sobre todo el acceso a datos con ORM, eliminando consultas N+1, ajustando políticas de carga perezosa/ansiosa, y diseñando consultas adecuadas para los casos de uso reales.

Bajado a tierra, para una tienda online una estrategia razonable empezaría por fijar un objetivo para el pago, del tipo “el 95 % de las peticiones, en menos de medio segundo, hasta tantos usuarios”. Luego llenarías preproducción con datos reales, pero anonimizados, y escribirías recorridos de usuario realistas que naveguen, busquen, filtren, añadan al carrito y paguen. Lanzarías pruebas de carga hasta el tráfico normal más un margen, y después pruebas de estrés de entre cinco y diez veces el tráfico esperado en los momentos de más tráfico, empezando cada una con la infraestructura en frío.

Mientras tanto, vigilarías el procesador, la memoria, el recolector de basura, cuántas consultas lanza cada petición, los bloqueos y las colas, y con todo eso arreglarías lo que salga: consultas N+1, índices, cachés, modelos de datos o la configuración de las conexiones. Es trabajo, sí. Pero es muchísimo menos trabajo que dar explicaciones después de un fracaso sonado.

Para eventos como Black Friday, eso significa probar a múltiplos considerables del tráfico esperado. Bob Buffone, CTO de Yottaa, lo resumía así en 2018: una web que no se ha probado a cinco veces su tráfico normal probablemente caerá. Yo apuntaría a entre cinco y diez. Y hay que repetir las pruebas periódicamente y no confiar en “hicimos un test el año pasado”. Las arquitecturas cambian, el catálogo cambia, las campañas cambian; pensar que el resultado de una prueba de hace doce meses sigue siendo válido es, como poco, optimista.

Y ahora, ¿qué haces tú con todo esto?

A estas alturas, probablemente ya has vivido algún mini-Black Friday en tu sistema, o lo vas a vivir pronto.

Mi consejo, visto lo que se sabe y lo que ya se ha publicado sobre el tema, es que dejes de ver las pruebas de rendimiento como “algo que estaría bien hacer algún día si sobra tiempo” y las metas en la definición mínima de “estar listo para producción”. Igual que nadie en su sano juicio desplegaría sin pruebas funcionales mínimamente serias, hacer un lanzamiento importante sin pruebas de carga razonables es admitir que la estrategia es “cruzar los dedos”.

Todo lo que hemos comentado (consultas N+1, bloqueos mal gestionados, GC descontrolado, picos no simulados) son problemas conocidos, documentados, con soluciones razonables, que solo necesitan una cosa para detectarlos a tiempo: que alguien tenga la mala educación de preguntar “¿y esto cómo se comporta con 10 veces más tráfico?”.

Te devuelvo la pelota: en el sistema en el que trabajas, ¿hay SLO de rendimiento claros y pruebas de carga automáticas que merezcan ese nombre, o todo sigue en el departamento de “ya veremos en producción”?


Glosario rápido

Para que nadie tenga que buscar en mitad del Black Friday qué era aquello del N+1.

  • Consultas N+1: patrón en el que se hace una consulta para traer una lista y luego otra por cada elemento para cargar sus relaciones; con N elementos, N+1 consultas.
  • ORM (Object-Relational Mapping): biblioteca que traduce los objetos del código a tablas de la base de datos y genera el SQL por ti, para bien y para mal.
  • Carga perezosa (lazy loading): estrategia del ORM que no carga una relación hasta que el código la toca; es la causa habitual del N+1.
  • SLO (Service Level Objective): objetivo medible de calidad de servicio, por ejemplo “el 95 % de los pagos, en menos de medio segundo”.
  • Bloqueo (lock): reserva que hace una transacción sobre una fila o una tabla para que nadie más la modifique a la vez; con mucha concurrencia, se forman colas.
  • SELECT ... FOR UPDATE: consulta que lee filas y las bloquea hasta que termina la transacción.
  • GC (garbage collector, recolector de basura): parte de la máquina virtual (la JVM, en Java) que libera la memoria que ya no se usa; algunas de sus fases detienen la aplicación.
  • Pool de conexiones: conjunto limitado de conexiones a la base de datos que la aplicación reutiliza; si se agota, las peticiones esperan.
  • TTFB (Time To First Byte): tiempo que tarda en llegar el primer byte de la respuesta.
  • Prueba de carga, de estrés y de picos: la de carga mide el comportamiento con el tráfico esperado; la de estrés lleva el sistema más allá de su límite, y la de picos mete un salto brusco de usuarios en muy poco tiempo.
  • Tiempo de enfriamiento (cooldown): margen que espera el autoescalado antes de volver a reducir la capacidad tras un pico.

Fuentes y referencias

Si buscas algo a lo que echarle la culpa cuando llegue tu Black Friday, aquí tienes donde empezar.

Otros idiomas: English