Saltar a contenido

combined-upstreams: Módulo NGINX Combined Upstreams

Instalación

Puedes instalar este módulo en cualquier distribución basada en RHEL, incluyendo, pero no limitado a:

  • RedHat Enterprise Linux 7, 8, 9 y 10
  • CentOS 7, 8, 9
  • AlmaLinux 8, 9
  • Rocky Linux 8, 9
  • Amazon Linux 2 y Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-combined-upstreams
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-combined-upstreams

Habilita el módulo añadiendo lo siguiente en la parte superior de /etc/nginx/nginx.conf:

load_module modules/ngx_http_combined_upstreams_module.so;

Este documento describe nginx-module-combined-upstreams v2.3.1 lanzado el 15 de abril de 2025.


El módulo introduce tres directivas add_upstream, combine_server_singlets y extend_single_peers disponibles dentro de los bloques de configuración de upstream, y un nuevo bloque de configuración upstrand para construir supercapas de upstreams. Además, se introduce la directiva dynamic_upstrand para elegir upstrands en tiempo de ejecución.

Directiva add_upstream

Puebla el upstream host con servidores listados en un upstream ya definido especificado por el primer parámetro obligatorio de la directiva. Los atributos del servidor como pesos, max_fails y otros se mantienen en el upstream host. Los parámetros opcionales pueden incluir valores backup para marcar todos los servidores del upstream fuente como servidores de respaldo y weight=N para calibrar los pesos de los servidores del upstream fuente multiplicándolos por el factor N.

Un ejemplo

upstream  combined {
    add_upstream    upstream1;            # src upstream 1
    add_upstream    upstream2 weight=2;   # src upstream 2
    server          some_another_server;  # si es necesario
    add_upstream    upstream3 backup;     # src upstream 3
}

Directiva combine_server_singlets

Produce múltiples singlet upstreams a partir de los servidores definidos hasta ahora en el upstream host. Un singlet upstream contiene solo un servidor activo mientras que otros servidores están marcados como de respaldo o inactivos. Si no se pasan parámetros, los singlet upstreams tendrán nombres del upstream host con un número de orden agregado del servidor activo en el upstream host. Se pueden usar 2 parámetros opcionales para ajustar sus nombres. El primer parámetro es un sufijo agregado después del nombre del upstream host y antes del número de orden. El segundo parámetro debe ser un valor entero que define la alineación cero del número de orden. Por ejemplo, si tiene un valor de 2, entonces los números de orden podrían ser '01', '02', ..., '10', ... '100' ....

Para marcar servidores secundarios como inactivos en lugar de de respaldo, usa otro parámetro opcional nobackup. Este parámetro debe colocarse al final, después de todos los demás parámetros.

Un ejemplo

upstream  uhost {
    server                   s1;
    server                   s2;
    server                   s3 backup;
    server                   s4;
    # construir singlet upstreams uhost_single_01,
    # uhost_single_02, uhost_single_03 y uhost_single_04
    combine_server_singlets  _single_ 2;
    server                   s5;
}

¿Por qué números, no nombres?

En el ejemplo anterior, los singlet upstreams tendrán nombres como uhost_single_01, pero nombres que contienen nombres de servidores como uhost_single_s1 se verían mejor y serían más convenientes. ¿Por qué no usarlos en lugar de números de orden? Desafortunadamente, Nginx no recuerda los nombres de los servidores después de que un servidor ha sido agregado a un upstream, por lo tanto, no podemos simplemente recuperarlos.

Actualización. ¡Hay buenas noticias! Desde la versión 1.7.2, Nginx recuerda los nombres de los servidores en los datos del upstream y ahora podemos usarlos al referirnos a una palabra clave especial byname. Por ejemplo,

    combine_server_singlets  byname;
    # o
    combine_server_singlets  _single_ byname;

Todos los dos puntos (:) en los nombres de los servidores se reemplazan con guiones bajos (_).

¿Dónde puede ser útil esto?

Un singlet upstream actúa como un solo servidor con modo de respaldo. Esto puede ser utilizado para gestionar sesiones HTTP persistentes cuando los servidores backend se identifican con un mecanismo adecuado como cookies HTTP.

upstream  uhost {
    server  s1;
    server  s2;
    combine_server_singlets;
}

