Pular para conteúdo

combined-upstreams: Módulo NGINX Combined Upstreams

Instalação

Você pode instalar este módulo em qualquer distribuição baseada em RHEL, incluindo, mas não se limitando a:

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

Ative o módulo adicionando o seguinte no topo de /etc/nginx/nginx.conf:

load_module modules/ngx_http_combined_upstreams_module.so;

Este documento descreve o nginx-module-combined-upstreams v2.3.1 lançado em 15 de abril de 2025.


O módulo introduz três diretivas add_upstream, combine_server_singlets e extend_single_peers disponíveis dentro dos blocos de configuração upstream, e um novo bloco de configuração upstrand para construir supercamadas de upstreams. Além disso, a diretiva dynamic_upstrand é introduzida para escolher upstrands em tempo de execução.

Diretiva add_upstream

Popula o upstream host com servidores listados em um upstream já definido especificado pelo parâmetro obrigatório 1 da diretiva. Os atributos do servidor, como weights, max_fails e outros, são mantidos no upstream host. Os parâmetros opcionais podem incluir valores backup para marcar todos os servidores do upstream de origem como servidores de backup e weight=N para calibrar os pesos dos servidores do upstream de origem multiplicando-os pelo fator N.

Um exemplo

upstream  combined {
    add_upstream    upstream1;            # src upstream 1
    add_upstream    upstream2 weight=2;   # src upstream 2
    server          some_another_server;  # se necessário
    add_upstream    upstream3 backup;     # src upstream 3
}

Diretiva combine_server_singlets

Produz múltiplos singlet upstreams a partir dos servidores definidos até agora no upstream host. Um singlet upstream contém apenas um servidor ativo, enquanto os outros servidores são marcados como backup ou fora do ar. Se nenhum parâmetro for passado, os singlet upstreams terão nomes do upstream host acrescidos pelo número de ordem do servidor ativo no upstream host. Dois parâmetros opcionais podem ser usados para ajustar seus nomes. O 1º parâmetro é um sufixo adicionado após o nome do upstream host e antes do número de ordem. O 2º parâmetro deve ser um valor inteiro que define o zero-alignment do número de ordem. Por exemplo, se tiver o valor 2, os números de ordem poderiam ser '01', '02', ..., '10', ... '100' ....

Para marcar servidores secundários como fora do ar em vez de backup, use outro parâmetro opcional nobackup. Este parâmetro deve ser colocado no final, após todos os outros parâmetros.

Um exemplo

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

Por que números, não nomes?

No exemplo acima, os singlet upstreams terão nomes como uhost_single_01, mas nomes que contêm nomes de servidores como uhost_single_s1 pareceriam melhores e mais convenientes. Por que não usá-los em vez de números de ordem? Infelizmente, o NGINX não se lembra dos nomes dos servidores após um servidor ter sido adicionado a um upstream, portanto, não podemos simplesmente recuperá-los.

Atualização. Há uma boa notícia! Desde a versão 1.7.2, o NGINX se lembra dos nomes dos servidores nos dados do upstream e agora podemos usá-los ao nos referirmos a uma palavra-chave especial byname. Por exemplo,

    combine_server_singlets  byname;
    # ou
    combine_server_singlets  _single_ byname;

Todos os dois-pontos (:) nos nomes dos servidores são substituídos por sublinhados (_).

Onde isso pode ser útil

Um singlet upstream atua como um único servidor com modo de fallback. Isso pode ser usado para gerenciar sessões HTTP persistentes quando servidores de backend se identificam com um mecanismo apropriado, 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 "Passado para $server_name";
    }
}
server {
    listen       8030;
    server_name  server2;
    location / {
        add_header Set-Cookie "rt=2";
        echo "Passado para $server_name";
    }
}

Nesta configuração, a primeira solicitação do cliente escolherá o servidor de backend aleatoriamente, o servidor escolhido definirá o cookie rt para um valor pré-definido (1 ou 2), e todas as solicitações subsequentes deste cliente serão encaminhadas automaticamente para o servidor escolhido até que ele fique fora do ar. Digamos que foi server1, então quando ele ficar fora do ar, o cookie rt no lado do cliente ainda será 1. A diretiva proxy_pass roteará a próxima solicitação do cliente para um singlet upstream uhost1 onde server1 é declarado ativo e server2 está em backup. Assim que server1 não estiver mais acessível, o NGINX roteará a solicitação para server2 que reescreverá o cookie rt e todas as solicitações subsequentes do cliente serão encaminhadas para server2 até que ele fique fora do ar.

Diretiva extend_single_peers

Os peers nos upstreams falham de acordo com as regras listadas na diretiva proxy_next_upstream. Se um upstream tiver apenas um peer em sua parte principal ou de backup, então esse peer nunca falhará. Isso pode ser um problema sério ao escrever um algoritmo personalizado para verificações de saúde ativas dos peers do upstream. A diretiva extend_single_peers, sendo declarada em um bloco upstream, adiciona um peer falso marcado como down na parte principal ou de backup do upstream se a parte originalmente contém apenas um peer. Isso faz com que o NGINX marque o peer único original como falhado quando ele falha em passar pelas regras de proxy_next_upstream, assim como no caso geral de múltiplos peers.

