ByteScope

503

HTTP 503 Service Unavailable 怎麼修

5xx 伺服器錯誤IETF 標準RFC 9110

有某個東西決定現在不服務你,而且是刻意的——維護頁、限流器,或者一台手上沒有任何健康後端可以轉的負載平衡器——這讓 503 成為唯一附帶到期日的 5xx。

為什麼會看到 503,以及該怎麼處理

同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。

用 curl 重現 503

對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。

curl
curl -sS -o /dev/null -D - -w '\n%{http_code} retry-after=%header{retry-after} in %{time_total}s\n' https://example.com/api/

`-D -` 會印出回應標頭,而 `%header{retry-after}` 把那一個欄位拉進總結那一行;它在目前的 curl 裡是有文件的 `--write-out` 變數,而在舊到沒有它的版本上,`-D -` 印出來的那份標頭一樣看得到。輸出告訴你的事分三部分。`retry-after` 有值,代表這次拒絕是刻意的、而且有人選了一個時長,這是把「排定維護或限流器」跟「一台後面根本沒有健康目標的負載平衡器」分開最快的方法;它是空的,代表拒絕你的那個東西無視了 RFC 9110 §15.6.4 的建議,那你挑的任何等待時間都是猜的。`server:` 標頭指名寫出這一頁的那一層,而那就是你該去讀設定的那一層。耗時則把「拒絕」和「失敗」分開:限流器或維護規則會在毫秒內回答,因為它根本沒去連任何 upstream。想知道是不是限流器造成的,就用比設定速率更快的頻率重複請求——`for i in $(seq 1 50); do curl -sS -o /dev/null -w '%{http_code} ' https://example.com/api/; done`——然後看那一排狀態碼在中途從 200 翻成 503,那是限流器獨有的簽名,別的東西不會這樣。加上 `--resolve example.com:443:203.0.113.10` 會直接連到來源機的位址、同時照樣送出原本的主機名稱和 SNI,所以 503 消失就是邊緣節點來的,503 還在就是來源機來的。

503 實際上在講什麼

RFC 9110 §15.6.4 把 503 定義成:伺服器因為暫時的過載或排定的維護,目前無法處理這個請求,而這個狀況在一段延遲之後很可能會緩解;並說伺服器「可以」送一個 `Retry-After` 標頭欄位,建議用戶端該等多久。那個標頭正是 503 在結構上和其他所有 5xx 不同的地方:§10.2.3 把 `Retry-After` 定義成一個秒數或一個 HTTP-date,而跟 503 一起送的時候,它講的是這個服務預期會不可用多久。這裡的「可以」很有份量:一個沒有 `Retry-After` 的 503 完全符合規格,所以那個標頭不在,除了「拒絕你的那個東西不願意說什麼時候回來」之外,什麼都告訴不了你——而一台連連線都接不下來的伺服器產生的是連線錯誤、不是 503,所以儀表板上沒有 503,並不能證明沒有東西過載。看著一個 503 的時候,還有兩個性質很重要。503 不在 RFC 9110 §15.1 那份可依啟發式規則快取的代碼裡——那份是 200、203、204、206、300、301、308、404、405、410、414 和 501——所以沒人叫它存它就不會存,而一個活得比原因還久的 503,是被重新產生出來的,不是被重播的。另外,503 完全沒有在講你這個請求本身——它是在陳述這個服務的狀態,而這正是它和 429 之間那條界線。它真正的來源很少是應用程式碼。nginx 的限流器預設就是它:`limit_req_status` 和 `limit_conn_status` 的文件寫的預設值都是 503,所以一個因為超過設定速率而被拒的請求,在沒人改過那個指令的情況下,會以 503 的樣子抵達。Apache 的 `mod_proxy` 文件寫著,後端失敗時會把連線池的 worker 推進錯誤狀態,之後 httpd「在逾時到期之前不會再轉任何請求給那台伺服器」——那個 `retry` 參數預設是 60 秒——所以那裡的拒絕,可以活得比修好它的那次重啟還久。而 AWS 的文件寫著,Application Load Balancer 持續回 HTTP 503 代表準備好接收請求的目標數量不足,那是健康檢查的結果,不是應用程式的故障。

503 最常被搞混的幾個代碼