server {
    listen       8010;
    server_name  main;
    location / {
        proxy_pass http://uhost$cookie_rt;
    }
}
server {
    listen       8020;
    server_name  server1;
    location / {
        add_header Set-Cookie "rt=1";
        echo "Pasado a $server_name";
    }
}
server {
    listen       8030;
    server_name  server2;
    location / {
        add_header Set-Cookie "rt=2";
        echo "Pasado a $server_name";
    }
}

En esta configuración, la primera solicitud del cliente elegirá el servidor backend aleatoriamente, el servidor elegido establecerá la cookie rt a un valor predefinido (1 o 2), y todas las solicitudes posteriores de este cliente serán enviadas al servidor elegido automáticamente hasta que se caiga. Digamos que fue server1, entonces cuando se cae, la cookie rt en el lado del cliente seguirá siendo 1. La directiva proxy_pass enviará la siguiente solicitud del cliente a un singlet upstream uhost1 donde server1 se declara activo y server2 está respaldado. Tan pronto como server1 ya no sea accesible, Nginx enviará la solicitud a server2 que reescribirá la cookie rt y todas las solicitudes posteriores del cliente serán enviadas a server2 hasta que se caiga.

Directiva extend_single_peers

Los pares en los upstreams fallan de acuerdo con las reglas listadas en la directiva proxy_next_upstream. Si un upstream tiene solo un par en su parte principal o de respaldo, entonces este par nunca fallará. Esto puede ser un problema serio al escribir un algoritmo personalizado para chequeos de salud activos de los pares upstream. La directiva extend_single_peers, al ser declarada en un bloque upstream, agrega un par falso marcado como down en la parte principal o de respaldo del upstream si la parte originalmente contiene solo un par. Esto hace que Nginx marque el par único original como fallido cuando no logra pasar las reglas de proxy_next_upstream al igual que en el caso general de múltiples pares.

Un ejemplo

upstream  upstream1 {
    server  s1;
    extend_single_peers;
}

upstream  upstream2 {
    server  s1;
    server  s2;
    server  s3 backup;
    extend_single_peers;
}

Ten en cuenta que si una parte (la principal o la de respaldo) de un upstream contiene más de un par (como la parte principal en upstream2 del ejemplo), entonces la directiva no tiene efecto: particularmente, en el upstream2 solo afecta la parte de respaldo del upstream.

Bloque upstrand

Está destinado a configurar una supercapa de upstreams que no pierden su identidad. Acepta un número de directivas incluyendo upstream, order, next_upstream_statuses y otras. Los upstreams cuyos nombres comienzan con tilde (~) coinciden con una expresión regular. Solo se consideran candidatos los upstreams que ya han sido declarados antes de la definición del bloque upstrand.

Un ejemplo

upstrand us1 {
    upstream ~^u0 blacklist_interval=60s;
    upstream b01 backup;
    order start_random;
    next_upstream_statuses error timeout non_idempotent 204 5xx;
    next_upstream_timeout 60s;
    intercept_statuses 5xx /Internal/failover;
}

El upstrand us1 combinará todos los upstreams cuyos nombres comienzan con u0 y el upstream b01 como respaldo. Los upstreams de respaldo se verifican si todos los upstreams normales fallan. La fallas significa que todos los upstreams en ciclos normales o de respaldo han respondido con estados listados en la directiva next_upstream_statuses o han sido blacklisted. Aquí, la respuesta del upstream significa el estado devuelto por el último servidor del upstream, que se ve fuertemente afectado por el valor de la directiva proxy_next_upstream. Un upstream se establece como blacklist cuando tiene el parámetro blacklist_interval y responde con un estado listado en el next_upstream_statuses. El estado de blacklist no se comparte entre los procesos trabajadores de Nginx.

Las siguientes cuatro directivas de upstrand son similares a las del módulo proxy de Nginx.

La directiva next_upstream_statuses acepta la notación de estados 4xx y 5xx y los valores error y timeout para distinguir entre los casos en que ocurren errores con las conexiones de los pares del upstream de aquellos en que los backends envían estados 502 o 504 (los valores simples 502 y 504 así como 5xx se refieren a ambos casos). También acepta el valor non_idempotent para permitir el procesamiento adicional de solicitudes no idempotentes cuando fueron respondidas por el último servidor de un upstream pero fallaron según otros estados listados en la directiva. Las solicitudes se consideran no idempotentes cuando sus métodos son POST, LOCK o PATCH, al igual que en la directiva proxy_next_upstream.

