nginx servindo conteúdo de proxy errado

nginx servindo conteúdo de proxy errado

Eu tenho um único servidor hospedando vários sites Rails. Todos os sites são identificados por nomes de host exclusivos (todos os nomes de domínio são mapeados para o mesmo endereço IP). Tanto Rails quanto nginx estão rodando em contêineres Docker. Estou usando o nginx 1.23.1 em execução em uma imagem Docker criada a partir da imagem oficial do Docker (adicionei apenas o certbot para processamento de certificado TLS).

Depois de adicionar recentemente outro site, uma coisa muito estranha começou a acontecer. Logo após iniciar o nginx, tudo funciona conforme o esperado: todo o conteúdo retornado corresponde ao nome do host na solicitação. Mas depois de algumas horas, o conteúdo retornado não corresponde ao nome do host solicitado. Mas isso afeta apenas o conteúdo do proxy; todos os recursos estáticos ainda são servidos corretamente com base no nome do host.

Por exemplo, quando solicitohttps://www.meaaa.com(todos os domínios aqui são exemplos, não domínios reais), obtenho o conteúdo HTML em bbb.me.com. E como o conteúdo do bbb.me.com pede estilos e imagens que espera encontrar no bbb.me.com, o servidor responde a todas essas solicitações com 404 (porque os ativos estáticos são servidos a partir dowww.meaaa.comarquivos, já que o nome do host da solicitação éwww.meaaa.com).

E se eu pedirhttps://bbb.me.com, recebo o conteúdo HTML dewww.meaaa.com. Novamente, espera-se que os ativos especificados na margem venham dewww.meaaa.com, mas como os ativos estáticos são buscados corretamente de acordo com o nome do host bbb.me.com na solicitação, eles não são encontrados.

Portanto, o conteúdo upstream do Rails dos dois sites parece ter trocado de lugar, enquanto os ativos estáticos são servidos corretamente.

Tenho usado nginx não-Docker há anos com vários sites Rails e nunca vi isso acontecer. Não se trata de solicitar um host indefinido; ambos os hosts são declarados na configuração. Se um host parasse de ser reconhecido, eu poderia assumir que o conteúdo retornado era apenas o servidor padrão, mas na verdade ambos são reconhecidos, apenas trocados. O fato de apenas o conteúdo do proxy ser alternado e não os ativos estáticos mostra que ambos os nomes de host estão sendo reconhecidos.

Para resumir os sintomas, aqui está o que o curl mostra:

$ curl -s https://www.meaaa.com | grep '<title>'
  <title>BBB Site</title>

$ curl -s https://bbb.me.com | grep '<title>'
  <title>MEAAA Site</title>

Solicitandowww.meaaa.comativos estáticos individualmente usando owww.meaaa.como nome do host funciona bem, assim como a solicitação de ativos do bbb.me.com do bbb.me.com.

Também verifiquei se o problema não é o Docker. Dentro do contêiner nginx, posso enrolar cada back-end e obter o conteúdo correto:

$ curl -s http://aaa:3000 | grep '<title>'
  <title>MEAAA Site</title>

$ curl -s http://bbb:3000 | grep '<title>'
  <title>BBB Site</title>

Aqui está a configuração dowww.meaaa.comsite:

upstream aaa-rails {
    server aaa:3000;
}

