Aller au contenu

combined-upstreams: Module NGINX Combined Upstreams

Installation

Vous pouvez installer ce module dans n'importe quelle distribution basée sur RHEL, y compris, mais sans s'y limiter :

  • RedHat Enterprise Linux 7, 8, 9 et 10
  • CentOS 7, 8, 9
  • AlmaLinux 8, 9
  • Rocky Linux 8, 9
  • Amazon Linux 2 et 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

Activez le module en ajoutant ce qui suit en haut de /etc/nginx/nginx.conf :

load_module modules/ngx_http_combined_upstreams_module.so;

Ce document décrit nginx-module-combined-upstreams v2.3.1 publié le 15 avril 2025.


Le module introduit trois directives add_upstream, combine_server_singlets et extend_single_peers disponibles dans les blocs de configuration upstream, et un nouveau bloc de configuration upstrand pour construire des super-couches d'upstreams. De plus, la directive dynamic_upstrand est introduite pour choisir des upstrands à l'exécution.

Directive add_upstream

Remplit l'upstream hôte avec des serveurs listés dans un upstream déjà défini spécifié par le premier paramètre obligatoire de la directive. Les attributs des serveurs tels que les poids, max_fails et autres sont conservés dans l'upstream hôte. Les paramètres optionnels peuvent inclure la valeur backup pour marquer tous les serveurs de l'upstream source comme serveurs de secours et weight=N pour calibrer les poids des serveurs de l'upstream source en les multipliant par le facteur N.

Un exemple

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

Directive combine_server_singlets

Produit plusieurs singlet upstreams à partir des serveurs définis jusqu'à présent dans l'upstream hôte. Un singlet upstream contient uniquement un serveur actif tandis que les autres serveurs sont marqués comme de secours ou hors service. Si aucun paramètre n'est passé, les singlet upstreams auront des noms de l'upstream hôte suivis du numéro d'ordre du serveur actif dans l'upstream hôte. Deux paramètres optionnels peuvent être utilisés pour ajuster leurs noms. Le premier paramètre est un suffixe ajouté après le nom de l'upstream hôte et avant le numéro d'ordre. Le deuxième paramètre doit être une valeur entière qui définit l'alignement à zéro du numéro d'ordre. Par exemple, s'il a la valeur 2, alors les numéros d'ordre pourraient être '01', '02', ..., '10', ... '100' ....

Pour marquer les serveurs secondaires comme hors service plutôt que de secours, utilisez un autre paramètre optionnel nobackup. Ce paramètre doit être placé à la fin, après tous les autres paramètres.

Un exemple

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

Pourquoi des numéros, pas des noms ?

Dans l'exemple ci-dessus, les singlet upstreams auront des noms comme uhost_single_01, mais des noms contenant des noms de serveurs comme uhost_single_s1 seraient plus beaux et plus pratiques. Pourquoi ne pas les utiliser à la place des numéros d'ordre ? Malheureusement, NGINX ne se souvient pas des noms de serveurs après qu'un serveur a été ajouté dans un upstream, par conséquent, nous ne pouvons pas simplement les récupérer.

Mise à jour. Il y a une bonne nouvelle ! Depuis la version 1.7.2, NGINX se souvient des noms de serveurs dans les données d'upstream et nous pouvons maintenant les utiliser en faisant référence à un mot-clé spécial byname. Par exemple,

    combine_server_singlets  byname;
    # ou
    combine_server_singlets  _single_ byname;

Tous les deux-points (:) dans les noms de serveurs sont remplacés par des underscores (_).

Où cela peut-il être utile

Un singlet upstream agit comme un seul serveur en mode de secours. Cela peut être utilisé pour gérer des sessions HTTP collantes lorsque les serveurs backend s'identifient avec un mécanisme approprié tel que des 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 "Passé à $server_name";
    }
}
server {
    listen       8030;
    server_name  server2;
    location / {
        add_header Set-Cookie "rt=2";
        echo "Passé à $server_name";
    }
}

