Ingeniería de rendimiento de aplicaciones
Rendimiento y escalado en Laravel
Aplicaciones que aguantan bajo carga - medidas desde producción y no desde un portátil, arregladas en la causa, y defendidas con presupuestos en CI para que las cifras se queden.
El trabajo de rendimiento tiene un final previsible. Un sprint se va en optimización, las gráficas bajan, y mes y medio después están exactamente donde empezaron. Los arreglos no estaban mal. No había nada que los sujetara en su sitio.
Cómo transcurre un encargo de rendimiento en Laravel
Medir primero, desde producción. Tráfico real, muestreado, segmentado por endpoint. Un p95 de 2,4 segundos en el listado de pedidos es una afirmación accionable; «la aplicación va lenta» no lo es, y tampoco un perfilado hecho en una máquina de desarrollo contra una base de datos sembrada.
Encontrar la causa, no el síntoma. Un endpoint lento en Laravel es casi siempre una de cinco cosas: el número de consultas, una sola consulta sin índice utilizable, trabajo que debería haberse encolado, una llamada a un tercero en la ruta de la petición, o la serialización de muchos más datos de los que la respuesta necesita. Trazamos cada una hasta la línea que la produjo.
Arreglar al nivel correcto. Mover trabajo a una cola, añadir el índice que el plan de la consulta quiere de verdad, cargar la relación por adelantado, sustituir una tubería de colecciones por un agregado, cachear un valor cuyo coste justifique de verdad el problema de invalidación que crea. Estos son los cambios que se sostienen.
Defender el resultado. Un presupuesto en CI: una aserción sobre el número de consultas en los endpoints que importan, y un build que falla para el pull request que lo supere. El presupuesto es el entregable; la gráfica es un efecto secundario.
Por qué vuelve la gráfica
Porque una gráfica es un resultado y de un resultado no es dueño nadie. Un presupuesto es una cifra dentro de una prueba, comprobada por una máquina, en cada pull request, y hace fallar el build de quien lo superó mientras todavía recuerda qué cambió.
Por eso la mayoría de los clientes no nos necesitan una segunda vez para esto. Optimizar sin presupuesto es endeudarse contra el trimestre siguiente: la misma relación pierde su carga anticipada en silencio, el mismo informe gana una columna más que resulta ser un accessor con una consulta detrás, y la regresión llega un commit razonable cada vez, de gente que no tenía forma de saberlo.
Adónde se va el tiempo en realidad en una petición de Laravel
Ordenado por la frecuencia con que es la respuesta:
La base de datos. Normalmente el número de consultas y no una consulta concreta, y por eso es un encargo propio cuando es el problema entero y no una parte de él.
Trabajo que no debería estar en la petición. Un PDF renderizado, una imagen redimensionada, un webhook entregado, un informe generado. Cada uno de ellos pertenece a una cola, y moverlos suele ser la mayor mejora disponible.
Un tercero en la ruta crítica. Una búsqueda de direcciones, un servicio de impuestos, la llamada de estado de un proveedor de pagos. Su latencia es su latencia, su caída es su caída, y ninguna de las dos se ve en su propio perfilado hasta que las busca.
La serialización. Un endpoint que devuelve doscientos campos porque el resource se escribió como «todo lo del modelo», hidratando relaciones para producir una lista de nombres.
La caché usada como parche. Cachear no es gratis: compra velocidad con obsolescencia, y una aplicación con una docena de claves de caché inconexas y ninguna estrategia de invalidación ha cambiado un problema de rendimiento por uno de corrección.
Escalar, una vez que es eficiente
Solo entonces la infraestructura es la respuesta, y el orden importa: una aplicación ineficiente escalada horizontalmente es una aplicación ineficiente por la que paga varias veces.
Cuando está justificado, el trabajo son réplicas de lectura con el enrutado decidido de forma explícita y no global, almacenes de sesión y caché que no sean la base de datos principal, workers de cola escalados por separado de la capacidad web porque fallan de otra manera, y un despliegue que no tira el trabajo en vuelo. Octane donde la medición diga que el arranque del framework es de verdad una parte significativa de la petición, que es menos a menudo de lo que sugiere su reputación.
Dónde cuestan de forma medible las comodidades de Laravel
En el framework se escribe con gusto, y un puñado de las cosas que lo hacen agradable tienen un coste que solo aparece con volumen. Nada de esto es un argumento contra usarlas - es saber dónde mirar cuando los números se torcen.
Una colección donde correspondía una consulta. Model::all() seguido de un
filtro carga todas las filas en memoria para descartar casi todas. Con diez mil
filas es un endpoint lento; con un millón es el proceso muriendo. chunk y
cursor existen para los casos que de verdad necesitan todas las filas.
Un accessor con una consulta detrás. Un atributo calculado que tira de una relación es invisible en el modelo y devastador en un bucle - un listado de doscientas filas que lo llama una vez cada una son doscientas consultas que ningún eager load arregla, porque la consulta está dentro del accessor y no en la relación.
Eventos de modelo en una operación masiva. update() sobre el query
builder se salta los eventos por completo, lo que es un fallo si algo dependía
de ellos; iterar y llamar a save() los lanza todos, que es el comportamiento
correcto y dos mil consultas. Elegir a propósito, y escribir cuál necesita esta
operación, es todo el arreglo.
Seleccionarlo todo. with('orders') en un listado que muestra un recuento
carga todos los pedidos para contarlos. Un withCount, o un select de las
columnas que usa la vista, convierte un payload medido en megas en uno medido
en kilos - y la serialización de datos que nada muestra es una parte
sorprendentemente grande de una respuesta lenta.
El informe de Laravel que tiene que ser rápido y no se puede cachear
Algunas cifras no pueden estar obsoletas y no se pueden calcular a demanda, y esa combinación es donde se cae la mayoría de los paneles.
La forma habitual es una pantalla que agrega sobre varias tablas grandes, la abren veinte personas cada mañana, y tiene que reflejar lo que pasó durante la noche. Calcularla por petición significa una consulta de treinta segundos ejecutada veinte veces. Cachearla una hora significa que alguien toma una decisión sobre una cifra equivocada, que es peor que lenta.
Lo que funciona es preagregar en su propia tabla, mantenida de forma incremental y no recalculada. Los totales de cada periodo se escriben una vez cuando el periodo se cierra, el periodo en curso se deriva de un rango mucho menor, y la pantalla lee filas en lugar de recorrer la historia. El trabajo caro ocurre de forma programada, sin que nadie lo mire, y el panel se convierte en una consulta directa.
Lo incremental es la parte que requiere criterio, porque un registro que llega tarde tiene que poder corregir un total ya escrito. Eso significa que el agregado sabe qué entradas tuvo en cuenta y se puede volver a derivar para un periodo en lugar de solo añadirle - la misma disposición que hace explicable una reformulación en lugar de misteriosa.
Una réplica de lectura ayuda con la carga restante y trae su propia advertencia, que conviene decir en voz alta: el retardo de replicación significa que una cifra leída de una réplica puede ir segundos por detrás de la escritura que la produjo. Para un informe eso suele estar bien. Para la pantalla a la que alguien llega justo después de guardar, produce un informe de error sobre datos que han desaparecido.
Qué recibe del trabajo de rendimiento y escalado en Laravel
Un documento de hallazgos priorizado con cada problema trazado hasta su causa y dimensionado por esfuerzo, los arreglos implementados como pull requests revisables, la configuración del presupuesto en CI, y una comparación de antes y después contra tráfico de producción una vez que el cambio lleve el tiempo suficiente en vivo como para significar algo.
Cuando el techo resulta ser la propia arquitectura —que pasa— los hallazgos lo dicen y ponen una cifra a la alternativa al lado. Esa es una conversación para la semana uno y no para el resumen de cierre.
