504
代理等某台 upstream 等到沒耐心了,於是替它回話——所以真正指認兇手的不是狀態碼,而是耗掉的時間,那永遠是某個人設定的逾時。
同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。
為什麼會看到它
請求不會失敗,它會卡住——一個轉大約一分鐘的載入圈,然後變成一頁沒有人設計過的錯誤頁。它打中的是某一個端點,通常是報表、匯出、全站搜尋或上傳,而且要的東西越大就越嚴重——那正是「有計時器介入」而不是「有 bug」的破綻。如果你自己設了 `AbortController` 的期限,你可能根本看不到那個 504:你的 abort 先觸發,於是這次失敗被報告成網路錯誤,把調查引到錯的地方去。
該怎麼做
先量,再推理。網路面板的時間欄,或 `performance.getEntriesByName(url)`,會給你耗掉的時間;而一個整數——60 秒、30 秒、120 秒——不是巧合,那就是答案。接著刻意把用戶端逾時設得比伺服器的短一點,這樣使用者看到的訊息由你控制,而不是繼承一頁代理的 HTML;並且把耗時跟失敗一起記下來,讓後端拿到的是那個計時器,不是一張截圖。不要重試 POST:代理不再聽之後,upstream 有可能還是把事情做完了,所以一次被重試的匯出會變成兩份匯出,一次被重試的付款可能變成兩筆付款。如果這個端點真的需要好幾分鐘,那它需要的是另一種形狀——請求立刻回一個 job id,用戶端再去輪詢——而不管你在哪一層調逾時,都不可能讓一個長請求在你和伺服器之間每一台代理、每一個行動網路上都可靠。
為什麼會看到它
你的某一個計時器走完了,而 nginx 會把是哪一個寫下來。`upstream timed out (110: Connection timed out) while reading response header from upstream` 是 `proxy_read_timeout`——連線建立了、請求送出去了,而 upstream 從頭到尾沒開始回答。同樣的訊息如果結尾是 `while connecting to upstream`,那是 `proxy_connect_timeout`,是網路或監聽的問題、不是應用程式慢;結尾是 `while sending request to upstream` 則是 `proxy_send_timeout`。三個預設都是 60 秒。那一行紀錄也會寫出 upstream 的位址,所以一組裡面只有一台會逾時,那是一台生病的執行個體,不是一個慢的端點。
該怎麼做
先讀錯誤紀錄那一行:它指名了計時器和 upstream,光這兩件事就能在你打開任何應用程式碼之前砍掉大部分的搜尋空間。接著去拿 upstream 自己的視角——如果應用程式有記請求耗時,那個產生 504 的請求通常還在裡面,而且記著一個比代理逾時更長的耗時,這就證明了那件事是在代理離開之後才做完的,也直接告訴你它真正要花多久。如果那個請求在應用程式紀錄裡從頭到尾沒出現,那就代表沒有東西慢:請求被排在應用程式前面等,那是工作行程池飽和、是容量問題,不是一句要優化的查詢。在你調大任何逾時之前,先看 `proxy_next_upstream`:它預設是 `error timeout`,所以一台慢的 upstream 會把用戶端等待的實際時間乘上這一組裡的伺服器數量,而 `proxy_next_upstream_timeout` 和 `proxy_next_upstream_tries` 是把它框住的兩個上限。調大逾時是最後手段,而且那是把失敗往後挪、不是把它修好——瀏覽器到你應用程式之間的每一層都有自己的計時器,而最短的那個永遠贏。
為什麼會看到它
這一頁載了大約一分鐘,然後放棄了。這代表網站是活著的,只是其中某一部分花的時間遠超過系統願意等的長度——常見的是搜尋、報表、大型下載或上傳。可能是網站正忙,也可能就是你要的那個東西本身很大。這不是你的連線問題:連線慢會讓頁面慢慢地一點一點載出來,不會在一個整數上直接停死。
該怎麼做
等一分鐘再試一次;如果那是搜尋或報表,就要少一點——縮短日期範圍、減少筆數、一次一個檔案而不是整個資料夾——因為你要的量,往往就是「拿到答案」和「逾時」之間的全部差別。如果你剛才在上傳或送出東西,重做之前先確認:逾時會把答案藏起來,但不會把動作取消,所以就算你的瀏覽器沒等到回音,那份上傳有可能在網站那邊已經完成了。先去看帳號頁、訂單紀錄或收件匣。如果同一個請求一直逾時,那值得回報,並附上確切時間和你要的是什麼,因為維運的人可以靠這兩個細節在紀錄裡找到那個慢請求。
對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。
curl -sS -o /dev/null -D - --max-time 180 -w '\n%{http_code} connect=%{time_connect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' https://example.com/slow`--max-time 180` 設定的是 curl 對整個操作的上限,而且它必須明顯比伺服器的計時器長,否則會是 curl 先中斷,那你除了「你比較沒耐心」之外什麼都學不到。三個時間就是診斷。`%{time_connect}` 是 TCP 連線完成的時刻,`%{time_starttransfer}` 是第一個位元組抵達的時刻,`%{time_total}` 是結束;一個 504 會有很快的 connect,而 total 會落在一個整數上。把那個數字對到某個有文件的預設值,你就知道是哪一層了:大約 60 秒是 nginx 的 `proxy_read_timeout` 或 Application Load Balancer 的閒置逾時,兩者預設都是 60;大約 125 秒是 Cloudflare 的讀取逾時——而那一個回報的是 524、不是 504,所以一個落在 125 秒的 504 不是 Cloudflare 的。total 明顯比任何預設值短,代表有人刻意設過那個計時器,於是範圍就縮到某個設定檔、而不是某個產品。接著把層次拆開:`--resolve example.com:443:203.0.113.10` 會直接連到來源機的位址,同時照樣送出原本的主機名稱和 SNI,所以如果來源機 90 秒回得出來、邊緣節點卻在 60 秒就放棄,那就是這件事真的慢,計時器只是報信的。跑這行指令的同時盯著來源機自己的存取紀錄——一個出現在那裡、而且耗時比逾時還長的請求,就是「代理不再等之後那件事還是做完了」的證據,而這正是在這裡重試寫入很危險的原因。
RFC 9110 §15.6.5 把 504 定義成:一台以閘道或代理身分運作的伺服器,沒有「及時」從它為了完成請求而必須存取的那台上游伺服器收到回應時,回出來的狀態。跟 §15.6.3(把 502 留給不合法的回應)擺在一起讀,「及時」這一個詞就是全部的分野,而且它附帶一支碼表:502 在毫秒內回來,因為有東西拒絕或斷掉了;504 則在一個整數上回來,因為有個計時器走完了。那些整數是有文件的,而且能指認出是哪一層。nginx 的 `proxy_read_timeout`、`proxy_connect_timeout` 和 `proxy_send_timeout` 預設都是 60 秒,而且它註明連線逾時通常無法超過 75 秒,因為作業系統自己的連線嘗試就在那裡結束。AWS 的文件寫著 Application Load Balancer 的連線閒置逾時預設是 60 秒、可設定範圍 1 到 4000,並建議在每個閒置週期走完之前至少送一個位元組,免得長時間的上傳被切斷。Cloudflare 自己的讀取逾時是 125 秒,而且它產生的是 524、不是 504。所以一個大約落在一分鐘的 504,是 nginx 或 ALB 的預設計時器;一個落在其他整數上的 504,是某個人刻意設的計時器。有兩個性質會改變你的下一步。504 不是 RFC 9110 §15.1 定義為可依啟發式規則快取的狀態碼之一,所以它是每個請求重新產生的,不會被一個沒被交代過的中介裝置存起來。另外,跟 502 不一樣,504 完全沒有攜帶「那件事有沒有做完」的證據:代理放棄的時候,upstream 手上還握著那條連線,所以它很可能一秒之後就完成了那個請求、把被要求的東西全部寫進去了。這讓「盲目重試一個非冪等的請求」成為這裡代價最高的錯誤。nginx 把同樣的謹慎寫進了設定——`proxy_next_upstream` 預設是 `error timeout`,所以逾時的確會換下一台伺服器;但非冪等方法的請求一旦送出去就不會被轉給下一台,除非你明寫 `non_idempotent`,那是 nginx 1.9.13 開始才有的參數。
| 容易搞混的 | 怎麼分 |
|---|---|
| 502 | 兩者都是中介裝置在報告 upstream 的狀況,而 RFC 9110 把界線畫得很精準:§15.6.3 是不合法的回應,§15.6.5 是沒有及時到的回應。碼表比字面更快給你答案。502 在毫秒內回來,因為連線被拒或被切斷;504 則是照計時器回。後果比標籤更重要:502 之後,那個請求幾乎可以確定什麼都沒做;504 之後,upstream 有可能已經把它做完了。 |
| 524 | 524 是 Cloudflare 自己的代碼、不是 IETF 的,而且它只用在一個地方的這一種情況:Cloudflare 連到來源機了,但在它 125 秒的讀取逾時內沒有等到任何 HTTP 回應。所以一個穿過 Cloudflare 抵達的 504,是別的東西產生的——你自己的代理,或某台以閘道身分運作的來源機——只是被轉過來。如果你想調大逾時,要知道只有 Enterprise 方案的區域才能改 Cloudflare 的,而且最多到 6,000 秒。 |
| 503 | 503 是事先做出的拒絕,504 是等到沒得等了。RFC 9110 §15.6.4 允許 503 帶 `Retry-After`,因為伺服器大概知道自己什麼時候會回來;504 沒有任何對應的東西,因為代理根本不知道 upstream 在幹嘛。如果你的降載機制產生的是 504,那它就不是在降載——它是把請求排隊排到被計時器殺掉,而 upstream 那份工還是照做了。 |
nginx | 錯誤紀錄會指名計時器,而那三則訊息值得分清楚。`while connecting to upstream` 是 `proxy_connect_timeout`,指向網路或沒有東西在監聽;`while reading response header from upstream` 是 `proxy_read_timeout`,指向應用程式慢;`while sending request to upstream` 是 `proxy_send_timeout`,通常代表一個大的請求 body 對上一個卡住的讀取端。三個預設都是 60 秒,而連線逾時通常無法超過 75 秒。 |
ELB 504 | AWS 的文件寫著 Application Load Balancer 的連線閒置逾時預設 60 秒、可調整範圍 1 到 4000,而目標在那之內沒有回應就回 504。他們自己對長時間操作的建議,是在每個閒置週期走完之前至少送一個位元組——這就是為什麼串流的回應活得下來、安靜的回應會死。目標端的 keep-alive 也要看:AWS 註明它不應該比負載平衡器的閒置逾時還短。 |
這些全都在你的瀏覽器裡跑,不會上傳任何東西。
查 504 的時候,通常也會順手看一下這幾個。
去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 server、via 和 cf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。
不重要,而且絕對不要拿它來做判斷。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,會把值班的人送去堆疊裡錯的那一半。
504 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。