Dans cette configuration, la première requête du client choisira un serveur backend au hasard, le serveur choisi définira le cookie rt à une valeur prédéfinie (1 ou 2), et toutes les requêtes suivantes de ce client seront automatiquement proxifiées vers le serveur choisi jusqu'à ce qu'il tombe en panne. Supposons que c'était server1, alors lorsqu'il tombe en panne, le cookie rt du côté client sera toujours 1. La directive proxy_pass dirigera la prochaine requête du client vers un singlet upstream uhost1server1 est déclaré actif et server2 est en secours. Dès que server1 n'est plus accessible, NGINX dirigera la requête vers server2 qui réécrira le cookie rt et toutes les requêtes suivantes du client seront proxifiées vers server2 jusqu'à ce qu'il tombe en panne.

Directive extend_single_peers

Les pairs dans les upstreams échouent selon les règles énumérées dans la directive proxy_next_upstream. Si un upstream n'a qu'un seul pair dans sa partie principale ou de secours, ce pair ne tombera jamais en panne. Cela peut poser un problème sérieux lors de l'écriture d'un algorithme personnalisé pour des vérifications de santé actives des pairs upstream. La directive extend_single_peers, déclarée dans un bloc upstream, ajoute un pair fictif marqué comme down dans la partie principale ou de secours de l'upstream si la partie contient à l'origine un seul pair. Cela fait que NGINX marque le pair unique original comme échoué lorsqu'il ne parvient pas à passer les règles de proxy_next_upstream tout comme dans le cas général de plusieurs pairs.

Un exemple

upstream  upstream1 {
    server  s1;
    extend_single_peers;
}

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

Remarquez que si une partie (la principale ou la de secours) d'un upstream contient plus d'un pair (comme la partie principale dans upstream2 de l'exemple), alors la directive n'a aucun effet : en particulier, dans le upstream2, elle n'affecte que la partie de secours de l'upstream.

Bloc upstrand

Est destiné à configurer une super-couche d'upstreams qui ne perdent pas leur identité. Accepte un certain nombre de directives, y compris upstream, order, next_upstream_statuses et d'autres. Les upstreams dont les noms commencent par un tilde (~) correspondent à une expression régulière. Seuls les upstreams qui ont déjà été déclarés avant la définition du bloc upstrand sont considérés comme candidats.

Un exemple

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;
}

L'upstrand us1 combinera tous les upstreams dont les noms commencent par u0 et l'upstream b01 comme secours. Les upstreams de secours sont vérifiés si tous les upstreams normaux échouent. L'échec signifie que tous les upstreams dans les cycles normaux ou de secours ont répondu avec des statuts énumérés dans la directive next_upstream_statuses ou ont été blacklistés. Ici, la réponse de l'upstream signifie le statut retourné par le dernier serveur de l'upstream, qui est fortement affecté par la valeur de la directive proxy_next_upstream. Un upstream est considéré comme blacklisté lorsqu'il a le paramètre blacklist_interval et répond avec un statut énuméré dans les next_upstream_statuses. L'état de blacklistage n'est pas partagé entre les processus de travail NGINX.

Les quatre directives upstrand suivantes sont similaires à celles du module proxy NGINX.

La directive next_upstream_statuses accepte la notation des statuts 4xx et 5xx ainsi que les valeurs error et timeout pour faire la distinction entre les cas où des erreurs se produisent avec les connexions des pairs de l'upstream et ceux où les backends envoient des statuts 502 ou 504 (les valeurs simples 502 et 504 ainsi que 5xx se réfèrent aux deux cas). Elle accepte également la valeur non_idempotent pour permettre un traitement ultérieur des requêtes non-idempotentes lorsqu'elles ont été répondues par le dernier serveur d'un upstream mais ont échoué selon d'autres statuts énumérés dans la directive. Les requêtes sont considérées comme non-idempotentes lorsque leurs méthodes sont POST, LOCK ou PATCH tout comme dans la directive proxy_next_upstream.

