我有一台服务器托管多个 Rails 站点。所有站点都通过唯一的主机名进行标识(所有域名都映射到同一个 IP 地址)。Rails 和 nginx 都在 Docker 容器中运行。我使用的是 nginx 1.23.1,它在从官方 Docker 映像构建的 Docker 映像中运行(我仅添加了 certbot 以进行 TLS 证书处理)。
最近添加另一个网站后,发生了一件非常奇怪的事情。我启动 nginx 后,一切都按预期工作:返回的所有内容都与请求中的主机名匹配。但几个小时后,返回的内容与请求的主机名不匹配。但这只会影响代理内容;所有静态资源仍根据主机名正确提供。
例如,当我请求https://www.meaaa.com(此处的所有域名均为示例,并非真实域名),我从 bbb.me.com 获取 HTML 内容。由于 bbb.me.com 的内容要求提供其希望在 bbb.me.com 中找到的样式和图像,因此服务器对所有这些请求都响应 404(因为静态资产由www.meaaa.com文件,因为请求主机名是www.meaaa.com)。
如果我要求https://bbb.me.com,我从中获取 HTML 内容www.meaaa.com。同样,标记中指定的资产预计来自www.meaaa.com,但由于根据请求中的主机名 bbb.me.com 正确获取了静态资产,因此找不到它们。
因此,两个站点的上游 Rails 内容似乎已经交换了位置,而静态资产则得到了正确的提供。
多年来,我一直在多个 Rails 站点上使用非 Docker nginx,从未见过这种情况。这不是请求未定义主机的问题;两个主机都在配置中声明。如果一个主机不再被识别,那么我可以假设返回的内容只是默认服务器,但实际上它们都被识别了,只是交换了。只有代理内容被切换而不是静态资产被切换,这表明两个主机名都被识别了。
总结一下症状,以下是 curl 显示的内容:
$ curl -s https://www.meaaa.com | grep '<title>'
<title>BBB Site</title>
$ curl -s https://bbb.me.com | grep '<title>'
<title>MEAAA Site</title>
请求www.meaaa.com静态资产单独使用www.meaaa.com主机名工作正常,从 bbb.me.com 请求 bbb.me.com 资产也是如此。
我还确认问题不是出在 Docker 上。在 nginx 容器内部,我可以 curl 每个后端并获取正确的内容:
$ curl -s http://aaa:3000 | grep '<title>'
<title>MEAAA Site</title>
$ curl -s http://bbb:3000 | grep '<title>'
<title>BBB Site</title>
这是配置www.meaaa.com地点:
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
}
以下是 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
}
对我来说最奇怪的是重启 nginx 可以解决问题,但只是暂时的。我认为没有任何缓存在进行,而且我在 nginx 日志中没有看到任何错误。任何关于应该查看什么的建议都将不胜感激。
更新
更改位置的两个站点是使用 HTTPS 的站点。我修改了其他几个站点以使用 HTTPS,现在其中三个站点在某种循环中出错:请求 aaa,获取 bbb;请求 bbb,获取 ccc;请求 ccc,获取 aaa。另外两个站点根本没有响应。就好像某些不可预测的事件触发了 nginx 破坏它用于提供代理内容的任何路由表。
目前,由于这是一台生产服务器,我每 60 分钟重启一次 nginx。我试图设置一个临时服务器作为生产服务器的副本,希望同样的问题会出现在那里,这样我就可以在不关闭网站的情况下尝试找出问题所在。
答案1
事实证明,问题源于 nginx 仅解析每个后端名称一次(加载其配置时),之后假设 IP 地址永远不会改变。感谢 nginx 邮件列表的 Ángel 指出了这一点。
就我而言,由于每个后端都是一个 Web 应用程序,因此我必须每天运行 logrotate,这样如果日志足够大,日志文件就会被轮换和压缩,并且相应的 Rails 应用程序将会重新启动。
两个或多个 Rails 应用程序几乎同时重新启动的情况很容易发生,但不能保证它们会以正确的顺序重新启动,以便它们在 Docker 内部网络中重新获得其以前的 IP 地址。这解释了发生了什么:Rails 应用程序偶尔会交换 IP 地址,但 nginx 对此并不知情,因此它继续将请求转发到旧 IP 地址,同时正确地提供静态资产。我通过停止和重新启动两个应用程序来模拟这种情况,并且能够准确重现该问题。此外,我还添加了一个每五分钟监控一次网站的脚本,日志显示问题似乎总是发生在同一时间左右,这与 logrotate 是触发器一致。
Ángel 指出,无需重新启动 nginx,只需让它重新加载配置即可,这样可以减少网站访问者的中断。他还指出,可以强制 nginx 每隔几分钟而不是只查找一次名称(请参阅https://forum.nginx.org/read.php?2,215830,215832#msg-215832)。
为了解决这个问题,至少目前我已经将 Docker 配置为向所有容器分配一个固定 IP 地址。这样,nginx 可以继续只解析一次名称。然而,这会带来一些后果,因为现在我无法使用与正在运行的 Rails 应用程序相同的 docker-compose.yml 文件运行诸如 db:migrate 或 assets:precompile 之类的命令(您会收到“地址正在使用中”错误);目前,我正在使用docker compose exec
而不是run
,但这似乎对命令有影响docker compose restart
(运行 后您会在几秒钟内收到“地址正在使用中”的错误exec
)。如果这成为一个问题,我可能会恢复到非固定 IP 地址并让 nginx 在 logrotate 过程中重新加载其配置。