La directiva next_upstream_timeout limita la duración total del tiempo que el upstrand cicla a través de todos sus upstreams. Si el tiempo se agota mientras el upstrand está listo para pasar a un siguiente upstream, se devuelve el resultado del último ciclo del upstream.

La directiva intercept_statuses permite failover de upstrand interceptando la respuesta final en una ubicación que coincide con la URI dada. Las interceptaciones deben ocurrir incluso cuando el upstrand se agota. Ten en cuenta también que recorrer upstreams en un upstrand y la URI de failover del upstrand no son interceptables. Hablando más generalmente, cualquier redirección interna (por error_page, proxy_intercept_errors, X-Accel-Redirect, etc.) romperá subsolicitudes anidadas en las que se basa la implementación del upstrand, lo que lleva a devolver respuestas vacías. Estos son casos extremadamente malos, y por eso se protegió el recorrido a través de upstreams contra interceptaciones. La URI de failover del upstrand se ve más afectada por esto ya que la implementación tiene menos control sobre su ubicación. Particularmente, el failover del upstrand tiene solo protección contra interceptaciones por error_page y proxy_intercept_errors. Esto significa que la ubicación de la URI de failover del upstrand debe ser lo más simple posible (por ejemplo, usando directivas simples como return o echo).

Dicho esto, hay una solución decente al problema de las ubicaciones de failover del upstrand y las redirecciones internas en ellas. ¿Cómo exactamente rompen las redirecciones internas las subsolicitudes? Bueno, borran los contextos de subsolicitudes necesarios en los filtros de respuesta del módulo. Entonces, si pudiéramos hacer que el contexto de la subsolicitud fuera persistente, ¿resolveríamos el problema? ¡La respuesta es sí! El módulo Nginx nginx-easy-context permite construir contextos de solicitud persistentes. Los upstrands pueden beneficiarse de ellos activando un interruptor en el archivo config y construyendo ambos módulos. Consulta los detalles en la sección Build and test.

La directiva order actualmente acepta solo un valor start_random que significa que los upstreams en ciclos normales y de respaldo después de que el trabajador se inicie serán elegidos aleatoriamente. Los upstreams en solicitudes posteriores se ciclarán de manera round-robin. Además, se acepta un modificador per_request también en la directiva order: desactiva el ciclo round-robin global por trabajador. La combinación de per_request y start_random hace que el upstream inicial en cada nueva solicitud sea elegido aleatoriamente.

Tal failover entre estados de fallo puede alcanzarse durante una sola solicitud alimentando una variable especial que comienza con upstrand_ a la directiva proxy_pass así:

location /us1 {
    proxy_pass http://$upstrand_us1;
}

¡Ten cuidado al acceder a esta variable desde otras directivas! ¡Esto inicia la maquinaria de subsolicitudes que puede no ser deseable en muchos casos!

Variables de estado de upstrand

Hay varias variables de estado de upstrand disponibles: upstrand_addr, upstrand_cache_status, upstrand_connect_time, upstrand_header_time, upstrand_response_length, upstrand_response_time y upstrand_status. Todas son contrapartes de las correspondientes variables de upstream y contienen los valores de estas para todos los upstreams pasados a través de una solicitud y todas las subsolicitudes cronológicamente. La variable upstrand_path contiene la ruta de todos los upstreams visitados durante la solicitud.

¿Dónde puede ser útil esto?

El upstrand se parece mucho a un simple upstream combinado, pero también tiene una diferencia crucial: los upstreams dentro de un upstrand no se aplanan y mantienen su identidad. Esto da la posibilidad de configurar un estado de failover para un grupo de servidores asociados con un solo upstream sin necesidad de verificarlos todos por turno. En el ejemplo anterior, el upstrand us1 puede contener una lista de upstreams como u01, u02, etc. Imagina que el upstream u01 contiene 10 servidores y representa una parte de un sistema backend distribuido geográficamente. Deja que el upstrand us1 combine todas esas partes en un todo, y dejemos que ejecutemos una aplicación cliente que sondee las partes para realizar algunas tareas. Deja que los backends envíen el estado HTTP 204 si no tienen nuevas tareas. En un upstream combinado plano, todos los 10 servidores pueden haber sido sondeados antes de que la aplicación finalmente reciba una nueva tarea de otro upstream. El upstrand us1 permite saltar al siguiente upstream después de verificar el primer servidor en un upstream que no tiene tareas. Esta maquinaria es claramente adecuada para difusión de upstream, cuando los mensajes se envían a todos los upstreams en un upstrand.

