NGINX como API Gateway, Balanceador de Carga, Servidor de Cache y Rate Limiter

NGINX como API Gateway, Balanceador de Carga, Servidor de Cache y Rate Limiter

NGINX conocido por su versatilidad como servidor HTTP y proxy reverso, también se puede emplear eficazmente como un API Gateway. En este artículo, exploraremos cómo NGINX puede manejar el tráfico de tus APIs, desde la publicación hasta la autenticación, pasando por la seguridad y el balanceo de carga.

¿Qué es NGINX?

NGINX es más que un servidor HTTP; es un servidor proxy inverso robusto, un servidor de correo y hasta un manejador de protocolos TCP/UDP generales. Respaldado por su amplio uso en sitios web con mucho tráfico, NGINX se ha convertido en una solución de confianza para los desarrolladores.

NGINX como API Gateway

Un API Gateway es un gestor de tráfico que actúa como intermediario entre el tráfico externo y los servicios de backend. En NGINX, un API Gateway ofrece ventajas significativas, tales como aumentar la seguridad de las APIs a través de una única interfaz, facilitar la implementación de políticas de control de acceso, límites de tasa, enrutamiento y mediación, y permite una recopilación completa de métricas.

Intentemos seguir el siguiente ejemplo para que queda más claro el concepto, imaginemos que tenemos dos APIs: una para productos y otra para usuarios. No queremos que estos servicios estén directamente expuestos al público, así que utilizamos NGINX como nuestro único punto de acceso, o API Gateway.

Para simular nuestras APIs, creamos dos aplicaciones Python simples que devuelven respuestas fijas. Luego configuramos NGINX para manejar las rutas entrantes hacia estos servicios. Aquí está el fragmento clave de la configuración de NGINX:

server {
   listen 80 default_server;
   listen [::]:80 default_server;

   # API de Productos
   location /api/products {
       proxy_pass http://products_api:8001;
   }

   # API de Usuarios
   location /api/users {
       proxy_pass http://users_api:8002;
   }
}

IMPORTANTE: Te podrías estar preguntando porque estoy utilizando products_api y users_api en lugar de sus direcciones IP, y aunque utilizar sus direcciones IP es posible, este artículo esta puesto en marcha en un contenedor, por lo que las aplicaciones products y users se han definido como alias de los servicios en el archivo docker-compose.yml.

Con esta configuración, cuando se realiza una solicitud GET a http://localhost/api/products, se espera una respuesta con los detalles de un producto y una solicitud GET a http://localhost/api/users devolverá los datos de un usuario.

GET http://localhost/api/products  
{  
  "name": "Producto 1",
  "description": "Detalles sobre el producto 1"
}
GET http://localhost/api/users  
{  
  "name": "Usuario 1",
  "email": "[email protected]"
}

Para una mejor organización, definimos grupos de servidores utilizando el módulo upstream. Esto permite referenciar contenedores de APIs por nombre en lugar de su dirección directa, lo que simplifica la gestión de las rutas.

upstream products_api_server {
    server products_api:8001;
}

upstream users_api_server {
    server users_api:8002;
}

server {
   listen 80 default_server;
   listen [::]:80 default_server;

   # API de Productos
   location /api/products {
       proxy_pass http://products_api_server;
   }

   # API de Usuarios
   location /api/users {
       proxy_pass http://users_api_server;
   }
}

Un upstream en NGINX es una configuración que permite definir un grupo de servidores que se pueden referenciar posteriormente en la configuración del servidor. Esto es útil cuando se tiene que balancear la carga entre múltiples servidores o se desea simplificar la configuración de proxy pasando a través de un nombre simbólico en lugar de una dirección específica del servidor.

Hemos utilizado upstream en nuestra configuración para agrupar las instancias de las APIs de Productos y Usuarios bajo nombres claros y manejables. Esto no solo mejora la legibilidad de la configuración, sino que también facilita la administración, ya que cualquier cambio futuro en las direcciones de las APIs requerirá una actualización en un solo lugar. Además, si en el futuro queremos añadir más servidores a la rotación para manejar más tráfico o proporcionar redundancia, podemos hacerlo fácilmente añadiendo más líneas server al bloque upstream correspondiente.

