題幹與適用情境
一個 B2B 分析平台需要提供報表匯出 API。單次匯出執行 5 分鐘到 3 小時,結果檔案最大 20 GiB;每天 09:00 附近可能集中收到 10,000 次提交。呼叫端包含瀏覽器與其他公司的服務。API gateway 會在 30 秒後中斷同步請求,呼叫端可能因為沒有收到回應而重試。
使用者需要查看排隊、執行、成功或失敗狀態,在可取消階段提出取消,並在成功後下載結果。題目中的時間、檔案大小、尖峰與逾時都是面試案例假設,不是業界門檻。工作佇列如何分片不是本題中心;核心問題是如何提供一個在重試、程序崩潰與狀態競態下仍清楚的非同步 HTTP 契約。
RFC 9110 規定,202 Accepted 只表示請求已被接受處理,處理尚未完成,最後甚至可能不執行;回應應描述目前狀態並指向狀態監視器。目前公開的資深 REST API 面試題也直接要求設計長時間操作,涵蓋狀態查詢、失敗、取消與冪等。因此本題適合後端工程師、API 平台工程師與需要設計跨服務契約的資深全端工程師,核心能力歸為 backend。
面試官考察重點
第一,候選人是否理解 202 的邊界。它不是「背景一定會成功」,也不能把尚未完成的工作包裝成 200 OK 的最終結果。好的回答會同步拒絕無效請求,成功接單後回傳穩定的操作資源,再把執行結果表達為後續狀態。
第二,能否把長時間工作建模為資源,而不只回傳佇列訊息 ID。操作資源需要租戶擁有者、請求指紋、狀態、進度、結果或錯誤、版本、建立與到期時間;狀態機必須定義允許的轉換、終止狀態與取消競態。
第三,接單與執行之間是否存在「已回傳 202,但工作永遠沒有入列」的缺口。資料庫操作記錄與待發布工作應在同一交易中落盤,再由 outbox 發布;消費者與執行階段仍要冪等,因為訊息至少一次投遞、執行者崩潰與租約接管都可能造成重複。
第四,用戶端契約能否承受真實流量。Location 告訴用戶端查詢位置,Retry-After、退避、抖動與條件式請求控制輪詢;服務間通知可使用簽章 webhook,瀏覽器即時進度可使用 SSE,但兩者都不能取代可復原查詢的操作資源。
最後是安全與驗證。難以猜測的 ID 不能取代物件層級授權,結果連結不能繞過租戶邊界;好的回答還會測試接單回應遺失、重複提交、發布失敗、執行者崩潰、取消與完成競態、輪詢尖峰與到期清理。
回答前需要釐清的問題
- 哪些項目必須同步驗證? 身分、租戶權限、請求結構、輸入是否存在與明顯超出配額,應在接單前檢查。若資料是否完整只能在數小時掃描後得知,它就是非同步執行失敗,不是接單時承諾成功。
- 重複請求代表重試,還是兩個獨立工作? 呼叫端能提供
Idempotency-Key時,同一租戶、端點、鍵與請求指紋應重播同一個操作;業務確實允許兩次相同匯出時,呼叫端必須使用不同的鍵。 - 進度能否度量? 能預先取得分片總數時可回傳完成單元與總單元;無法估計時只回傳階段與 heartbeat 時間,不能用隨意增加到 99% 的百分比製造確定性。
- 結果是哪一種資源? 小型結果可放入操作回應,大型檔案應成為獨立受保護資源。題設採用短期下載憑證,並分開操作中繼資料、結果物件與下載憑證的保留期限。
- 取消代表什麼? 只停止尚未發生的工作,還是必須撤銷已產生的副作用?若步驟不可逆,契約必須說明取消是盡力而為,並定義補償、部分結果與最終狀態。
- 呼叫端能接收哪些通知? 瀏覽器通常不能提供 callback 位址,適合輪詢或 SSE;服務間呼叫可選 webhook。網路限制與可接受延遲會改變通知方式,但不會改變操作資源是真相來源。
- 平行操作是否衝突? 同一報表設定能否平行匯出,設定刪除或更新時在途操作如何處理?答案決定要序列化、快照輸入、拒絕衝突或允許舊版本繼續執行。
- 需要保留多久? 本題假設終止狀態的操作保留 7 天、結果物件保留 24 小時、每個下載憑證有效 15 分鐘。這些是產品契約選擇,需要依稽核、成本與重新取得結果的能力調整。
30 秒回答框架
「我會把執行與 HTTP 請求拆開,但不只回傳一個 job ID。接單端點先驗證權限與可立即發現的錯誤,再用一個交易寫入 operation 與 outbox;成功後回傳 202、Location 與建議輪詢間隔。用戶端透過已授權的 operation 資源讀取穩定狀態、進度、結構化錯誤與結果連結,同一冪等鍵和相同請求會重播同一個 operation。執行者依 operation ID 冪等處理,狀態轉換用版本條件保護。輪詢採用 Retry-After、退避與抖動,服務間可加上簽章 webhook,取消則進入 cancel_requested 並處理與完成的競態。最後我會故障注入遺失回應、重複訊息、執行者崩潰、取消競態與到期清理,證明不會出現幽靈工作、重複結果或越權下載。」
分步深入解答
第一步定義兩個資源:匯出請求表達使用者要建立的結果,操作資源表達這次執行的生命週期。提交端點可為 POST /v1/report-exports,查詢端點是 GET /v1/report-operations/{operation_id}。接單回應如下:
HTTP/1.1 202 Accepted
Location: /v1/report-operations/op_7f3a
Retry-After: 5
Content-Type: application/json
{
"id": "op_7f3a",
"status": "queued",
"statusUrl": "/v1/report-operations/op_7f3a",
"cancelUrl": "/v1/report-operations/op_7f3a"
}202 只承諾「已接受處理」。請求結構錯誤、沒有權限或輸入根本不存在時,立即回傳對應的 4xx,不建立操作。必須耗時運算才能發現的業務失敗記錄在操作資源中。Location 與 Retry-After 是本 API 的用戶端契約;不能宣稱 RFC 9110 強制每個 202 都使用這兩個 header。
第二步定義操作記錄與狀態機。最小記錄包含 id、tenantid、idempotencykey、requestfingerprint、status、進度、結果參照、結構化錯誤、version、建立/更新時間與 expiresat。建議狀態轉換為:
queued -> running -> succeeded
-> failed
queued -> cancel_requested -> canceled
running -> cancel_requested -> canceled | succeeded | failed取消與完成可能同時發生,因此 cancel_requested 不是終止狀態。執行者提交結果時,使用帶 version 與允許前置狀態的條件更新;只有一個轉換會成功。若副作用已不可逆,取消最後可能成為 succeeded 或 failed,不能為了符合按鈕語意而偽造 canceled。狀態回應可採用以下結構:
{
"id": "op_7f3a",
"status": "running",
"progress": {
"completedUnits": 37,
"totalUnits": 100
},
"result": null,
"error": null,
"lastUpdatedAt": "2026-07-18T23:18:11Z",
"expiresAt": "2026-07-25T23:08:11Z"
}只有工作單元有真實分母時才回傳這組進度。執行失敗時,讀取操作本身仍可回傳 200,把失敗放在終止狀態與結構化 error 中;這表示「狀態資源讀取成功,其代表的執行失敗」。若團隊選擇讓狀態端點回傳執行對應的 4xx,也必須在所有 SDK 中統一,不能同時混用兩種語意。
第三步確保接單、重試與執行正確。以 (tenantid, route, idempotencykey) 建立唯一約束,並儲存正規化請求的指紋。同鍵同指紋回傳原操作與目前狀態;同鍵不同指紋回傳明確衝突,防止呼叫端誤把鍵重複用於另一份報表。冪等記錄至少保留到用戶端不會再重試,並與操作保留政策協調。
在同一資料庫交易中插入操作記錄與 outbox 事件,然後才回傳 202。獨立發布器將 outbox 送入佇列並可重複傳送;消費者依 operation ID 去重。每個執行階段也要有冪等寫入或 fencing token,避免工作節點完成外部寫入後崩潰,另一節點接管時產生兩份可見結果。這裡只需證明接單到佇列的邊界,不必把完整排程器設計塞進 API 回答。
第四步控制查詢與通知流量。初始回應與後續狀態回應提供合理的 Retry-After;用戶端採用指數退避、上限與抖動,服務端用 ETag 搭配條件式請求,減少未變更的回應內容。若狀態查詢超過配額,回傳限流資訊,而不是允許 10,000 個呼叫端每秒輪詢。
瀏覽器需要低延遲進度時可訂閱 SSE,斷線後仍以 operation ID 查詢;服務間呼叫可註冊簽章 webhook,投遞端重試並由接收端去重。兩種推送都可能遺失、延遲或重複,因此操作資源仍是復原與核對的真相來源。成功時可在操作內容中回傳結果連結;若選擇重新導向獨立結果資源,使用已文件化的 303 語意,並確認 SDK 不會把原 POST 重播到結果位址。
第五步處理授權、取消與保留。每次讀取、取消與取得結果都依 tenant_id、呼叫身分與操作權限進行物件層級授權;隨機 ID 只降低列舉便利,不是授權。下載服務再次檢查結果歸屬,再簽發題設中有效 15 分鐘的憑證,操作回應不儲存長期公開位址。
DELETE /v1/report-operations/{id} 可表達取消請求:可取消時回傳目前的 cancel_requested 表示已接受,不可取消或已進入終止狀態時回傳穩定、可重試的結果。工作節點在階段邊界檢查取消標記,停止尚未開始的步驟並清理暫存物件;已提交的外部副作用依預先定義的補償規則處理。終止狀態 7 天後刪除操作,已知曾存在但已到期的 ID 可回傳 410 Gone,未知或無權存取的 ID 則依安全策略回傳 404。
第六步用故障而不是 happy path 驗收。至少涵蓋:伺服器提交交易後遺失 202 回應,用戶端重試只能得到同一 operation;outbox 發布器在傳送前後崩潰,最後仍有工作且只有一個可見結果;執行者寫完結果後遺失確認,接管者不能覆蓋已提交終態;取消與完成同時抵達,狀態機只產生一個合法終態;狀態未變時輪詢遵守退避與條件式請求;跨租戶讀取、取消與下載全部失敗;清理後中繼資料、結果與冪等鍵依契約到期。
可重複使用的判斷規則是:202 解決連線等待,operation 資源解決可觀察性,原子接單與冪等狀態機才解決正確性。
高品質示範回答
「我會先區分接單成功與執行成功。這個工作最長 3 小時,不能占用 30 秒 gateway 連線,所以 POST /v1/report-exports 只做身分、租戶權限、請求結構、輸入存在性與明顯配額檢查。通過後,它在一個交易裡建立 operation 和 outbox,提交成功才回傳 202。回應帶 Location 指向操作資源,並用 Retry-After 給出第一次查詢間隔。202 不代表報表一定成功。
operation 會儲存租戶、冪等鍵、請求指紋、狀態、可驗證的進度、結果或錯誤、版本與到期時間。狀態從 queued 到 running,再到 succeeded 或 failed;取消先進入 cancel_requested,因為工作節點可能剛好已提交結果。同一租戶與端點下,相同冪等鍵加相同請求會回傳同一個 operation;鍵相同但請求不同就回報衝突。即使 202 回應遺失,重試也不會產生第二份報表。
佇列採用至少一次投遞假設。outbox 可以重複發布,消費者依 operation ID 去重,各階段的外部寫入也要冪等或帶 fencing token。狀態更新使用版本條件,避免舊執行者在租約被接管後覆蓋新結果。失敗發生在接單前就直接回傳 4xx;執行中失敗則儲存終態與結構化錯誤,讓用戶端分辨網路失敗、狀態讀取失敗與報表執行失敗。
用戶端依 Retry-After、指數退避與抖動查詢,狀態端點支援 ETag。瀏覽器要即時顯示進度可以加 SSE,合作方服務可使用簽章 webhook,但斷線或重複通知後都回到 operation 查詢。結果檔案不放公開 URL;成功後由下載端點重新授權並簽發 15 分鐘憑證。題設中終態操作保留 7 天、檔案保留 24 小時,到期規則會寫入公開契約。
最後我會特別測試交易提交後回應遺失、outbox 重送、執行者寫入結果後崩潰、取消與完成競態、10,000 個呼叫端同時輪詢,以及跨租戶存取。驗收標準不是能在背景完成一次,而是這些故障下仍然沒有幽靈工作、重複可見結果、非法狀態轉換與越權下載。」
常見錯誤
- 回傳 202 後直接啟動記憶體執行緒 → 程序重啟會留下永遠找不到的工作,接單與啟動之間也沒有原子邊界 → 持久化 operation 與 outbox 後再確認接單。
- 把 202 當成最終成功 → HTTP 明確保留工作最後不執行或失敗的可能 → 把最終結果、錯誤與狀態監視器放入後續契約。
- 只回傳佇列訊息 ID → 缺少租戶擁有權、穩定狀態、錯誤、結果與保留政策 → 建立獨立、已授權的 operation 資源。
- 用戶端固定每秒輪詢 → 尖峰提交會變成持續讀取尖峰 → 提供 Retry-After,並使用退避、抖動、ETag 與配額。
- 用隨機 operation ID 取代授權 → ID 從日誌、瀏覽器歷程或內部連結外洩後仍可越權 → 每次狀態、取消與結果存取都執行物件層級授權。
- 冪等鍵只放 Redis 短鎖 → 鎖到期、崩潰與結果重播仍會建立重複操作 → 使用持久唯一約束、請求指紋與可重播回應。
- 按下取消就立即標記 canceled → 執行者可能已提交不可逆副作用 → 先進入 cancel_requested,以條件轉換與補償決定合法終態。
- 總是回傳虛構百分比 → 不可預測的階段會長時間停在 99%,誤導呼叫端 → 能度量時回傳完成單元,不能度量時回傳階段與更新時間。
- webhook 成功就刪除操作 → webhook 可能遺失、重複或接收端暫時故障 → 保留有期限的 operation 作為復原真相來源。
追問與應對
追問一:資料庫交易已提交,但 202 回應在網路中遺失,用戶端再次提交怎麼辦?
用戶端重複使用原本的 Idempotency-Key。服務端以租戶、端點與鍵找到既有 operation,確認請求指紋相同後重播目前內容與 Location,不再插入 operation 或 outbox。若鍵相同但請求內容不同,回傳衝突並要求新鍵。測試要斷言資料庫只有一筆操作記錄,即使執行佇列重複投遞也只有一個可見結果。
追問二:工作已執行 90%,使用者要求取消,工作節點同時完成結果提交,哪個狀態獲勝?
事先定義允許的轉換,並用版本條件更新決定唯一贏家。若結果交易先從 running 提交為 succeeded,後到的取消讀取終態並回傳 succeeded;若取消先進入 cancelrequested,執行者在提交前必須檢查是否仍允許完成。不可逆步驟可能讓 cancelrequested 合法轉為 succeeded 或 failed;契約不能承諾絕對撤銷。
追問三:進度無法估算,但產品堅持顯示百分比怎麼辦?
先說明虛假百分比會製造錯誤預期。可以顯示已完成階段、目前階段、最近 heartbeat 與根據歷史區間得出的非承諾時間範圍;只有存在穩定總工作量時才提供 completedUnits / totalUnits。若部分階段可度量,就分別顯示階段內進度,不把成本不同的階段簡單平均。
追問四:合作方不想輪詢,是否應該只提供 webhook?
webhook 可以降低正常路徑延遲與查詢量,但不能成為唯一復原機制。callback 會遇到 DNS、憑證、防火牆、簽章輪替、重複與亂序問題。服務端為 webhook 簽章並重試,事件攜帶 operation ID 與版本,接收端去重;合作方漏掉通知後仍能用 operation 資源對帳。瀏覽器沒有穩定 callback 位址時,繼續使用輪詢或 SSE。
追問五:同一份報表產生 20 GiB 檔案,operation API 應直接回傳下載連結嗎?
操作只回傳結果資源參照。下載前重新檢查租戶與權限,再簽發題設中 15 分鐘有效的憑證;不要把長期物件儲存位址寫進 operation。檔案保留 24 小時而操作中繼資料保留 7 天,因此檔案到期後,operation 仍可說明結果曾成功但產物已到期,並提供重新產生動作。
追問六:工作量激增,狀態查詢流量比執行流量還大,先改哪裡?
先檢查用戶端是否遵守 Retry-After、指數退避、上限與抖動,再啟用 ETag 條件式請求、租戶配額與限流。需要更低延遲的瀏覽器連線可透過 SSE 合併更新,服務間使用 webhook,但仍保留低頻查詢。若大量用戶端在同一秒開始輪詢,回應中的建議間隔也要加入抖動,避免把 09:00 提交尖峰平移成狀態端點的週期尖峰。