Ajuste de Búferes de Nginx: Optimizando `client_body_buffer_size` y `proxy_buffer_size`

Ajusta los búferes de solicitud y proxy de Nginx sin desperdiciar memoria ni provocar almacenamiento en disco evitable.

Ajuste de Búferes de Nginx: Optimizando client_body_buffer_size y proxy_buffer_size

El ajuste de búferes de Nginx no consiste en hacer cada búfer enorme. Se trata de decidir qué datos deben residir en memoria, cuáles pueden volcarse de forma segura al disco y qué respuestas deben transmitirse en lugar de almacenarse completamente en búfer.

La parte confusa es que varias directivas suenan similares. client_body_buffer_size se aplica a los cuerpos de las solicitudes entrantes de los clientes. proxy_buffer_size y proxy_buffers se aplican a las respuestas que provienen de un servidor ascendente cuando Nginx actúa como proxy inverso. FastCGI tiene sus propias directivas equivalentes. Si ajustas el lado equivocado, no mejorarás nada.

Una buena sesión de ajuste comienza con los síntomas:

  • Los registros de error de Nginx mencionan archivos temporales.
  • Las subidas o solicitudes POST grandes se sienten lentas.
  • Las respuestas de API proxy inverso se pausan bajo carga.
  • Los workers usan más memoria de la esperada.
  • Los encabezados de respuesta grandes provocan errores ascendentes.

Esos síntomas no tienen todos la misma solución. Búferes más grandes pueden reducir la E/S de disco, pero también aumentan la presión de memoria por solicitud. En un sitio concurrido, una configuración que parece pequeña por solicitud puede volverse grande cuando se multiplica por miles de conexiones concurrentes.

Almacenamiento en búfer del cuerpo de la solicitud: client_body_buffer_size

client_body_buffer_size controla el búfer utilizado mientras Nginx lee el cuerpo de una solicitud de cliente. Piensa en envíos de formularios, cuerpos JSON POST y subidas de archivos. Si el cuerpo de la solicitud es más grande que este búfer, Nginx puede escribir parte de él en un archivo temporal bajo client_body_temp_path.

Una configuración básica se ve así:

http {
    client_body_buffer_size 128k;
}

Esto no cambia el tamaño máximo permitido de subida. Eso está controlado por client_max_body_size:

server {
    client_max_body_size 20m;
    client_body_buffer_size 128k;
}

Esas dos directivas responden a preguntas diferentes. client_max_body_size dice: "¿Qué tan grande puede ser la solicitud antes de que Nginx la rechace?" client_body_buffer_size dice: "¿Cuánto del cuerpo de la solicitud debe mantener Nginx en memoria antes de usar un archivo temporal?"

Para una API JSON donde la mayoría de los cuerpos de solicitud son menores de 32 KB y algunos son de 200 KB, un búfer de 128k puede reducir las escrituras de archivos temporales sin ser excesivo. Para un servicio de subida de archivos que acepta videos de 50 MB, establecer client_body_buffer_size 50m globalmente sería un mal equilibrio. Estarías reservando demasiada memoria para una carga de trabajo donde el almacenamiento en búfer de disco puede ser aceptable.

Revisa el registro de errores en busca de pistas como:

[warn] a client request body is buffered to a temporary file

Esa advertencia no es automáticamente un fallo. Es Nginx diciéndote que un cuerpo excedió el búfer en memoria. Si ocurre ocasionalmente durante subidas grandes, está bien. Si ocurre constantemente para solicitudes API ordinarias, ajusta el búfer o arregla los clientes que envían cargas útiles grandes.

Almacenamiento en búfer de respuestas proxy: proxy_buffer_size y proxy_buffers

Cuando Nginx actúa como proxy hacia una aplicación ascendente, el almacenamiento en búfer de respuestas suele estar habilitado por defecto. Nginx lee rápidamente la respuesta ascendente, la almacena en búferes y la envía al cliente. Si la respuesta no cabe en los búferes de memoria, parte de ella puede escribirse en un archivo temporal. La documentación oficial del módulo proxy de Nginx describe este comportamiento y vincula la escritura de archivos temporales a proxy_max_temp_file_size y proxy_temp_file_write_size.

El primer búfer está controlado por proxy_buffer_size:

proxy_buffer_size 16k;

Este búfer maneja el encabezado de la respuesta y la primera parte de la respuesta. Si tu servidor ascendente envía encabezados grandes, como muchas cookies, encabezados de autenticación grandes o metadatos de rastreo sobredimensionados, puedes ver errores como "upstream sent too big header". En ese caso, proxy_buffer_size suele ser la directiva a revisar.

Los búferes de respuesta restantes están controlados por proxy_buffers:

proxy_buffers 8 32k;

Eso significa ocho búferes de 32 KB cada uno para datos de respuesta proxy. No significa que cada solicitud consuma siempre la cantidad total de principio a fin, pero aún así debes tratar estas configuraciones como configuraciones de memoria por solicitud bajo carga.

Un bloque de proxy inverso común se ve así:

location /api/ {
    proxy_pass http://app_backend;

    proxy_buffering on;
    proxy_buffer_size 16k;
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;
}

No copies esto ciegamente. Un sitio HTML pequeño, una API JSON y un servicio de descarga de archivos tienen necesidades diferentes.

Archivos temporales y proxy_max_temp_file_size

