Nginx reverse proxy - essayez en amont A, puis B, puis A à nouveau

22

J'essaie de configurer nginx en tant que proxy inverse, avec un grand nombre de serveurs principaux. Je voudrais démarrer les backends à la demande (à la première demande qui arrive), j'ai donc un processus de contrôle (contrôlé par des requêtes HTTP) qui démarre le backend en fonction de la demande qu'il reçoit.

Mon problème est de configurer nginx pour le faire. Voici ce que j'ai jusqu'à présent:

server {
    listen 80;
    server_name $DOMAINS;

    location / {
        # redirect to named location
        #error_page 418 = @backend;
        #return 418; # doesn't work - error_page doesn't work after redirect

        try_files /nonexisting-file @backend;
    }

    location @backend {
        proxy_pass http://$BACKEND-IP;
        error_page 502 @handle_502; # Backend server down? Try to start it
    }

    location @handle_502 { # What to do when the backend server is not up
        # Ping our control server to start the backend
        proxy_pass http://127.0.0.1:82;
        # Look at the status codes returned from control server
        proxy_intercept_errors on;
        # Fallback to error page if control server is down
        error_page 502 /fatal_error.html;
        # Fallback to error page if control server ran into an error
        error_page 503 /fatal_error.html;
        # Control server started backend successfully, retry the backend
        # Let's use HTTP 451 to communicate a successful backend startup
        error_page 451 @backend;
    }

    location = /fatal_error.html {
        # Error page shown when control server is down too
        root /home/nginx/www;
        internal;
    }
}

Cela ne fonctionne pas - nginx semble ignorer tous les codes d'état renvoyés par le serveur de contrôle. Aucune des error_pagedirectives de l' @handle_502emplacement ne fonctionne et le code 451 est envoyé tel quel au client.

J'ai renoncé à essayer d'utiliser la redirection interne nginx pour cela, et j'ai essayé de modifier le serveur de contrôle pour émettre une redirection 307 vers le même emplacement (afin que le client réessaye la même demande, mais maintenant avec le serveur principal démarré). Cependant, maintenant nginx écrase bêtement le code d'état avec celui qu'il a obtenu lors de la tentative de demande de backend (502), malgré le fait que le serveur de contrôle envoie un en-tête "Location". Je l'ai finalement "fait fonctionner" en changeant la ligne error_page enerror_page 502 =307 @handle_502;, forçant ainsi toutes les réponses du serveur de contrôle à être renvoyées au client avec un code 307. C'est très hacky et indésirable, car 1) il n'y a aucun contrôle sur ce que nginx devrait faire ensuite en fonction de la réponse du serveur de contrôle (idéalement, nous ne voulons réessayer le backend que si le serveur de contrôle signale le succès), et 2) pas tous HTTP les clients prennent en charge les redirections HTTP (par exemple, les utilisateurs curl et les applications utilisant libcurl doivent activer explicitement les redirections suivantes).