Los ejemplos anteriores muestran que un upstrand puede considerarse un upstream 2-dimensional que comprende varios clústeres que representan upstreams naturales y permite un ciclo corto sobre ellos.

Para ilustrar esto, emulemos un upstream sin balanceo round-robin. Cada nueva solicitud de cliente comenzará por proxy a el primer servidor en la lista de upstream y luego fallará al siguiente servidor.

    upstream u1 {
        server localhost:8020;
        server localhost:8030;
        combine_server_singlets _single_ nobackup;
    }

    upstrand us1 {
        upstream ~^u1_single_ blacklist_interval=60s;
        order per_request;
        next_upstream_statuses error timeout non_idempotent 5xx;
        intercept_statuses 5xx /Internal/failover;
    }

La directiva combine_server_singlets en el upstream u1 genera dos singlet upstreams u1_single_1 y u1_single_2 para habitar el upstrand us1. Debido a la ordenación per_request dentro del upstrand, los dos upstreams serán recorridos en el orden u1_single_1 → u1_single_2 en cada solicitud del cliente.

Directiva dynamic_upstrand

Permite elegir un upstrand de las variables pasadas en tiempo de ejecución. La directiva puede ser establecida en cláusulas de servidor, ubicación y ubicación-si.

En la siguiente configuración

    upstrand us1 {
        upstream ~^u0;
        upstream b01 backup;
        order start_random;
        next_upstream_statuses 5xx;
    }
    upstrand us2 {
        upstream ~^u0;
        upstream b02 backup;
        order start_random;
        next_upstream_statuses 5xx;
    }

    server {
        listen       8010;
        server_name  main;

        dynamic_upstrand $dus1 $arg_a us2;

        location / {
            dynamic_upstrand $dus2 $arg_b;
            if ($arg_b) {
                proxy_pass http://$dus2;
                break;
            }
            proxy_pass http://$dus1;
        }
    }

Los upstrands devueltos en las variables dus1 y dus2 deben ser elegidos de los valores de las variables arg_a y arg_b. Si arg_b está establecido, entonces la solicitud del cliente será enviada a un upstrand con un nombre igual al valor de arg_b. Si no hay un upstrand con este nombre, entonces dus2 estará vacío y proxy_pass devolverá el estado HTTP 500. Para prevenir la inicialización de una variable de upstrand dinámica con un valor vacío, su declaración debe ser terminada con un nombre literal que corresponda a un upstrand existente. En este ejemplo, la variable de upstrand dinámica dus1 será inicializada por el upstrand us2 si arg_a está vacío o no está establecido. En total, si arg_b no está establecido o está vacío y arg_a está establecido y tiene un valor igual a un upstrand existente, la solicitud será enviada a este upstrand, de lo contrario (si arg_b no está establecido o está vacío y arg_a está establecido pero no se refiere a un upstrand existente) proxy_pass probablemente devolverá el estado HTTP 500 (excepto si hay una variable compuesta de la cadena literal upstrand_ y el valor de arg_a que apunta a un destino válido), de lo contrario (ambos arg_b y arg_a no están establecidos o vacíos) la solicitud será enviada al upstrand us2.

Ver también

Hay varios artículos sobre el módulo en mi blog, en orden cronológico:

  1. Простой модуль nginx для создания комбинированных апстримов (en ruso). Un artículo completo que descubre detalles de la implementación de la directiva add_upstream que también puede considerarse como un pequeño tutorial para el desarrollo de módulos Nginx.
  2. nginx upstrand to configure super-layers of upstreams. Una visión general del uso del bloque upstrand y algunos detalles sobre su implementación.
  3. Не такой уж простой модуль nginx для создания комбинированных апстримов (en ruso). Una visión general de todas las características del módulo con ejemplos de configuración y muestras de sesiones de prueba.

GitHub

Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-combined-upstreams.