Qué es Nginx y cómo funciona
Actualizado el
a las
Nginx es un servidor web, proxy inverso, balanceador de carga, proxy TCP/UDP y proxy para correos electrónicos escrito en el lenguaje de programación C por Igor Sysoev. Su primera versión fue lanzada en 2004 como una solución al problema llamado C10K.
Publicado el
a las
Qué es Nginx
Nginx es un servidor web diseñado para tareas de alto rendimiento, pensado para incrementar la seguridad y reducir la carga del sistema al balancearla entre distintos servidores según la carga de cada uno al momento de procesar las solicitudes del cliente.
El incipiente crecimiento de la popularidad de Internet en la primera parte de los años 2000 dejó en evidencia un problema; el cada vez peor rendimiento al procesar una enorme cantidad de conexiones concurrentes.
En 2002 y a raíz del problema C10K, nació la idea que en 2004 se materializó como Nginx. Un servidor web que evolucionó a balanceador de carga y proxy inverso con reputación de robustez, eficiencia y rendimiento.
Su arquitectura
A diferencia de otros servidores donde cada nueva conexión se gestiona con un nuevo proceso o hilo que bloquea la red o las operaciones de escritura, la arquitectura de Nginx se compone de un proceso principal llamado master process y uno o varios worker processes.
- El master process se encarga de todas las operaciones con privilegios como leer las configuraciones, vincular puertos o gestionar y supervisar a los worker processes.
- Un worker process se encarga de gestionar todas las peticiones. Cuando hay más de uno definidos Nginx las distribuye basándose en eventos y mecanismos del sistema operativo anfitrión.
De esta manera Nginx evita crear un nuevo proceso por cada nueva conexión, en su lugar, cada uno de los worker process escucha a través de un socket compartido. Dentro de cada worker process se ejecuta un bucle de ejecución que maneja las conexiones de manera asíncrona.
Esta arquitectura es más eficiente en términos de gestión de recursos, especialmente si se compara con la arquitectura de otros servidores, donde cada nueva conexión crea un nuevo proceso o hilo, se prepara el entorno en tiempo de ejecución, y consume recursos del sistema que pueden ser aprovechados en otras tareas.
Lo anterior también explica la creciente popularidad de Nginx.
Proxy Inverso
Como Proxy Inverso, Nginx actúa como intermediario, se sitúa delante de los servidores principales que pueden o no estar en otras máquinas o VPS.
De esta forma, cuando un cliente realiza una solicitud a un sitio web, Nginx recibe la solicitud y la envía al servidor correspondiente. Esto incrementa la seguridad, ya que el servidor de destino puede escuchar en un puerto restringido solo accesible para Nginx.
Además, al utilizar un proxy inverso el servidor final puede ocultar su dirección IP real, evitando que los clientes accedan a él directamente, esto mejora la privacidad y la protección contra ataques ya que reduce las opciones de los atacantes.
Un proxy inverso es útil por varias razones, entre ellas, la gestión de registros de actividad, eventos, solicitudes e incluso la mitigación de ataques, pudiendo compartir archivos de configuración entre sí.
Un sitio web puede contar con un dominio principal, por ejemplo daniloarancibia.es, y este a su vez con un subdominio llamado workspace.daniloarancibia.es, tanto el primero como el segundo cuentan con su propio archivo de registro de actividad, pero comparten configuraciones como bloqueos por IP, agente de usuario o excepciones de registros.
Ambos, el dominio principal y el subdominio están detrás de un proxy inverso que reenvía las peticiones a los servidores donde están alojadas las aplicaciones.
Balanceador de Carga
Como balanceador de carga Nginx gestiona el destino de las solicitudes entrantes. El concepto es parcialmente similar al de un proxy inverso, pero con matices.
Cuando intercepta una solicitud, utiliza un algoritmo llamado Round Robin para decidir a qué servidor reenviarla. También cuenta con otros como el Least Connections, pero el primero es la opción por defecto. La lógica de este algoritmo es sencilla. Consiste en distribuir las solicitudes de manera equitativa y circular.
Algoritmo Round Robin
Round Robin es el algoritmo que utiliza Nginx para procesar solicitudes de forma equitativa y circular. Es necesario aclarar que el algoritmo utilizado por Nginx no es el mismo que se utiliza en sistemas operativos, el Round Robin Scheduling, cuya diferencia no quedará sin explicación.
La lógica del algoritmo es sencilla y de hecho, no es una funcionalidad necesaria en la mayoría de los casos mientras el rendimiento no se vea afectado por el tráfico.
El siguiente bloque de código extraído de la documentación de Nginx es un ejemplo sobre como funciona Round Robin.
El ejemplo anterior cuenta con 3 servidores que alojan la instancia de una misma aplicación.
Cuando Nginx intercepta las peticiones de los usuarios, por ejemplo, 3 solicitudes, se distribuyen equitativamente entre los 3 servidores.
La primera solicitud va al servidor 1, la segunda al servidor 2, la tercera al 3 y al entrar una cuarta solicitud el ciclo comienza otra vez, es decir el servidor 1 gestionará esa 4ª solicitud.
Round Robin Scheduling:
En el contexto de sistemas operativos se introduce un concepto matemático: los quantums. Estos hacen referencia al tiempo asignado a los procesos en cola.
Cada uno cuenta con el mismo tiempo para utilizar el procesador y, una vez se agota, si el proceso no se ha completado regresa al final de la cola para dar paso al siguiente. La secuencia se repite hasta que cada proceso se ha completado.
Los quantums pueden variar de acuerdo a la configuración del algoritmo.
Un quantum demasiado largo puede dar la sensación de lentitud mientras que uno demasiado corto puede sobrecargar el sistema con cambios de contexto demasiado frecuentes.
Esa es la diferencia entre el algoritmo Round Robin que utiliza Nginx y la versión Round Robin Scheduling.
Optimización y rendimiento
Al visitar un sitio web, generalmente y de forma automática, el navegador envía múltiples solicitudes al servidor para descargar los recursos de la página. Cada una de ellas debe ser resuelta antes de dar paso a la siguiente, y en consecuencia, cada solicitud crea una nueva conexión y la cierra al ser resuelta.
Lo anterior es ineficiente y es más evidente en conexiones lentas. Para resolver este problema Nginx utiliza un método llamado multiplexación.
La multiplexación permite transportar múltiples solicitudes en una sola conexión y responder con un streaming de datos utilizando HTTP/2, sin necesidad de resolver una solicitud a la vez y por orden de llegada, que es como funciona HTTP/1.1 y es la principal causa del head-of-line-blocking.
HTTP/2
Algunas versiones de Nginx cuentan con el módulo HTTP/2, se trata de una versión optimizada del protocolo de transferencia HTTP. Con ello introduce una serie de mejoras que incrementan el rendimiento en comparación a versiones anteriores. Sin embargo, como lo explica la IETF, HTTP/2 no hace que la versión HTTP/1.1 sea obsoleta, y es más bien opcional y recomendada.
Para enviar múltiples solicitudes en una sola conexión TCP, HTTP/1.1 utiliza un método llamado request pipelining, este método tiene un problema subyacente, el head-of-line-blocking.
El head-of-line-blocking pasa cuando un cliente envía múltiples solicitudes a un servidor que utiliza HTTP/1.1, aunque todas viajan a través de una sola conexión, las respuestas del servidor deben ser devueltas en el mismo orden de llegada (FIFO).
Esto quiere decir que, si un cliente envía 7 solicitudes, el servidor puede procesarlas paralelamente en caso que cuente con un procesador multithreading, a pesar de ello y aunque la mayoría de solicitudes estén resueltas, el servidor no puede enviar la respuesta al cliente mientras la solicitud 3 no lo esté.
HTTP/2 propone un uso más eficiente de los recursos de red, para ello se basa en mecanismos de streaming y compresión de cabeceras. En una conexión HTTP/2 el cliente y el servidor crean un canal de intercambio bidireccional, por cada solicitud se crea un stream con identificador único y pueden coexistir múltiples streams concurrentes.
En el contexto de HTTP/2, un stream es un flujo de frames y estos últimos a su vez son pequeños trozos de datos. Lo anterior significa que en una conexión HTTP/2 con múltiples streams concurrentes, el servidor envía respuestas parciales por cada solicitud. De esta forma HTTP/2 resuelve el head-of-line-blocking, porque el servidor no tiene que esperar a que cada respuesta esté resuelta, sino que envía pequeños trozos de cada una según disponibilidad.
Compresión de archivos
Nginx cuenta con dos módulos para la compresión de archivos, Gzip y Brotli, ambos deshabilitados por defecto, sin embargo cada uno cuenta con sus respectivas directivas para habilitarlos.
Es importante saber que habilitar Gzip resulta más sencillo que Brotli, ya que este último en Nginx Plus, la versión de pago, está compilado por defecto. En cambio en la versión open source no lo está, lo que se traduce en tener que compilar el módulo manualmente y de forma personalizada.
El siguiente bloque de código muestra la sintaxis para activar Gzip, seguido de su estado por defecto y por último el contexto donde dicha configuración es aplicable.
Los virtual-hosts de Nginx utilizan la directiva location que, a grosso modo, sirve para interceptar solicitudes que coinciden con el patrón de la directiva, un ejemplo sería location /api/. Cualquier solicitud que coincida con dicho patrón será interceptada por esa directiva y resuelta según la configuración que contenga el bloque.
El siguiente ejemplo muestra como procesar las solicitudes de location /api/ y a su vez habilitar la compresión y aplicar otras configuraciones que puedan resultar útiles.
Estos mecanismos de compresión ayudan a que Nginx pueda rendir mejor al comprimir los recursos solicitados por el cliente, ya que, si la cache está habilitada en Nginx, los puede mantener en cache sin la necesidad de leer o comprimir en cada solicitud.
La posibilidad de habilitar o deshabilitar estos módulos a nivel granular, como en el caso de la directiva location, ayuda mitigar ciertas incompatibilidades.
Por ejemplo con Next JS, que cuenta con su propio sistema de compresión de recursos, hacer que Brotli funcione requiere configuraciones adicionales en el framework. Sin embargo, al poder deshabilitar el módulo en la directiva location, es posible seguir utilizando Brotli en otras rutas donde sea necesaria la compresión de archivos.
Seguridad y monitoreo
La seguridad en Nginx no se compone de un módulo en específico, sino de una serie de prácticas y combinación de configuraciones para garantizar un sistema robusto, securizado y a prueba de fallos.
Un ejemplo de ello es cuando funciona como proxy inverso, si los sitios web que sirve están alojados en otros servidores físicos, el de Nginx puede ser accesible desde la red mientras el que aloja la aplicación permanece completamente restringido y solo accesible desde la IP del servidor de Nginx.
Esto es una capa de seguridad adicional, pero no la única y tampoco es infalible; si un atacante vulnera el servidor donde está Nginx, tendrá acceso a la dirección del servidor donde está alojada la aplicación y podrá atacar a través del proxy o acceder directamente si el proxy tiene las claves SSH almacenadas.
Todo lo anterior por sí solo no es suficiente, también es necesario utilizar certificados SSL, restringir el acceso al servidor y revisar de forma habitual los registros de actividad (logs).
Rate limiting
Para aplicar el rate limiting, Nginx incluye el módulo ngx_http_limit_req_module, está pensado para limitar la cantidad de solicitudes que un cliente puede ejecutar en un intervalo de tiempo establecido por el servidor, para ello, se basa en el algoritmo "leaky bucket". Este módulo se utiliza para mitigar ataques de tipo DDoS y cuenta con 5 directivas de configuración:
- limit_req_zone: Se utiliza para configurar la clave, generalmente se suele utilizar la variable binary_remote_addr, que es una representación binaria de la IP del cliente, la zona y el pico máximo de solicitudes. Esta directiva solo se puede utilizar en el bloque http.
- limit_req: Dado un contexto, por ejemplo location, se usa para especificar qué zona debe utilizar Nginx. Es posible definir múltiples zonas.
- limit_req_dry_run: Ejecuta las configuraciones de limit_req en modo de prueba, es decir, las configuraciones se ejecutarán sin aplicar un límite real, pero simulará la configuración completa.
- limit_req_log_level: Sirve para especificar el nivel de los registros cuando la cantidad de solicitudes excede el límite establecido, los niveles son: info, notice, warn y error.
- limit_req_status: Se utiliza para configurar el código de estado de la respuesta del servidor, un ejemplo es que, en lugar de devolver un 429, que corresponde al código de estado para el rate limiting, se puede devolver un código 500.
En la descripción de limit_req_status, se menciona la posibilidad de devolver un código de estado 500 en lugar del 429, la razón para devolver un código de estado distinto son los bots.
En la mayoría de situaciones los ataques son ejecutados por bots que escanean la red en busca de sitios web con vulnerabilidades de configuración.
Estos bots se basan encódigos de estado para ejecutar el ataque, y aunque enviar un código de estado distinto no garantiza evitar el ataque, sí permite reducir la información que se expone sobre el servidor. El siguiente fragmento de código muestra un ejemplo de como configurar el rate limiting en Nginx:
El bloque anterior muestra dos zonas cada una con un límite distinto y, el siguiente muestra un bloque location de un virtual host que aplica los rate limits para distintos bloques location.
Certificados SSL
Nginx ofrece soporte para conexiones seguras a través del módulo ngx_http_ssl_module. Entre las directivas de este módulo están ssl_certificate, ssl_dhparam y ssl_certificate_key, entre otras que controlan el comportamiento de las conexiones SSL/TLS.
Existen herramientas gratuitas y de código abierto que automatizan la obtención, instalación y renovación de certificados SSL.
Una de ellas es Certbot, un proyecto de la Electronic Frontier Foundation (EFF), que automatiza todo el proceso de generar certificados SSL usando Let's Encrypt. Certbot configura los certificados generados en los virtual hosts y los renueva automáticamente a través de un cron job en servidores GNU/Linux.
Permisos de acceso
Nginx cuenta con el módulo ngx_http_access_module para gestionar el acceso.
Este módulo cuenta con dos directivas, deny y allow, el siguiente bloque de código muestra el contexto donde las directivas son aplicables.
Con este módulo es posible restringir el acceso a direcciones IP específicas o bien a un bloque de IPs.
Por ejemplo, en el siguiente bloque de código el administrador deniega el acceso a la IP 192.168.1.1, pero permite el acceso al resto con 192.168.1.0/24, sin embargo, al final del bloque configura deny all, esta última regla sobrescribe al resto porque las reglas se leen de arriba hacia abajo, lo que significa que el acceso está restringido en general.
Registros de actividad (Logs)
Para el registro de actividad, Nginx cuenta con el módulo ngx_http_log_module, este módulo permite configurar el formato, la compresión de los registros y donde van a ser escritos.
Sin embargo, para registrar los errores, tiene una directiva que forma parte del núcleo y no depende directamente de este módulo, se trata de la directiva error_log, que, como bien lo dice su nombre, sirve para registrar los fallos a nivel de sistema y virtual hosts.
El bloque anterior muestra un ejemplo de las directivas access_log y error_log, en el primero se trata de la configuración por defecto para access_log. Combined le dice a Nginx que debe usar el formato por defecto, mientras que if=$loggable; le dice que solo registre la actividad si se cumple dicha condición.
En este caso la condición de loggable se encuentra en el archivo de configuración global de Nginx, nginx.conf
El módulo ngx_http_map_module permite al administrador declarar variables que dependen del valor de otras variables, para ello se utiliza su directiva map. Dicho de otra forma, en el bloque anterior loggable depende de remote_addr que no es más que la dirección IPV4 del cliente, o si cuenta con ella, la dirección IPV6 y por defecto registra todos los accesos.
La variable remote_addr también forma parte del núcleo de Nginx y es accesible en todos los archivos de configuración y virtual hosts.
Si el administrador necesita bloquear los registros de actividad vinculados a su IP, sería suficiente con incluir su dirección IP dentro del bloque donde se declara $loggable.
/etc/nginx
En Gnu/Linux, el directorio /etc/nginx contiene los archivos de configuración de Nginx, como los virtual-hosts que están divididos en dos directorios. Siendo /etc/nginx/sites-available para los sitios disponibles y /etc/nginx/sites-enabled para los sitios habilitados publicamente.
Si un virtual-host existe en sites-available, mientras no exista un enlace simbólico que apunte a sites-enables, Nginx no lo considera un sitio público. Por lo tanto no será reconocido por herramientas como Certbot.
También contiene archivos de configuración globales como nginx.conf o mime.types, pero no son los únicos. Existe también el directorio conf.d, que sirve para agrupar configuraciones.
sites-available
En una instalación limpia de Nginx, sites-available contiene un único archivo llamado default. Este archivo es un ejemplo sobre como se debe configurar un virtual-host en el directorio sites-available.
Cada administrador tiene su propia nomenclatura para nombrar a los virtual-hosts. Y dejando de lado por un momento mi fascinación por escribir en tercera persona, recomiendo encarecidamente que uséis el nombre del dominio o subdominio para nombrar al virtual-host cuya configuración gestiona.
Debajo os dejo un ejemplo con mi sitio web daniloarancibia.es, pero dentro de un contenedor Docker con la imagen de Debian Trixie.
Incluso es recomendable organizarlos por directorios de la siguiente manera:
A nivel funcional lo anterior no afecta en lo absoluto. Sin embargo, para mantener múltiples dominios es casi obligatorio llevar un orden dentro de sites-available, cosa que se agradece especialmente si no eres el único administrador del servidor.
default
El siguiente es un ejemplo de un archivo de configuración default en una instalación limpia de Nginx en Debian Trixie.
El archivo default debe permanecer intacto, sin modificar, mover o eliminar. La razón es simple. Al actualizar el servidor con el archivo intacto y si entre los paquetes con actualizaciones disponibles está Nginx, el archivo se actualizará automáticamente con nueva información en caso que la haya.
La naturaleza de wiki y de fall back del archivo genera algunas contradicciones. La primera llega con el hecho que default no debe tener un enlace simbólico que apunte hacia sites-enabled, aunque por defecto y con fines ilustrativos, existe un enlace simbólico en sites-enabled desde el primer momento.
La razón está implícita, personalizarlo no es una opción. Además, un archivo susceptible a cambios y con una directiva tan importante como default_server genera dudas sobre como tratar con el, pero su naturaleza de plantilla es la respuesta a estas contradicciones.
Para evitar divergencias se debe crear una copia con otro nombre, por ejemplo:
Y eliminar el enlace simbólico de default de sites-enabled:
Y finalmente remplazarlo por la copia:
Ahora Main es solo un archivo que funciona como fall back y default como una wiki sobre los virtual-hosts. Lo anterior además ser un ejercicio de separación de responsabilidades, permite que Main se pueda personalizar sin las consecuencias de modificar default.
Main podría incluso no existir como remplazo de default como fall back en sites-enabled y Nginx mostraría el suyo propio, como el siguiente:
La directiva mencionada anteriormente, default_server, sirve para indicar a Nginx que archivo resuelve las peticiones cuya cabecera no corresponde a ningún virtual-host del servidor.
symlinks
¿Por qué un enlace simbólico y no una copia? Un enlace simbólico es, de forma simplificada, un puntero al inodo del archivo original mientras que una copia es un inodo nuevo.
Esto significa que cada cambio en el archivo original se verá reflejado en el enlace simbólico. La separación a nivel de directorios pero sincronización a través de los enlaces simbólicos permiten habilitar o no un virtual-host y mantener un orden coherente entre dominios.
conf.d
conf.d es un directorio dedicado para almacenar de forma organizada configuraciones globales del servidor o de los virtual-hosts. A diferencia de sites-available y sites-enabled, en una instalación limpia conf.d no contiene ningún archivo de configuración por defecto.
El concepto es similar a lo que se explica en el bloque sites-available pero esta vez en el contexto de configuraciones globales o específicas de cada virtual-host.
Un modo de representarlo de forma organizada es la siguiente:
nginx.conf
nginx.conf es el archivo principal de configuración de Nginx, está estrechamente relacionado con el directorio conf.d y los virtual-hosts.
El siguiente ejemplo supone que el archivo white-list.conf existe en el directorio conf.d y que su único propósito es permitir que un grupo o un rango de IPS puedan acceder a recursos privados de un virtual-host en específico.
Para ello sería suficiente con incluir el archivo white-list en el virtual-host con la directiva include de la siguiente forma:
El ejemplo anterior es útil en diversas situaciones, una de ellas es cuando se está migrando de una web hecha con Prestashop a una web hecha a medida que además cuenta con un nuevo diseño. Lo ideal es que el resto de departamentos den su feedback sobre la nueva web o Q&A pueda ir a la par con el equipo de desarrollo. Para que lo anterior sea posible debe existir un subdominio, por ejemplo: staging.daniloarancibia.es.
De esta forma además de recibir feedback interno, también se evita exponer páginas que no deberían ser publicas en una etapa tan temprana. Y si las IPS del archivo white-list.conf solo se pueden utilizar en la oficina, también se evitan filtraciones.
Ahora bien, si en lugar de incluir el archivo de configuración white-list.conf en un virtual-host, se incluye en nginx.conf, las configuraciones del archivo ya no solo afectan a un solo virtual-host, sino que se aplican a todos los virtual-hosts disponibles, sin excepción alguna.
mime.types
mime.types es un archivo de configuración que Nginx utiliza para informar al cliente como debe tratar los recursos que recibe desde el servidor.
Si la aplicación se sirve a través de un proxy, es probable que esta ya envíe sus propios headers con el tipo de contenido para cada recurso.
En frameworks como NextJS esto no supone un problema funcional. Porque si el mime type no existe en mime.types, el framework ya está enviando los headers por su cuenta. Esta división de dependencias provoca que al intentar forzar un cambio de headers en NextJS a través de Nginx se duplique el header.
Si la aplicación se sirve como recurso estático a través de Nginx y los recursos están en /var/www/html/, el fall back funcional será que el navegador intentará descargar el recurso en lugar de interpretarlo.
C'est tout.