題幹與適用場景
一個介面在 Node 中執行多步外部呼叫,正常耗時 5 到 35 秒。Node 的請求超時設為 60 秒,NGINX 的 proxyreadtimeout 保持預設值。線上偶發 502,使用者認為任務失敗;應用程式日誌卻顯示業務寫入稍後完成。壓測還發現並發升高時連線被長時間占用。
請畫出用戶端、NGINX、Node、下游依賴與任務儲存的時間線,說明 502 的來源、應用程式為何仍完成工作,以及如何選擇同步回應、串流保活或非同步佇列。時長是面試假設,核心考察跨層超時語意、取消與副作用邊界、資源隔離和可驗證修復,因此歸入 backend。
面試官考察點
高品質回答會先區分「連線關閉」「應用程式截止時間」和「業務任務完成」三個事件,而非看到 502 就重試。還要知道 NGINX 的讀取超時是兩次上游讀取之間的閒置時間,不等於完整回應總時長;AbortSignal.timeout() 只會向監聽該 signal 的操作發出取消通知。
面試官也會看你是否盤點所有代理、閘道、負載平衡器、SDK 與用戶端的預設值,是否把 HTTP 請求與長任務解耦,以及是否能證明修復沒有造成重複寫入、連線洩漏或更大的重試流量。
回答前需要釐清的問題
- 502 是 NGINX 產生還是 Node 回傳後被改寫?對照回應標頭、代理錯誤日誌與上游存取日誌。
- 超時時業務副作用是否已提交?結果未知時不能直接重送。
- 回應是否必須在本次請求返回?若只需最終結果,同步鏈路沒有必要承載 35 秒任務。
- 上游是否持續傳送回應位元組?有資料流時,讀取超時與總截止時間意義不同。
- 取消訊號是否傳到資料庫、HTTP 用戶端與外部 SDK?關閉瀏覽器連線不會自動停止所有工作。
- 並發、連線池、事件迴圈和佇列指標怎樣變化?先確認資源瓶頸再改數字。
30 秒回答框架
「我先用同一個 trace ID 對齊用戶端、NGINX、Node 和下游日誌,確認 502 由哪一層在何時產生。NGINX 的 proxyreadtimeout 只限制連續讀取之間的閒置間隔,Node 的 60 秒也不能保證業務任務在用戶端斷線後停止。若必須同步返回,就建立端到端截止時間,讓代理、應用程式與下游保留收尾餘量;若任務超過互動預算,就持久化任務、立即返回 ID,由 worker 執行並提供狀態查詢。修復後用故障注入驗證沒有重複副作用、連線占用和重試放大。」
分步驟深入解答
先畫時間線:用戶端發起請求,NGINX 轉送,Node 開始工作;若 Node 在一段時間內沒有傳送回應位元組,NGINX 的讀取計時器到期並關閉上游連線,代理可能返回 502 或 504。Node 不一定同時收到取消,也可能已提交寫入,之後繼續計算並記錄完成。因此使用者看到失敗與業務完成可以同時成立。
用 trace ID、請求 ID 和任務 ID 串起 NGINX access/error log、Node 請求開始/結束/abort、下游呼叫與資料庫提交。比較 upstreamresponsetime、handler duration、用戶端收到回應時間與副作用提交時間,才能判斷是代理閒置超時、應用程式截止時間、下游超時還是用戶端主動斷開。
NGINX proxyreadtimeout 預設為 60 秒,含義是兩次讀取之間允許的最長閒置間隔;持續有位元組時計時會重新開始。調大它只能改變代理容忍度,不能阻止連線占用,也不能修復用戶端更短的截止時間。若改設定,應把代理上限、應用截止時間和用戶端預算寫成表格,並留出回應、清理和網路抖動餘量。
應用程式可用 AbortSignal.timeout() 建立截止訊號,傳給支援取消的 fetch、資料庫或 SDK。取消是合作式的,未監聽 signal 的函式仍可能繼續;已提交的資料庫交易也不能靠 abort 變成未發生。每個副作用要有冪等鍵、狀態機或補償路徑,記錄 accepted、running、succeeded、failed、unknown 等可解釋狀態。
若任務 p99 明顯超過互動預算,改用非同步邊界:API 驗證輸入並寫入任務記錄或佇列,回傳 202 與任務 ID;worker 領取任務、設定租約、執行重試並保存結果;用戶端輪詢或訂閱狀態。任務寫入必須冪等,worker 重啟可恢復,重複投遞不會重複扣款或建立資源。
若任務只略超時且結果能安全分塊,可以發送心跳或分塊回應,但要確認所有中間層允許長連線、用戶端能處理增量資料,且仍有總截止時間、最大位元組數和取消處理。不能用心跳掩蓋無界工作。
修復驗證包括:將 NGINX 讀取超時設成已知值,注入超過閒置時間的下游延遲;用戶端在不同階段斷開;並發壓測檢查事件迴圈延遲、連線池、佇列深度和 worker 吞吐。斷言每個邏輯任務最多一次成功副作用、重試不越過截止時間,且日誌能以 trace ID 對齊。
高品質示範回答
「我不會先把 30 秒改成 5 分鐘。第一步用 trace ID 對齊 NGINX、Node、下游和資料庫時間,確認 502 是代理等待上游位元組時產生,還是 Node 自己返回。NGINX 的 proxyreadtimeout 是兩次讀取間的閒置上限;Node 的請求超時與業務任務完成也不是同一事件,所以代理關閉連線後 Node 仍可能提交副作用。
如果必須同步返回,我會定義端到端截止時間,向下游傳剩餘預算,使用 AbortSignal.timeout() 並確認 SDK 真的監聽取消;結果未知的寫入使用冪等鍵和狀態查詢。若任務 p99 遠超互動預算,API 持久化任務後立即回傳 202 和 ID,由 worker 執行、重試與保存狀態。驗證時注入代理閒置、用戶端斷線、worker 重啟和重複投遞,檢查沒有重複副作用、連線池沒有被長任務占滿。」
常見錯誤
- 只調大 NGINX 超時 → 長連線與並發占用持續增長 → 先定義業務截止時間與非同步邊界。
- 看到 502 就重試 → 原任務可能已提交副作用 → 先查狀態並使用冪等鍵。
- 把
proxyreadtimeout當總回應時長 → 持續小流量會延長連線壽命 → 另設總截止時間與最大回應時長。 - 關閉用戶端連線就假設服務端停止 → 許多函式未監聽取消 → 逐層驗證 abort、連線與交易行為。
- 用心跳掩蓋無界任務 → 資源持續洩漏 → 限制總時長、位元組數與並發。
- 同步 handler 執行 35 秒任務 → HTTP 連線承載全部工作 → 持久化任務並由 worker 執行。
- 只看 Node 日誌 → 錯過代理錯誤和時間 → 同時採集代理 access/error 和上游耗時。
- 非同步化卻沒有冪等狀態 → 重複投遞造成重複扣款 → 用唯一任務鍵和狀態機約束寫入。
追問及應對
追問一:把 proxyreadtimeout 改成 60 秒就夠了?
不夠。它只控制兩次讀取間的閒置時間,其他層可能更早截止;長連線也會繼續占用資源。應先定義互動預算,再統一配置各層。
追問二:用戶端斷線後 Node 如何停止?
監聽請求關閉事件並觸發 AbortController,把 signal 傳給支援取消的操作;不支援取消的呼叫需隔離與丟棄結果。已提交副作用仍需冪等與對帳。
追問三:什麼時候用串流回應?
結果可安全分塊、所有代理支援長連線且總時長可控時使用。仍要有心跳間隔、總截止時間、輸出上限和取消傳播。
追問四:非同步佇列如何避免重複執行?
以業務請求產生唯一任務鍵,資料庫唯一約束只建立一條任務;worker 以租約領取,完成寫入帶條件更新,外部副作用沿用同一冪等鍵。
追問五:如何證明修復生效?
預發布注入代理閒置、下游慢回應、用戶端斷線、網路重置和 worker 重啟,記錄各層時間戳,檢查錯誤來源、連線占用、重複副作用和佇列延遲。
追問六:為什麼應用日誌成功,使用者仍收到 502?
代理可能已先關閉連線,或回應在用戶端鏈路遺失;完成日誌只代表程式結束,不代表回應交付。要對齊代理狀態、Node 寫回應結果與用戶端觀測。
追問七:非同步化會犧牲什麼?
增加狀態儲存、worker、重試、死信與最終一致性;換來 HTTP 連線壽命與任務時間解耦,以及獨立控制並發和重放失敗。