Um exemplo

upstream  upstream1 {
    server  s1;
    extend_single_peers;
}

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

Observe que se uma parte (a principal ou a de backup) de um upstream contém mais de um peer (como a parte principal em upstream2 do exemplo), então a diretiva não tem efeito: particularmente, no upstream2 ela afeta apenas a parte de backup do upstream.

Bloco upstrand

Destina-se a configurar uma supercamada de upstreams que não perdem suas identidades. Aceita um número de diretivas, incluindo upstream, order, next_upstream_statuses e outras. Upstreams com nomes começando com til (~) correspondem a uma expressão regular. Apenas upstreams que já foram declarados antes da definição do bloco upstrand são considerados candidatos.

Um exemplo

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

O upstrand us1 combinará todos os upstreams cujos nomes começam com u0 e o upstream b01 como backup. Os upstreams de backup são verificados se todos os upstreams normais falharem. A falha significa que todos os upstreams nos ciclos normais ou de backup responderam com status listados na diretiva next_upstream_statuses ou foram blacklisted. Aqui, a resposta do upstream significa o status retornado pelo último servidor do upstream, que é fortemente afetado pelo valor da diretiva proxy_next_upstream. Um upstream é definido como blacklisted quando tem o parâmetro blacklist_interval e responde com um status listado nos next_upstream_statuses. O estado de blacklisting não é compartilhado entre os processos trabalhadores do NGINX.

As próximas quatro diretivas do upstrand são semelhantes às do módulo proxy do NGINX.

A diretiva next_upstream_statuses aceita notação de status 4xx e 5xx e valores error e timeout para distinguir entre casos em que ocorrem erros com as conexões dos peers do upstream e aqueles em que os backends enviam status 502 ou 504 (valores simples 502 e 504 assim como 5xx referem-se a ambos os casos). Ela também aceita o valor non_idempotent para permitir o processamento adicional de requisições non-idempotent quando foram respondidas pelo último servidor de um upstream, mas falharam de acordo com outros status listados na diretiva. As requisições são consideradas não idempotentes quando seus métodos são POST, LOCK ou PATCH, assim como na diretiva proxy_next_upstream.

A diretiva next_upstream_timeout limita a duração total que o upstrand cicla por todos os seus upstreams. Se o tempo expirar enquanto o upstrand está pronto para passar para um próximo upstream, o resultado do último ciclo do upstream é retornado.

A diretiva intercept_statuses permite failover do upstrand interceptando a resposta final em uma localização que corresponde à URI dada. As interceptações devem ocorrer mesmo quando o upstrand expira. Observe também que percorrer upstreams em um upstrand e a URI de failover do upstrand não são interceptáveis. Falando de forma mais geral, qualquer redirecionamento interno (por error_page, proxy_intercept_errors, X-Accel-Redirect etc.) quebrará subrequisições aninhadas nas quais a implementação do upstrand se baseia, o que leva a retornar respostas vazias. Esses são casos extremamente ruins, e é por isso que percorrer upstreams foi protegido contra interceptações. A URI de failover do upstrand é mais afetada por isso, pois a implementação tem menos controle sobre sua localização. Particularmente, o failover do upstrand tem apenas proteção contra interceptações por error_page e proxy_intercept_errors. Isso significa que a localização da URI de failover do upstrand deve ser o mais simples possível (por exemplo, usando diretivas simples como return ou echo).

Dito isso, há uma solução decente para o problema com as localizações de failover do upstrand e redirecionamentos internos nelas. Como exatamente os redirecionamentos internos quebram subrequisições? Bem, eles apagaram os contextos de subrequisição necessários nos filtros de resposta do módulo. Portanto, se pudéssemos tornar o contexto da subrequisição persistente, resolveríamos o problema? A resposta é sim! O módulo NGINX nginx-easy-context permite construir contextos de requisição persistentes. Os upstrands podem se beneficiar deles ativando um interruptor no arquivo config e construindo ambos os módulos. Veja detalhes na seção Build and test.

A diretiva order atualmente aceita apenas um valor start_random, que significa que os upstreams em ciclos normais e de backup após o trabalhador ser iniciado serão escolhidos aleatoriamente. Os upstreams em solicitações posteriores serão ciclicamente escolhidos em modo round-robin. Além disso, um modificador per_request também é aceito na diretiva order: ele desativa o ciclo round-robin global por trabalhador. A combinação de per_request e start_random faz com que o upstream inicial em cada nova solicitação seja escolhido aleatoriamente.

Tal failover entre status de falha pode ser alcançado durante uma única solicitação alimentando uma variável especial que começa com upstrand_ na diretiva proxy_pass assim:

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

