proxy inverso nginx: intente aguas arriba A, luego B, luego A nuevamente

22

Estoy tratando de configurar nginx como proxy inverso, con una gran cantidad de servidores de fondo. Me gustaría iniciar los backends a pedido (en la primera solicitud que llega), por lo que tengo un proceso de control (controlado por solicitudes HTTP) que inicia el backend dependiendo de la solicitud que reciba.

Mi problema es configurar nginx para hacerlo. Esto es lo que tengo hasta ahora:

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

Esto no funciona: nginx parece ignorar los códigos de estado devueltos por el servidor de control. Ninguna de las error_pagedirectivas de la @handle_502ubicación funciona, y el código 451 se envía tal cual al cliente.

Dejé de intentar usar la redirección interna de nginx para esto, e intenté modificar el servidor de control para emitir una redirección 307 a la misma ubicación (para que el cliente vuelva a intentar la misma solicitud, pero ahora con el servidor de fondo iniciado). Sin embargo, ahora nginx está sobrescribiendo estúpidamente el código de estado con el que obtuvo del intento de solicitud de fondo (502), a pesar de que el servidor de control está enviando un encabezado de "Ubicación". Finalmente lo conseguí "trabajando" cambiando la línea error_page aerror_page 502 =307 @handle_502;, lo que obliga a que todas las respuestas del servidor de control se envíen de vuelta al cliente con un código 307. Esto es muy hacky e indeseable, porque 1) no hay control sobre lo que nginx debe hacer a continuación dependiendo de la respuesta del servidor de control (idealmente solo queremos volver a intentar el backend solo si el servidor de control informa de éxito), y 2) no todo HTTP los clientes admiten redireccionamientos HTTP (por ejemplo, los usuarios curl y las aplicaciones que usan libcurl deben habilitar los siguientes redireccionamientos explícitamente).

¿Cuál es la forma correcta de hacer que nginx intente proxy al servidor ascendente A, luego B, luego A nuevamente (idealmente, solo cuando B devuelve un código de estado específico)?

Vladimir Panteleev
fuente

Respuestas:

20

Puntos clave:

  • No se moleste con los upstreambloques para la conmutación por error, si hacer ping a un servidor abrirá otro, no hay forma de decirle a nginx (al menos, no la versión FOSS) que el primer servidor está activo nuevamente. nginx probará los servidores en orden en la primera solicitud, pero no las solicitudes de seguimiento, a pesar de ninguna backup, weighto la fail_timeoutconfiguración.
  • Usted debe permitir a recursive_error_pagesla hora de implementar la conmutación por error usando error_pagey los lugares mencionados.
  • Habilítelo proxy_intercept_errorspara manejar códigos de error enviados desde el servidor ascendente.
  • La =sintaxis (por ejemplo error_page 502 = @handle_502;) es necesaria para manejar correctamente los códigos de error en la ubicación nombrada. Si =no se usa, nginx usará el código de error del bloque anterior.

La respuesta original / registro de investigación sigue:


Aquí hay una mejor solución que encontré, que es una mejora ya que no requiere una redirección del cliente:

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

Luego, solo haga que el servidor de control devuelva 502 en "éxito" y espere que el código nunca sea devuelto por los servidores.


Actualización: nginx sigue marcando la primera entrada del upstreambloque como inactiva, por lo que no prueba los servidores en orden en solicitudes sucesivas. Intenté agregar weight=1000000000 fail_timeout=1a la primera entrada sin ningún efecto. Hasta ahora no he encontrado ninguna solución que no implique una redirección de cliente.


Editar: Una cosa más que desearía saber: para obtener el estado de error del error_pagecontrolador, use esta sintaxis: error_page 502 = @handle_502;ese signo igual hará que nginx obtenga el estado de error del controlador.


Editar: Y lo tengo funcionando! Además de la error_pagesolución anterior, ¡todo lo que se necesitaba era habilitar recursive_error_pages!

Vladimir Panteleev
fuente
1
Para mí, proxy_next_upstreamhice el truco (bueno, mi escenario no era tan complejo como el tuyo), solo quería que nginx probara el siguiente servidor si ocurriera un error, por lo que tuve que agregar proxy_next_upstream error timeout invalid_header non_idempotent;( non_idempotentporque quiero enviar principalmente POSTsolicitudes).
Philipp
1

Podrías probar algo como lo siguiente

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
fuente
nginx no volverá a intentarlo a.example.netdespués de fallar una vez en la misma solicitud. Enviará al cliente el error encontrado al intentar conectarse b.example.net, que no será lo que esperaban a menos que yo también implemente la proxy en el servidor de control.
Vladimir Panteleev
¿Y qué pasaría con su configuración en la siguiente situación: solicitud al error de retorno ascendente A, error de retorno ascendente B, luego intentamos nuevamente el flujo ascendente A y también fallamos (502)?
ALex_hha
Upstream B es el servidor de control. Su propósito es asegurarse de que la próxima solicitud para el flujo ascendente A tenga éxito. El objetivo es probar el flujo ascendente A, si falla, intente el flujo ascendente B, si "tuvo éxito" (utilizando nuestra convención interna de "éxito"), intente nuevamente el flujo ascendente A. Si mi pregunta no era lo suficientemente clara, avíseme cómo puedo mejorarla.
Vladimir Panteleev
Hmm, supongamos que el flujo ascendente A está inactivo, por ejemplo, por algún problema de hardware. ¿Qué hará aguas arriba B? ¿Es capaz de devolver la respuesta a la solicitud del cliente?
ALex_hha
Este problema no se trata de la conmutación por error para fallas de hardware. Este problema se trata de iniciar backends ascendentes a pedido. Si el servidor de control (upstream B) no puede reactivar el backend (upstream A), lo ideal sería que el usuario reciba un mensaje de error apropiado, pero no es el problema lo que estoy tratando de resolver: el problema es que nginx vuelva a intentarlo A después de B nuevamente, dentro de la misma solicitud.
Vladimir Panteleev