Si el almacenamiento en búfer de respuestas proxy está habilitado y la respuesta es más grande que los búferes configurados, Nginx puede escribir parte de ella en el disco. proxy_max_temp_file_size limita el tamaño de ese archivo temporal. El valor predeterminado de Nginx se documenta comúnmente como 1024m, y establecerlo en 0 deshabilita el almacenamiento en búfer de respuestas proxy en archivos temporales.

location /api/ {
    proxy_pass http://app_backend;
    proxy_buffering on;
    proxy_max_temp_file_size 50m;
}

Usa 0 con cuidado:

proxy_max_temp_file_size 0;

Eso no significa que las respuestas grandes sean gratuitas. Significa que Nginx no usará archivos temporales para esas respuestas proxy almacenadas en búfer. Dependiendo de la respuesta, la velocidad del cliente y el comportamiento del almacenamiento en búfer, es posible que necesites más memoria, diferentes configuraciones de búfer o un diseño de transmisión.

Un matiz importante de la documentación de Nginx: la restricción de proxy_max_temp_file_size no se aplica a las respuestas que se almacenarán en caché o en disco. Si usas proxy_cache o proxy_store, revisa esas configuraciones por separado.

Cuando proxy_buffering off es la mejor respuesta

A veces no deberías almacenar en búfer la respuesta en absoluto. Los endpoints de transmisión, eventos enviados por el servidor, descargas grandes y salidas de larga duración a menudo funcionan mejor con el almacenamiento en búfer de proxy deshabilitado:

location /events/ {
    proxy_pass http://app_backend;
    proxy_buffering off;
}

Con el almacenamiento en búfer desactivado, Nginx pasa la respuesta ascendente al cliente a medida que la recibe en lugar de intentar recopilar la respuesta completa. proxy_buffer_size sigue siendo importante porque Nginx necesita un búfer para los datos recibidos del servidor ascendente, pero proxy_buffers y el comportamiento de los archivos temporales se vuelven menos centrales.

No desactives el almacenamiento en búfer globalmente a menos que entiendas la compensación. El almacenamiento en búfer protege a los servidores ascendentes de clientes lentos. Si los clientes descargan lentamente y el almacenamiento en búfer está desactivado, las conexiones ascendentes pueden permanecer ocupadas por más tiempo.

Búferes FastCGI para PHP y configuraciones similares

Si Nginx se comunica con PHP-FPM, las directivas proxy_* no son las que necesitas. FastCGI usa nombres equivalentes:

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;

    fastcgi_buffer_size 16k;
    fastcgi_buffers 8 32k;
}

La misma lógica se aplica: el primer búfer maneja los encabezados y los datos iniciales; la matriz de búferes maneja más contenido de respuesta. Si PHP envía encabezados grandes, fastcgi_buffer_size puede necesitar atención. Si las páginas grandes generadas por PHP se vuelcan al disco, revisa fastcgi_buffers y la carga de trabajo.

Cómo ajustar sin adivinar

Comienza revisando los registros:

sudo tail -f /var/log/nginx/error.log

Busca advertencias sobre cuerpos de solicitud o respuestas ascendentes que se almacenan en búfer en archivos temporales. Luego mide los tamaños reales involucrados. Para APIs, muestra los tamaños de solicitud y respuesta de los registros de acceso, registros de aplicación o tu sistema de observabilidad. Para subidas, separa las solicitudes de metadatos de los endpoints de subida de archivos. No deberían compartir las mismas suposiciones.

Ajusta en el contexto más específico que tenga sentido. En lugar de esto:

http {
    client_body_buffer_size 10m;
}

prefiere algo como esto solo para el endpoint de subida:

location /upload/ {
    client_max_body_size 50m;
    client_body_buffer_size 512k;
    proxy_pass http://upload_backend;
}

Para una API JSON con respuestas ocasionales de 1 MB, podrías ajustar solo esa ubicación de API:

location /reports/ {
    proxy_pass http://report_backend;
    proxy_buffering on;
    proxy_buffer_size 32k;
    proxy_buffers 16 64k;
    proxy_max_temp_file_size 20m;
}

Luego calcula el peor caso. Si 1,000 solicitudes concurrentes pueden usar cada una varios cientos de KB de búferes de respuesta, eso es memoria real. Deja espacio para los procesos worker, TLS, conexiones ascendentes, cachés, la caché de página del SO y otros servicios.

Valida la configuración y observa el sistema

Después de cada cambio:

sudo nginx -t
sudo systemctl reload nginx

Luego observa la memoria, la E/S de disco y los registros de errores. Una prueba de sintaxis exitosa solo prueba que el archivo se analiza. No prueba que los valores sean buenos.

Las comprobaciones útiles incluyen:

free -h
iostat -xz 1
sudo tail -f /var/log/nginx/error.log

Si las advertencias de archivos temporales desaparecen pero la presión de memoria aumenta, te pasaste. Si la memoria se mantiene saludable y la E/S de disco disminuye durante la carga de trabajo afectada, es probable que el cambio haya ayudado.

El ajuste de búferes de Nginx funciona mejor cuando es local, medido y vinculado al tráfico real. Mantén las solicitudes ordinarias en memoria cuando eso ahorre latencia, deja que las subidas o descargas realmente grandes usen la ruta correcta y evita configuraciones globales que hagan que cada conexión pague por el caso extremo más grande.