Tenha cuidado ao acessar esta variável de outras diretivas! Ela inicia a mecânica de subrequisições, o que pode não ser desejável em muitos casos.

Variáveis de status do upstrand

Há várias variáveis de status do upstrand disponíveis: upstrand_addr, upstrand_cache_status, upstrand_connect_time, upstrand_header_time, upstrand_response_length, upstrand_response_time e upstrand_status. Todas elas são correspondentes às variáveis upstream correspondentes e contêm os valores destas para todos os upstreams percorridos durante a requisição e todas as subrequisições cronologicamente. A variável upstrand_path contém o caminho de todos os upstreams visitados durante a requisição.

Onde isso pode ser útil

O upstrand parece muito semelhante a um upstream combinado simples, mas também tem uma diferença crucial: os upstreams dentro de um upstrand não são achatados e mantêm suas identidades. Isso possibilita configurar um status de failover para um grupo de servidores associados a um único upstream sem a necessidade de verificá-los todos por turno. No exemplo acima, o upstrand us1 pode manter uma lista de upstreams como u01, u02 etc. Imagine que o upstream u01 contém 10 servidores internos e representa uma parte de um sistema de backend distribuído geograficamente. Deixe o upstrand us1 combinar todas essas partes em um todo, e vamos executar um aplicativo cliente que consulta as partes para realizar algumas tarefas. Deixe os backends enviarem status HTTP 204 se não tiverem novas tarefas. Em um upstream combinado plano, todos os 10 servidores podem ter sido consultados antes que o aplicativo receba finalmente uma nova tarefa de outro upstream. O upstrand us1 permite pular para o próximo upstream após verificar o primeiro servidor em um upstream que não tem tarefas. Essa mecânica é claramente adequada para broadcasting de upstream, quando mensagens estão sendo enviadas para todos os upstreams em um upstrand.

Os exemplos acima mostram que um upstrand pode ser considerado um upstream 2-dimensional que compreende vários clusters representando upstreams naturais e permite ciclos curtos sobre eles.

Para ilustrar isso, vamos emular um upstream sem balanceamento round-robin. Cada nova solicitação de cliente começará encaminhando para o primeiro servidor na lista de upstream e, em seguida, falhará para o próximo 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;
    }

A diretiva combine_server_singlets no upstream u1 gera dois singlet upstreams u1_single_1 e u1_single_2 para habitar o upstrand us1. Devido ao ordenamento per_request dentro do upstrand, os dois upstreams serão percorridos na ordem u1_single_1 → u1_single_2 em cada solicitação do cliente.

Diretiva dynamic_upstrand

Permite escolher um upstrand a partir de variáveis passadas em tempo de execução. A diretiva pode ser definida em cláusulas de servidor, localização e localização-se.

Na seguinte configuração

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

Os upstrands retornados nas variáveis dus1 e dus2 devem ser escolhidos a partir dos valores das variáveis arg_a e arg_b. Se arg_b estiver definido, a solicitação do cliente será enviada para um upstrand com nome igual ao valor de arg_b. Se não houver um upstrand com esse nome, então dus2 ficará vazio e proxy_pass retornará o status HTTP 500. Para evitar a inicialização de uma variável de upstrand dinâmica com valor vazio, sua declaração deve ser encerrada com um nome literal que corresponda a um upstrand existente. Neste exemplo, a variável de upstrand dinâmica dus1 será inicializada pelo upstrand us2 se arg_a estiver vazio ou não definido. No total, se arg_b não estiver definido ou vazio e arg_a estiver definido e tiver um valor igual a um upstrand existente, a solicitação será enviada para esse upstrand, caso contrário (se arg_b não estiver definido ou vazio e arg_a estiver definido, mas não se referir a um upstrand existente) proxy_pass provavelmente retornará o status HTTP 500 (exceto se houver uma variável composta da string literal upstrand_ e o valor de arg_a que aponta para um destino válido), caso contrário (tanto arg_b quanto arg_a não estão definidos ou vazios) a solicitação será enviada para o upstrand us2.

Veja também

Há vários artigos sobre o módulo no meu blog, em ordem cronológica:

  1. Простой модуль nginx для создания комбинированных апстримов (em russo). Um artigo abrangente que descobre detalhes da implementação da diretiva add_upstream, que também pode ser considerado um pequeno tutorial para desenvolvimento de módulos NGINX.
  2. nginx upstrand to configure super-layers of upstreams. Uma visão geral do uso do bloco upstrand e alguns detalhes sobre sua implementação.
  3. Не такой уж простой модуль nginx для создания комбинированных апстримов (em russo). Uma visão geral de todos os recursos do módulo com exemplos de configuração e amostras de sessões de teste.

GitHub

Você pode encontrar dicas adicionais de configuração e documentação para este módulo no repositório GitHub para nginx-module-combined-upstreams.