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 exec
em vez de run
, mas isso parece afetar o docker compose restart
comando (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.