La directive next_upstream_timeout limite la durée totale que l'upstrand parcourt tous ses upstreams. Si le temps s'écoule alors que l'upstrand est prêt à passer à un upstream suivant, le résultat du dernier cycle d'upstream est retourné.

La directive intercept_statuses permet le failover d'upstrand en interceptant la réponse finale dans une location qui correspond à l'URI donnée. Les interceptions doivent se produire même lorsque l'upstrand expire. Remarquez également que le passage à travers les upstreams dans un upstrand et l'URI de failover d'upstrand ne sont pas interceptables. Pour parler plus généralement, toute redirection interne (par error_page, proxy_intercept_errors, X-Accel-Redirect etc.) rompra les sous-requêtes sur lesquelles l'implémentation de l'upstrand est basée, ce qui conduit à retourner des réponses vides. Ce sont des cas extrêmement mauvais, et c'est pourquoi le passage à travers les upstreams a été protégé contre les interceptions. L'URI de failover d'upstrand est plus affectée par cela car l'implémentation a moins de contrôle sur sa localisation. En particulier, le failover d'upstrand n'a que la protection contre les interceptions par error_page et proxy_intercept_errors. Cela signifie que la location de l'URI de failover d'upstrand doit être aussi simple que possible (par exemple, en utilisant des directives simples comme return ou echo).

Cela dit, il existe une solution décente au problème des emplacements de failover d'upstrand et des redirections internes en eux. Comment exactement les redirections internes rompent les sous-requêtes ? Eh bien, elles effacent les contextes de sous-requête nécessaires dans les filtres de réponse du module. Donc, si nous pouvions rendre le contexte de sous-requête persistant, résoudrions-nous le problème ? La réponse est oui ! Le module NGINX nginx-easy-context permet de construire des contextes de requête persistants. Les upstrands peuvent en bénéficier en activant un commutateur dans le fichier config et en construisant les deux modules. Voir les détails dans la section Build and test.

La directive order accepte actuellement uniquement une valeur start_random qui signifie que les upstreams dans les cycles normaux et de secours après le démarrage du worker seront choisis au hasard. Les upstreams dans les requêtes suivantes seront parcourus de manière round-robin. De plus, un modificateur per_request est également accepté dans la directive order : il désactive le cycle round-robin global par worker. La combinaison de per_request et start_random fait que l'upstream de départ dans chaque nouvelle requête est choisi au hasard.

Un tel failover entre les statuts échec peut être atteint lors d'une seule requête en alimentant une variable spéciale qui commence par upstrand_ à la directive proxy_pass comme suit :

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

Soyez prudent lorsque vous accédez à cette variable à partir d'autres directives ! Elle active le mécanisme des sous-requêtes, ce qui peut ne pas être souhaitable dans de nombreux cas.

Variables d'état d'upstrand

Il existe un certain nombre de variables d'état d'upstrand disponibles : upstrand_addr, upstrand_cache_status, upstrand_connect_time, upstrand_header_time, upstrand_response_length, upstrand_response_time et upstrand_status. Elles sont toutes des équivalents des variables upstream correspondantes et contiennent les valeurs de ces dernières pour tous les upstreams traversés lors d'une requête et toutes les sous-requêtes chronologiquement. La variable upstrand_path contient le chemin de tous les upstreams visités pendant la requête.

Où cela peut-il être utile

