MOOSSin
選單

全部文章 · 開源工具

nginx:當靜態檔伺服器,也當幾個後端前面的閘道

nginx 從 2004 年到現在,設定語法幾乎沒變,錯誤訊息會告訴你第幾行,重新載入不斷線。這篇談把它放在容器裡的做法、用路徑前綴分流幾個後端時的兩個細節,以及 add_header 一個很多人踩過的繼承規則。從一位很無聊的櫃台人員說起。

如果把一個網站想成一棟大樓,nginx 就是一樓的櫃台。訪客進門,櫃台問你要找誰,然後指路:找靜態檔的往左,找後端 A 的搭電梯上三樓,找後端 B 的上五樓。它自己不做任何有趣的事,它只負責指路,而且指了二十年沒有指錯過幾次。

nginx 由 Igor Sysoev 在 2004 年發表,BSD-2-Clause 授權,現在是全世界最常見的網頁伺服器之一。我用它做兩件事:提供建置好的靜態網站,以及站在幾個後端應用程式前面依路徑分流。兩件事它都做得很無聊,而無聊正是我想要的。

為什麼我喜歡無聊的東西

做網站的人都經歷過那種階段:什麼都想用最新的、最酷的、GitHub 上星星最多的。然後某天半夜那個很酷的東西出了一個很酷的錯誤,文件還沒寫到那一頁,你一個人對著 issue 列表發呆。

nginx 是反面教材。它的設定語法很多年沒有大改,你十年前學的東西今天還能用。錯誤訊息會告訴你第幾行錯了,不是「發生了未預期的錯誤」。改完設定 nginx -s reload,舊連線走完、新連線用新設定,中間不會斷線。在一個所有東西都在動的技術堆疊裡,有一塊東西的行為完全可以預測,對一個人維護的站很重要。它像那種每天準時開門、菜單十年沒換、但你從來沒吃壞肚子的早餐店。

放進容器,設定烤進映像

官方映像用 digest 釘死,容器以非 root 執行、根檔案系統唯讀、能力全部丟掉。設定檔不是掛載進去的,是建置映像時複製進去的,這樣映像本身就能回答「它的設定是什麼」,不會出現「映像是對的,但掛載進去的設定檔被誰改過」這種偵探劇。

網站內容則以唯讀掛載提供,換版就是換掛載目標,映像不用重建。發布前可以在一個隔離的容器裡把每條路徑打一遍,比對狀態碼和標頭,通過才切換。這一步有點像上菜前先自己嘗一口,很多餐廳不做,做的都活得比較久。

用路徑前綴分流幾個後端

幾個獨立的應用程式要併到同一個主機名底下,各自掛在 /a/、/b/ 這樣的前綴後面,nginx 依前綴 proxy_pass 到對應的後端。回到大樓比喻,就是原本三間店各有各的門牌,現在併成一棟樓,共用一個地址,靠樓層區分。有兩個細節要決定。

前綴要不要剝掉。proxy_pass http://backend/; 尾端帶斜線會把前綴剝掉再轉發,不帶就原樣送過去。這一撇斜線的差別,我保證你會忘記,然後在某個下午重新學一次。後端如果是 uvicorn 這類用 --root-path 得知前綴的 ASGI 伺服器,它會把前綴接回請求路徑,所以閘道要剝;後端如果是帶著前綴建置的靜態站,閘道要保留。同一張路由表裡兩種做法並存很正常,寫清楚就好,最好在設定檔裡留一行註解給三個月後的自己。

代理標頭要不要加。如果後端刻意不信任 X-Forwarded-*,閘道就不要加,並用 proxy_set_header Host $host 保留原始的 Host。少一層信任關係,就少一種偽造標頭能造成的問題。櫃台不需要替訪客編故事,把訪客原本說的話轉達上去就好。

add_header 不會繼承

這是 nginx 最常被踩的規則之一,踩過的人都會有一種「這不合理吧」的表情。add_header 在某個區塊裡只要出現一次,外層區塊的所有 add_header 就全部不套用,是取代,不是疊加。你在 server 層設了五個安全標頭,然後在某個 location 裡為了加一個快取標頭寫了一行 add_header,恭喜,那個 location 的五個安全標頭全部消失了。

閘道如果自己加了任何安全標頭,很容易把後端精心設定的 CSP 蓋掉。我的做法是閘道一個安全標頭都不宣告,全部交給各個後端。櫃台不決定每一層樓的門禁規則,每一層自己管自己的門。

小劇場:這不是很老的技術嗎

「nginx 不是很老了嗎?現在不是都用什麼新的東西?」

「老是優點。老代表每一個坑都有人踩過、都有人寫過怎麼爬出來。」