容易搞混的怎麼分
429429(RFC 6585 §4)說的是「你」送太多請求了;503 說的是這個服務對所有人都不可用。兩者會糊在一起是因為一個預設值:nginx 的文件把 `limit_req_status` 和 `limit_conn_status` 都寫成 503,所以網際網路上大多數的限流都被報告成當機。如果限流器是你在管的,把它設成 429——用戶端就能只對那一個呼叫方退避,而不是假設整個服務掛了,你的儀表板也不會再把節流算成停機。
502同一個底層事件,會因為你跑的是哪一台代理而產生不同的代碼,這就是為什麼光看數字診斷得很差。nginx 對一個拿不到合法回應的後端報 502;而 Apache 的 `mod_proxy` 會把失敗的 worker 拉出輪替,在 `retry` 窗(預設 60 秒)內拒絕再轉給它,所以用戶端看到的是那次拒絕。可以這樣讀:502 代表有東西沒答出話,503 代表有東西不肯去問。
500500 是一個非預期的狀況,503 是一個預期中的。RFC 9110 給了 503 一條 `Retry-After` 的管道,卻什麼都沒給 500,因為對一個未處理的例外,實在沒辦法講出「它什麼時候會停」這種話。一個靠丟例外來降載的服務,是把一個計畫中的決定報告成一次當機,而所有以 503 為依據去退避的用戶端,都會立刻重試它。
nginx在 nginx 上,503 幾乎從來不是應用程式的。`limit_req` 和 `limit_conn` 預設都是它,維護區塊通常就是一句 `return 503`,而錯誤紀錄會用 `limit_req_log_level` 設定的層級記下限流拒絕,延遲則記在比拒絕低一級的地方。拿限流 zone 的名稱去 grep 那份紀錄,一行就知道你面對的是限流器,還是某個穿著同樣三位數字的別的東西。
Retry-AfterRFC 9110 §10.2.3 把這個欄位定義成兩種形式——一個非負的秒數,或一個 HTTP-date——而一個只解析其中一種的用戶端,會安靜地把世界上一半的伺服器處理錯。跟 503 一起送時,它講的是這個服務預期會不可用多久;跟 3xx 一起送時意思不一樣,那是「發出被轉址的那個請求之前的最短等待時間」。兩種形狀都要解析,而標頭不在時要當成「不知道」,絕對不要當成「立刻重試」。
原因片語
Service Unavailable
類別
5xx 伺服器錯誤
定義出處
RFC 9110
標準
IETF 標準

站上可以搭配的工具

這些全都在你的瀏覽器裡跑,不會上傳任何東西。

相關狀態碼

查 503 的時候,通常也會順手看一下這幾個。

常見問題

怎麼知道這個狀態碼是哪一台伺服器回的?

去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 serverviacf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。

數字後面那句原因片語(像 Not Found)重要嗎?

不重要,而且絕對不要拿它來做判斷。RFC 9112 要求用戶端忽略原因片語,伺服器可以隨意改它,而 HTTP/2 和 HTTP/3 根本沒有這個欄位——所以在 HTTP/1.1 上是 404 Not Found,到了 HTTP/2 就只剩一個 404。這個網站會寫出登記在案的片語,是因為那是大家會拿去搜尋、也是會出現在 HTTP/1.1 紀錄裡的字串,不是因為有任何軟體依賴它。

這個代碼失敗的請求該不該重試?

5xx 和 429 可以重試,4xx 不要——下一次送過去也不會有任何不同。如果回應帶了 Retry-After 就照它做,RFC 9110 定義它就是為了這件事,值可以是秒數也可以是一個時間點。沒有的話,就用帶抖動的指數退避加上一個硬上限,而且只對冪等的方法這樣做:一個被重試的 POST 可能會刷兩次卡。對一台已經在失敗的伺服器丟出重試風暴,是短暫事故變成長時間事故最常見的那條路。

我可以讓自己的伺服器改回別的狀態碼嗎?

可以,唯一要守的規則是那個代碼必須是真的。所有會自動讀你回應的東西——搜尋引擎、監控、快取、用戶端的重試邏輯——都只看那個數字,從來不看那一頁寫了什麼。用 200 回一頁錯誤訊息,會把故障從你自己的警報裡藏起來;用 200 回一頁不存在的內容,會讓錯誤被當成內容收進索引;而對一個只是格式不對的請求回 500,會把值班的人送去堆疊裡錯的那一半。

503 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。