Le upstrand ressemble beaucoup à un simple upstream combiné mais il a aussi une différence cruciale : les upstreams à l'intérieur d'un upstrand ne sont pas aplatis et conservent leur identité. Cela donne la possibilité de configurer un statut de failover pour un groupe de serveurs associés à un seul upstream sans avoir besoin de les vérifier tous à tour de rôle. Dans l'exemple ci-dessus, l'upstrand us1 peut contenir une liste d'upstreams comme u01, u02 etc. Imaginez que l'upstream u01 contienne 10 serveurs et représente une partie d'un système backend géographiquement distribué. Laissons l'upstrand us1 combiner toutes ces parties en un tout, et exécutons une application cliente qui interroge les parties pour effectuer certaines tâches. Laissons les backends envoyer un statut HTTP 204 s'ils n'ont pas de nouvelles tâches. Dans un upstream combiné plat, tous les 10 serveurs pourraient avoir été interrogés avant que l'application ne reçoive finalement une nouvelle tâche d'un autre upstream. L'upstrand us1 permet de passer à l'upstream suivant après avoir vérifié le premier serveur dans un upstream qui n'a pas de tâches. Ce mécanisme est manifestement adapté à la diffusion d'upstream, lorsque des messages sont envoyés à tous les upstreams dans un upstrand.

Les exemples ci-dessus montrent qu'un upstrand peut être considéré comme un upstream 2-dimensionnel qui comprend un certain nombre de clusters représentant des upstreams naturels et permet un court-cyclage sur eux.

Pour illustrer cela, émuler un upstream sans équilibrage round-robin. Chaque nouvelle requête client commencera par un proxy vers le premier serveur de la liste des upstreams, puis échouera vers le serveur suivant.

    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 directive combine_server_singlets dans l'upstream u1 génère deux singlet upstreams u1_single_1 et u1_single_2 pour habiter l'upstrand us1. En raison de l'ordre per_request à l'intérieur de l'upstrand, les deux upstreams seront parcourus dans l'ordre u1_single_1 → u1_single_2 à chaque requête client.

Directive dynamic_upstrand

Permet de choisir un upstrand à partir des variables passées à l'exécution. La directive peut être définie dans les clauses server, location et location-if.

Dans la configuration suivante

    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;
        }
    }

Les upstrands retournés dans les variables dus1 et dus2 doivent être choisis à partir des valeurs des variables arg_a et arg_b. Si arg_b est défini, alors la requête client sera envoyée à un upstrand dont le nom est égal à la valeur de arg_b. S'il n'y a pas d'upstrand avec ce nom, alors dus2 sera vide et proxy_pass retournera le statut HTTP 500. Pour éviter l'initialisation d'une variable d'upstrand dynamique avec une valeur vide, sa déclaration doit être terminée par un nom littéral qui correspond à un upstrand existant. Dans cet exemple, la variable d'upstrand dynamique dus1 sera initialisée par l'upstrand us2 si arg_a est vide ou non défini. En résumé, si arg_b n'est pas défini ou vide et arg_a est défini et a une valeur égale à un upstrand existant, la requête sera envoyée à cet upstrand, sinon (si arg_b n'est pas défini ou vide et arg_a est défini mais ne fait pas référence à un upstrand existant) proxy_pass retournera très probablement le statut HTTP 500 (sauf s'il y a une variable composée de la chaîne littérale upstrand_ et de la valeur de arg_a qui pointe vers une destination valide), sinon (si arg_b et arg_a ne sont pas définis ou vides) la requête sera envoyée à l'upstrand us2.

Voir aussi

Il existe plusieurs articles sur le module dans mon blog, par ordre chronologique :

  1. Простой модуль nginx для создания комбинированных апстримов (en russe). Un article complet découvrant les détails de l'implémentation de la directive add_upstream qui peut également être considéré comme un petit tutoriel pour le développement de modules NGINX.
  2. nginx upstrand to configure super-layers of upstreams. Un aperçu de l'utilisation du bloc upstrand et quelques détails sur son implémentation.
  3. Не такой уж простой модуль nginx для создания комбинированных апстримов (en russe). Un aperçu de toutes les fonctionnalités du module avec des exemples de configuration et des échantillons de session de test.

GitHub

Vous pouvez trouver des conseils de configuration supplémentaires et de la documentation pour ce module dans le dépôt GitHub pour nginx-module-combined-upstreams.