「可是新的東西功能比較多。」

「你需要的功能是什麼?」

「就把請求送到對的地方。」

「那你需要的是一個不會出錯的櫃台,不是一個會跳舞的櫃台。」

「跳舞的櫃台聽起來也不錯。」

「半夜三點它跳給你看的時候你就不會這樣想了。」

錯誤頁與日誌

上游不可達時 nginx 預設回自己的錯誤頁,就是那個大家都看過的白底黑字「502 Bad Gateway」。用 error_page 指到一個你自己的頁面比較體面,至少讓訪客知道他們沒有走錯棟。存取日誌送到標準輸出,容器的日誌系統會收,觀察期用它抓 404 和指向舊網址的引用很好用,你會發現網路上有多少東西還記得你三年前的網址。

沒選的替代品

Caddy 自動處理憑證很吸引人,設定檔也比 nginx 短很多,我認真考慮過。但如果流量是經由 Cloudflare 隧道進來、憑證在邊緣終止,這個優勢就用不到,多的是一個要管理的行程。把靜態站整個交給 Cloudflare Pages 或物件儲存也很乾淨,代價是發布模型從「本機不可變的 release」變成「上傳到別人那裡」,回復和驗證的方式都要重寫。這兩個我都沒選,理由到今天還成立。

選工具有時候不是選最好的,是選那個在你的情境裡最沒有意外的。對我來說那就是一樓那位很無聊、但從來不會指錯路的櫃台。

一點關於 nginx 的身世

Igor Sysoev 在 2002 年開始寫 nginx,那時候他在俄羅斯一間入口網站公司上班,要解決的問題有一個很有名的名字叫 C10K:一台伺服器怎麼同時撐住一萬個連線。當時主流的 Apache 每個連線開一個行程或執行緒,一萬個連線就是一萬個,記憶體很快就沒了。nginx 用事件驅動的模型,少數幾個行程輪流照顧所有連線,誰有事就處理誰,沒事的不佔資源。這就是為什麼它從一開始就是「櫃台」的氣質:一個人服務一整個大廳,不需要每個訪客配一個接待員。

2004 年正式釋出,名字唸作 engine x。2011 年成立公司,2019 年被 F5 收購。開源的部分一直都在,商業版另外賣,這個模式維持了十幾年。

兩個我每天都在用的指令:nginx -t 檢查設定檔有沒有語法錯誤,會告訴你第幾行,改完設定一定先跑它;nginx -s reload 平滑重載,舊的連線走完、新的連線用新設定,服務不中斷。這兩個加起來,讓「改設定」這件事從一場賭注變成一個日常動作。

常見的三個誤會

第一個誤會是「nginx 只能做反向代理」。它端靜態檔的能力是頂級的,很多大流量的靜態站就是一台 nginx 加一顆硬碟。反向代理只是它眾多角色裡最常被提到的一個。

第二個誤會是「location 的比對規則很直覺」。不直覺。精確比對、前綴比對、正規表示式比對,優先順序不是照你寫的順序,是照一套規則。第一次寫兩個 location 互相打架的時候,你會花一個下午讀那一頁文件,然後從此小心翼翼。

第三個誤會是「設定檔改了就生效」。要 reload。這聽起來很基本,但每個人都會有一次改了設定、測了半天、懷疑人生,最後發現沒有 reload 的經驗。我有過,而且不只一次。

一個週末可以試的小練習

拿官方的 nginx 容器映像,寫一個最小的設定檔掛進去,裡面一個 server 區塊,server 層加 add_header X-One 1;,再開一個 location /test/,裡面加 add_header X-Two 2;。啟動,用 curl -I 分別打根路徑和 /test/。你會看到根路徑有 X-One,/test/ 只有 X-Two,X-One 不見了。

這就是「add_header 不會繼承」的現場版。親眼看一次,比讀十遍文件記得牢。然後把 /test/ 裡的 add_header X-One 1; 也補上,再打一次,兩個都有了。恭喜,你剛學會了 nginx 最常被踩的坑,用五分鐘而不是一個下午。

後記

我對「無聊的工具」有一種偏愛,這篇大概看得出來。年輕的時候會覺得無聊等於不夠好,後來慢慢覺得,在半夜三點還能讓你安心睡覺的東西,無聊是它最好的品質。nginx 就是那種你裝好之後可以忘記它存在的東西,而在基礎設施裡,能被忘記是最高的讚美。

官方網站:nginx.org

閱讀偏好

返回全部文章作者與內容更正

留言(0)

還沒有留言。讀完有想法,歡迎留下第一則。

留言經站長審核後才會顯示。暱稱和留言會公開,請不要寫下電話、地址等個人資料;資料怎麼處理,請見隱私權政策。