我有一台伺服器託管多個 Rails 網站。所有網站均由唯一的主機名稱識別(所有網域名稱對應到相同 IP 位址)。 Rails 和 nginx 都在 Docker 容器中運作。我使用的是從官方 Docker 映像建置的 Docker 映像中執行的 nginx 1.23.1(我只新增了用於 TLS 憑證處理的 certbot)。
最近新增另一個網站後,一件非常奇怪的事情開始發生。啟動 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 -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 應用程式或多或少同時重新啟動的情況,但不能保證它們會以精確正確的順序重新啟動,以便將先前的 IP 位址還原到 Docker 內部網路。這解釋了發生的事情:每隔一段時間,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 過程中重新載入其配置。