server {
    server_name www.meaaa.com source.meaaa.com aaa.meinternal.com;
    root /var/www/aaa-rails/public;
    index index.html index.htm;

    location /cms {
      deny 172.22.188.2; # public network interface
      try_files $uri @app;
    }
    location / {
        try_files $uri/index.html $uri @app;
    }

    location @app {
        proxy_pass http://aaa-rails;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Origin $scheme://$http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Host $host;
        proxy_redirect off;
    }

    location ~* ~/assets/ {
        try_files $uri @app;
    }

    listen 80;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/www.meaaa.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/www.meaaa.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

E aqui está a configuração do site bbb.me.com:

upstream bbb-rails {
    server bbb:3000;
}

server {
    server_name bbb.me.com bbb-source.me.com bbb.meinternal.com;
    root /var/www/bbb-rails/public;
    index index.html index.htm;

    client_max_body_size 50m;

    location / {
        try_files $uri @app;
    }

    location @app {
        proxy_pass http://bbb-rails;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Origin $scheme://$http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Host $host;
        proxy_redirect off;
    }

    location ~* ~/assets/ {
        try_files $uri @app;
    }

    listen 80;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/bbb.me.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/bbb.me.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

Para mim, o mais estranho é que reiniciar o nginx resolve o problema, mas apenas temporariamente. Não acho que haja nenhum cache em andamento e não vejo nenhum erro nos logs do nginx. Qualquer sugestão sobre o que observar seria muito apreciada.

ATUALIZAR

Os dois sites que mudaram de lugar foram os que usavam HTTPS. Modifiquei vários outros para usar HTTPS, e agora três deles estão errados em uma espécie de round-robin: peça aaa, obtenha bbb; peça bbb, ganhe ccc; peça ccc, receba aaa. Dois outros sites simplesmente não respondem. É como se algum evento imprevisível acionasse o nginx para corromper quaisquer tabelas de roteamento usadas para servir conteúdo proxy.

Por enquanto, como este é um servidor de produção, estou reiniciando o nginx a cada 60 minutos. Estou tentando configurar um servidor de teste como uma duplicata do servidor de produção, esperando que o mesmo problema apareça lá para que eu possa tentar descobrir o problema sem derrubar os sites.

Responder1

Acontece que o problema resultou do fato de que o nginx resolve cada nome de back-end apenas uma vez (quando carrega sua configuração) e depois disso assume que o endereço IP nunca será alterado. Obrigado a Ángel da lista de discussão nginx, que apontou isso.

No meu caso, como cada back-end é uma aplicação web, tenho que executar o logrotate diariamente, para que se os logs ficarem grandes o suficiente, os arquivos de log serão rotacionados e compactados, e o aplicativo Rails correspondente será reiniciado.

Pode facilmente acontecer que dois ou mais aplicativos Rails sejam reiniciados mais ou menos simultaneamente, mas não é garantido que eles serão reiniciados precisamente na ordem certa para que recuperem seu endereço IP anterior na rede interna do Docker. Isso explica o que estava acontecendo: de vez em quando, aplicativos Rails trocavam endereços IP, mas o nginx não sabia disso, então continuou encaminhando solicitações para os endereços IP antigos e, ao mesmo tempo, servindo ativos estáticos corretamente. Simulei isso parando e reiniciando dois aplicativos e consegui reproduzir o problema com exatidão. Além disso, adicionei um script que monitoraria os sites a cada cinco minutos, e os logs mostravam que o problema sempre parecia acontecer na mesma hora, o que era consistente com o logrotate sendo o gatilho.

Ángel destacou que, ao invés de reiniciar o nginx, é possível simplesmente fazer com que ele recarregue a configuração, o que deve resultar em menos interrupções para os visitantes dos sites. Ele também apontou que é possível forçar o nginx a procurar nomes a cada poucos minutos, em vez de apenas uma vez (vejahttps://forum.nginx.org/read.php?2,215830,215832#msg-215832).

Para resolver o problema, pelo menos por enquanto configurei o Docker para atribuir um endereço IP fixo a todos os contêineres. Dessa forma, o nginx pode continuar a resolver nomes apenas uma vez. Isso tem algumas consequências, no entanto, já que agora não consigo executar comandos como db:migrate ou assets:precompile usando o mesmo arquivo docker-compose.yml do aplicativo Rails em execução (você recebe um erro "endereço em uso"); por enquanto, estou usando docker compose execem vez de run, mas isso parece afetar o docker compose restartcomando (você obtém "endereço em uso" por alguns segundos após executar o exec). Se isso se tornar um problema, posso reverter para endereços IP não fixos e fazer com que o nginx recarregue sua configuração como parte do processo de rotação de log.

informação relacionada