Quelle est la bonne façon de faire en sorte que nginx essaie de faire un proxy vers le serveur en amont A, puis B, puis A de nouveau (idéalement, uniquement lorsque B renvoie un code d'état spécifique)?

Vladimir Panteleev
la source

Réponses:

20

Points clés:

  • Ne vous embêtez pas avec les upstreamblocs pour le basculement, si envoyer un ping à un serveur en amènera un autre - il n'y a aucun moyen de dire à nginx (au moins, pas la version FOSS) que le premier serveur est à nouveau opérationnel. nginx essaiera les serveurs dans l'ordre à la première demande, mais pas les demandes de suivi, malgré tout backup, weightou les fail_timeoutparamètres.
  • Vous devez activer recursive_error_pageslors de l'implémentation du basculement à l'aide d' error_pageemplacements nommés.
  • Activez cette option proxy_intercept_errorspour gérer les codes d'erreur envoyés par le serveur en amont.
  • La =syntaxe (par exemple error_page 502 = @handle_502;) est requise pour gérer correctement les codes d'erreur à l'emplacement nommé. Si =n'est pas utilisé, nginx utilisera le code d'erreur du bloc précédent.

Le rapport original de réponse / recherche suit:


Voici une meilleure solution de contournement que j'ai trouvée, ce qui est une amélioration car elle ne nécessite pas de redirection client:

upstream aba {
    server $BACKEND-IP;
    server 127.0.0.1:82 backup;
    server $BACKEND-IP  backup;
}

...

location / {
    proxy_pass http://aba;
    proxy_next_upstream error http_502;
}

Ensuite, demandez au serveur de contrôle de renvoyer 502 en cas de "succès" et espérez que le code ne sera jamais renvoyé par les backends.


Mise à jour: nginx continue de marquer la première entrée du upstreambloc comme arrêtée, il n'essaie donc pas les serveurs dans l'ordre lors de requêtes successives. J'ai essayé d'ajouter weight=1000000000 fail_timeout=1à la première entrée sans effet. Jusqu'à présent, je n'ai trouvé aucune solution n'impliquant pas de redirection client.


Edit: Une autre chose que j'aimerais savoir - pour obtenir le statut d'erreur du error_pagegestionnaire, utilisez cette syntaxe: error_page 502 = @handle_502;- ce signe égal fera que nginx obtiendra le statut d'erreur du gestionnaire.


Edit: Et je l'ai fait fonctionner! En plus du error_pagecorrectif ci-dessus, il suffisait d'activer recursive_error_pages!

Vladimir Panteleev
la source
1
Pour moi proxy_next_upstream, le truc a fait l'affaire (enfin mon scénario n'était pas aussi complexe que le vôtre), je voulais juste que nginx essaie le serveur suivant si une erreur se produisait, j'ai donc dû ajouter proxy_next_upstream error timeout invalid_header non_idempotent;( non_idempotent, parce que je veux principalement transmettre des POSTrequêtes).
Philipp
1

Vous pouvez essayer quelque chose comme le suivant

upstream backend {
    server a.example.net;
    server b.example.net backup;
}

server {
    listen   80;
    server_name www.example.net;

    proxy_next_upstream error timeout http_502;

    location / {
        proxy_pass http://backend;
        proxy_redirect      off;
        proxy_set_header    Host              $host;
        proxy_set_header    X-Real-IP         $remote_addr;
        proxy_set_header    X-Forwarded-for   $remote_addr;
    }

}
ALex_hha
la source
nginx ne réessayera pas a.example.netaprès avoir échoué une fois sur la même demande. Il enverra au client l'erreur rencontrée lors de la tentative de connexion b.example.net, ce qui ne sera pas ce à quoi il s'attendait à moins que j'implémente également le proxy sur le serveur de contrôle.
Vladimir Panteleev
Et quelle serait votre configuration dans la situation suivante: la demande à l'échec du retour en amont A, l'échec du retour en amont B, puis nous essayons à nouveau en amont A et obtenons également l'échec (502)?
ALex_hha
En amont B est le serveur de contrôle. Son but est de s'assurer que la prochaine requête en amont A réussira. Le but est d'essayer en amont A, s'il a échoué, d'essayer en amont B, s'il a "réussi" (en utilisant notre convention interne de "succès"), d'essayer en amont A à nouveau. Si ma question n'était pas assez claire, faites-moi savoir comment je peux l'améliorer.
Vladimir Panteleev, le
Hmm, supposons que l'amont A soit en panne, par exemple pour un problème matériel. Que va faire l'amont B? Est-il capable de renvoyer la réponse à la demande du client?
ALex_hha
Ce problème ne concerne pas le basculement pour les pannes matérielles. Ce problème concerne le démarrage de backends en amont à la demande. Si le serveur de contrôle (en amont B) ne peut pas réactiver le backend (en amont A), l'idéal est que l'utilisateur reçoive un message d'erreur approprié, mais ce n'est pas le problème que j'essaie de résoudre - le problème est de faire réessayer nginx A après B à nouveau, dans la même requête.
Vladimir Panteleev, le