La configuración upstream puede estar dentro del archivo principal de configuración de NGINX, nginx.conf, o puede estar en un archivo separado que luego se incluye en nginx.conf. NGINX es muy flexible en cuanto a cómo puedes organizar tus archivos de configuración.

En muchas instalaciones, especialmente aquellas que manejan múltiples sitios o aplicaciones, es común separar las configuraciones en archivos diferentes para mantener las cosas ordenadas. Por ejemplo, podrías tener un archivo gateway.conf para todas tus definiciones de API Gateway, incluyendo los bloques upstream y las ubicaciones location que manejan las rutas específicas de la API.

Si decides separar las configuraciones, simplemente incluirías el archivo gateway.conf en tu nginx.conf utilizando la directiva include:

http {
    include /etc/nginx/conf.d/*.conf;  # Incluye todos los archivos .conf del directorio conf.d
    include /etc/nginx/sites-enabled/*;  # Incluye todos los archivos en sites-enabled

    # También puedes incluir archivos específicos
    include /etc/nginx/gateway.conf;  # Incluiría tu archivo gateway.conf específico
}

Y para poder utilizar upstream, es necesario haber habilitado el módulo ngx_http_upstream_module, el cual proporciona las capacidades para definir los bloques upstream. Este módulo es el que hace posible agrupar servidores para que puedan ser referenciados posteriormente por directivas como proxy_pass, fastcgi_pass, uwsgi_pass, scgi_pass, memcached_pass, y grpc_pass.

NGINX como Balanceador de Carga

El concepto de balanceo de carga es fundamental en la arquitectura de servicios web moderna, particularmente cuando se trata de manejar un gran volumen de tráfico y mantener la alta disponibilidad. En nuestro caso, vamos a adaptar la configuración de NGINX para manejar un escenario donde la API de Usuarios experimenta una carga elevada.

Para abordar esto, introducimos una instancia adicional denominada users_api_balance y la agregamos al grupo de servidores upstream. La configuración quedaría así:

upstream users_api_server {
    server users_api:8002;
    server users_api_balance:8002;
}

Con esta configuración, NGINX distribuirá automáticamente las solicitudes entrantes entre las dos instancias de la API de Usuarios de manera uniforme mediante el método Round Robin.

api-gateway-nginx    | 10.10.121.7 - [25/Nov/2023 06:00:10 +0000] "GET /api/users HTTP/1.0" 200 11
users_api            | 10.10.121.8 - [25/Nov/2023 06:00:10 +0000] "GET /api/users HTTP/1.0" 200 11
api-gateway-nginx    | 10.10.121.7 - [25/Nov/2023 06:00:11 +0000] "GET /api/users HTTP/1.1" 200 123
users_api_balance    | 10.10.121.9 - [25/Nov/2023 06:00:11 +0000] "GET /api/users HTTP/1.1" 200 123

Al observar los registros de los contenedores, se puede confirmar que las solicitudes se reparten entre los servidores de manera equitativa, evidenciando la efectividad del balanceo de carga.

Para refinar aún más nuestra estrategia de balanceo de carga, implementamos el método least_conn que dirige nuevas solicitudes al servidor con el menor número de conexiones activas. Esto ayuda a evitar la sobrecarga de servidores ya ocupados y distribuye el tráfico de manera más eficiente.

upstream users_api_server {
    least_conn;
    server users_api:8002;
    server users_api_balance:8002;
}

NGINX ofrece diversos métodos de balanceo de carga como ip_hash, generic hash, random, y least_time (este último solo disponible en NGINX Plus). Cada uno de estos métodos se puede aplicar según sea necesario, basándose en la naturaleza del tráfico y los requisitos de infraestructura.

Consideramos un escenario en el que uno de nuestros servidores (users_api) tiene una infraestructura más robusta y queremos priorizar el tráfico hacia este. Esto se logra asignando un weight mayor a ese servidor en la configuración upstream. Así, el servidor users_api recibirá una cantidad de solicitudes proporcionalmente mayor comparado con el servidor balanceado.

upstream users_api_server {
    least_conn;
    server users_api:8002 weight=5;
    server users_api_balance:8002;
}

En este caso, hemos asignado un peso de 5 al servidor users_api, lo que significa que tendrá cinco veces más probabilidades de ser elegido para manejar una nueva conexión en comparación con el servidor users_api_balance.

Esta configuración demuestra la flexibilidad y potencia de NGINX como un balanceador de carga, asegurando que las APIs puedan manejar la carga de tráfico de manera efectiva y eficiente.

Podrías preguntarte porque he elegido 5 y no 2, y es un razonamiento válido, entonces si deseas priorizar users_api sobre users_api_balance, asignarle un valor de peso más alto resultará en que users_api reciba más conexiones que users_api_balance. La elección entre 2, 5 o cualquier otro valor depende de cuánto más tráfico quieras que maneje users_api en relación con users_api_balance, por ejemplo:

  • Si asignas un peso de 2 a users_api y un peso de 1 a users_api_balance, users_api manejará aproximadamente dos tercios del tráfico, porque el peso total es 3 (2+1) y users_api tiene 2 de esos 3 "puntos" de peso.

  • Si asignas un peso de 5 a users_api y un peso de 1 a users_api_balance, users_api manejará cinco sextos del tráfico, ya que de un total de 6 "puntos" de peso (5+1), users_api tiene 5.

El peso que asignes debe reflejar la capacidad relativa de users_api para manejar más tráfico en comparación con users_api_balance. El peso no indica cuántas veces más solicitudes recibirá un servidor; en cambio, indica la proporción del tráfico que recibirá en relación con el total combinado de peso de todos los servidores en el grupo upstream.

NGINX como servidor de Cache

Implementar caché en NGINX es una forma efectiva de optimizar los tiempos de respuesta, especialmente cuando se trabaja con APIs que no cambian frecuentemente, como podría ser nuestra API de Productos. Para configurar una caché básica, necesitamos dos directivas principales: proxy_cache_path y proxy_cache.

La directiva proxy_cache_path define la ubicación en el disco y la configuración de la caché, mientras que la directiva proxy_cache activa el uso de esta caché para ubicaciones específicas dentro de la configuración del servidor.

Aquí está el bloque de configuración que establece la caché para la API de Productos:

proxy_cache_path /var/cache/nginx/products levels=1:2 keys_zone=products_cache:30m max_size=5g inactive=24h use_temp_path=off;

upstream products_api_server {
    server products_api:8001;
}

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # API de Productos con caché
    location /api/products {
        proxy_cache products_cache;
        proxy_pass http://products_api_server;
    }

    # API de Usuarios sin caché
    location /api/users {
        proxy_pass http://users_api_server;
    }
}

Vamos a desglosar lo que significa cada parámetro de la directiva proxy_cache_path:

  • /var/cache/nginx/products: Es el directorio en el disco donde se almacenarán los archivos de caché.
  • levels=1:2: Crea una jerarquía de directorios de dos niveles para almacenar los archivos de caché.
  • keys_zone=products_cache:30m: Configura una zona de memoria compartida para almacenar las claves de caché y metadatos, como temporizadores de uso, con un tamaño de 30 MB.
  • max_size=5g: Especifica el tamaño máximo que la caché puede crecer, en este caso 5 gigabytes.
  • inactive=24h: Define cuánto tiempo un archivo puede permanecer en la caché sin ser accedido antes de ser eliminado, aquí se establece como 1 dia o 24 horas.
  • use_temp_path=off: Instruye a NGINX para escribir archivos temporales en el mismo directorio donde estarán almacenados, en lugar de un área de almacenamiento temporal.

La directiva proxy_cache dentro de la ubicación /api/products activa la caché que hemos definido, lo que significa que las respuestas de esta API serán almacenadas en caché de acuerdo con las reglas especificadas, mejorando el tiempo de respuesta para solicitudes subsiguientes.

Para una efectiva distribución de caché, se necesita considerar tanto la memoria como el espacio en disco:

  1. Memoria (RAM): La memoria se utiliza para almacenar las claves de caché y los metadatos asociados. NGINX requiere que se configure una zona de memoria compartida (keys_zone) para cada caché. La zona de memoria compartida no almacena el contenido en sí, sino las claves y metadatos que permiten a NGINX gestionar la caché de manera eficiente. No necesita ser excesivamente grande, ya que no contiene los datos en sí, pero debe ser lo suficientemente grande para manejar todas las claves activas y metadatos.

  2. Espacio en disco: El contenido en sí se almacena en el disco. Necesitarás suficiente espacio en disco para almacenar tus archivos de caché. El max_size define el tamaño máximo de la caché en el disco. Si esperas manejar una gran cantidad de datos, necesitarás un disco con una capacidad adecuada para almacenar la caché sin correr el riesgo de quedarte sin espacio.

La eficiencia de la caché también dependerá de la tasa de aciertos de la caché (cache hit rate), que es la proporción de solicitudes que pueden ser atendidas desde la caché sin tener que recuperar los datos del servidor de origen. Un tamaño de caché bien dimensionado que almacena los objetos correctos puede mejorar significativamente el rendimiento, reduciendo la latencia y la carga en los servidores de origen.

Para determinar los valores adecuados para la configuración de la caché de una API de Productos, consideraremos varios factores, como la frecuencia de actualización de los datos de los productos, la cantidad de datos y el tráfico esperado. A continuación, presento una estructura sencilla y los valores que podrían ser designados para la caché en un escenario típico:

  • Tamaño de la zona de memoria compartida (keys_zone): Si tienes miles de productos, cada uno de los cuales podría ser una clave en la caché, un tamaño de zona de memoria de 10 a 50 megabytes podría ser suficiente para almacenar las claves y metadatos. Esto se define con la directiva keys_zone.

  • Tamaño máximo de caché (max_size): Si el tamaño promedio de la respuesta de la API de un producto es pequeño y los datos de los productos no cambian con frecuencia, un tamaño de caché de 1 a 10 gigabytes podría ser suficiente para almacenar respuestas de productos durante un tiempo. Esto te permitiría almacenar una gran cantidad de respuestas de productos y servirlos rápidamente desde la caché.

  • Tiempo de inactividad (inactive): Si los productos son consultados regularmente, pero no quieres mantener elementos raramente accedidos en la caché, podrías configurar un tiempo de inactividad de 1 hora a 1 día. Esto significa que si un elemento no se ha accedido en ese período, se eliminará de la caché.

  • Estructura de directorios (levels): Una estructura de directorio de uno o dos niveles (por ejemplo, levels=1:2) suele ser suficiente para la mayoría de las implementaciones, ya que proporciona un buen equilibrio entre el rendimiento del sistema de archivos y la gestión del espacio.

NGINX como Rate Limiter

El rate limiting o limitación de tasa de solicitudes es una técnica crucial para proteger las APIs internas contra un volumen excesivo de solicitudes, que podrían provenir de ataques de denegación de servicio distribuido (DDoS). En NGINX, esta protección se configura mediante dos directivas principales: limit_req_zone y limit_req.

La directiva limit_req_zone se utiliza para definir los parámetros de limitación de tasa. Esta directiva se declara a nivel de HTTP y establece un área de memoria compartida para llevar un registro del estado de las solicitudes de cada dirección IP única:

limit_req_zone $binary_remote_addr zone=products_rate:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=user_rate:10m rate=10r/s;

Aquí, $binary_remote_addr se usa como la clave para identificar solicitudes únicas de diferentes direcciones IP. La zone define el nombre y el tamaño de la memoria compartida (en este caso, 10m para 10 megabytes), y rate define la tasa máxima de solicitudes permitidas por segundo (1r/s para la API de Productos y 10r/s para la API de Usuarios).

La directiva limit_req se utiliza dentro de los bloques server o location para habilitar la limitación de tasa en contextos específicos. Si la tasa de solicitudes excede el límite definido, NGINX por defecto devolverá un error 503 (Servicio Temporalmente No Disponible). Para personalizar la respuesta, se utiliza limit_req_status para cambiar el código de estado que se devuelve cuando se alcanza el límite de tasa. En este caso, se usa el estado 429 (Demasiadas Solicitudes):

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # API de Productos
    location /api/products {
        proxy_cache products_cache;
        limit_req zone=products_rate burst=5 nodelay;
        limit_req_status 429;
        proxy_pass http://products_api_server;
    }

    # API de Usuarios
    location /api/users {
        limit_req zone=user_rate burst=10 nodelay;
        limit_req_status 429;
        proxy_pass http://users_api_server;
    }
}

En este ejemplo, hemos añadido burst y nodelay para permitir un procesamiento más flexible de las solicitudes en ráfagas. El burst define un número de solicitudes que pueden ser procesadas en exceso del límite definido por rate, permitiendo así que los usuarios realicen ráfagas cortas de solicitudes sin ser limitados. nodelay se utiliza para indicar que las solicitudes en exceso del límite de rate pero dentro de burst no deben ser retrasadas.

  • Para la API de Productos, se establece un rate de 1 solicitud por segundo (1r/s). Sin embargo, imagina que no se ha especificado un parámetro burst, lo que significa que cualquier solicitud que exceda el límite de 1 por segundo podría ser rechazada inmediatamente y recibir un estado 429 (Demasiadas Solicitudes).

  • Para la API de Usuarios, se configura un rate de 10 solicitudes por segundo (10r/s), lo que indica que cualquier solicitud por encima de 10 por segundo podría ser rechazada.

Pero cuando se incluye un burst en la configuración, este actúa como una especie de colchón que permite un número extra de solicitudes por encima del límite de tasa (rate) antes de empezar a rechazar solicitudes adicionales con un 429. Por ejemplo:

limit_req zone=products_rate burst=5 nodelay;

En este caso con burst=5, NGINX permitiría hasta 5 solicitudes adicionales sobre el límite establecido por rate en un momento dado sin retraso (nodelay), procesándolas inmediatamente si es posible. Esto es útil para manejar picos cortos de tráfico. Si el tráfico es sostenidamente alto y supera el burst, entonces NGINX comenzaría a devolver un 429 para las solicitudes adicionales.

Entonces, con un burst de 5 y un rate de 1r/s para la API de Productos, NGINX permitiría efectivamente hasta 6 solicitudes en un segundo (1 por rate más 5 por burst) antes de emitir un error 429. Para la API de Usuarios, con un rate de 10r/s y un burst hipotético de 10, se permitirían hasta 20 solicitudes en un segundo (10 por rate más 10 por burst) antes de comenzar a rechazar solicitudes adicionales.

Conclusión

Hemos explorado las capacidades robustas de NGINX como un API Gateway, destacando su versatilidad y potencia para gestionar tráfico, asegurar APIs y optimizar respuestas. Desde la implementación de reglas de balanceo de carga que garantizan la distribución equitativa del tráfico y la gestión eficiente de múltiples instancias de servicio, hasta el uso estratégico de la caché para mejorar los tiempos de respuesta.

NGINX tiene aún muchas otras ventajas y módulos que pueden ayudar a mejorar la seguridad y el rendimiento de sitios webs, pero algunas de estas herramientas son de paga y solo se ofrecen en la versión Plus, otras son módulos libres que se pueden instalar adicionalmente (pero esto requiere un conocimiento más profundo sobre NGINX), aunque nunca esta de más conocer de que es capaz y como utilizarlo.

Happy coding! :D


Photo by Taylor Vick on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?