ChatGPT Tasks 排程任務 (Design ChatGPT Tasks)

一句話定位 + TL;DR(含 35 分鐘心智時鐘)

設計一個 排程任務系統:使用者透過自然語言跟 ChatBot 說「每天早上八點幫我總結財經新聞」,系統就把它變成一個 一次性或週期性的 job,在 正確時間點 觸發執行(呼叫 connector / 產生摘要 / 發通知)。本質是 分散式 cron + 可靠任務執行,規模 10K 排程請求/秒,硬需求是 至少一次執行保證(at-least-once)
招牌取捨:把 「查詢到期 job」(watcher)與「執行 job」(worker)用 queue 解耦,並接受 at-least-once(而非 exactly-once)——放棄「精準一次」的語意簡單,換取 突發流量吸收、執行失敗隔離、worker 獨立擴展、crash 後不靜默丟失;代價是 job 必須 idempotent。其餘決勝點:time_bucket partition key(避免掃全表 broadcast read)、SQS Visibility Timeout 當 heartbeat(偵測 worker crash)、append-only RunEvent outbox 驅動 recurring(對 immutable event 反應而非輪詢 mutable state)、MCP server 把自然語言意圖橋接到 Job Scheduler API。
配速(35 min 預算):~5 min 釐清需求 → ~4 min 容量估算 → ~3 min API + ~5 min 資料模型(time_bucket 是重點)→ ~6 min 高階架構(含 MCP)→ ~9 min deep dive(watcher/worker 解耦 / at-least-once / MCP 可靠性)→ ~3 min 擴展瓶頸 → ~2 min 收尾。開場先一句話複述題目、宣告配速、把 MCP 與 at-least-once 標記為「待 deep dive」——先把端到端 scheduling 串起來,再回頭挖兩個硬骨頭。

1. 釐清需求與範圍 (Clarify & Scope ~5 min)

排程系統的魔鬼在「準時」與「不丟」兩個字。開場先把這兩件事問清楚,別急著畫 queue。

釐清問題 為何要問 / 影響到哪個決策 本題假設(依來源)
Job 有哪幾種觸發型態? 決定資料模型與 recurring 的處理方式 immediate / one-time / recurring(cron 格式)
排程請求量多大? 直接驅動 DB 選型(SQL vs NoSQL) 最高 10K jobs/sec 排程寫入
執行保證等級?exactly-once 還是 at-least? 這是整題的命脈,決定整個可靠性架構 at-least-once(至少一次)
準時度要求多嚴格?秒級還是分鐘級? 決定 watcher 掃描頻率與時間窗 接近準時即可(來源用 5 分鐘掃描窗)
使用者怎麼下單? 決定要不要 MCP / 自然語言層 自然語言 → ChatBot(需 MCP 橋接)
使用者要看 job 狀態嗎?怎麼排序? 決定 viewing 路徑的 index / GSI 設計 ,依 建立時間 排序列出
可用性? 決定容錯 / 多 worker / DLQ High Availability
defer 話術

at-least-once 具體怎麼實作、以及 ChatGPT 怎麼把自然語言變成一筆 job 我先標記,等 deep dive 再展開——現在先假設有個 POST /v1/jobs 的 API 會收到結構化的 job spec,我們先把 schedule → store → trigger → execute 這條主幹串起來。」把 MCP 與可靠性細節推遲,保住 scheduling 主敘事的連貫。

2. 功能性與非功能性需求 (FR / NFR 與設計北極星)

設計北極星(NFR 直接導出後續所有選型的判準):

NFR 導出的設計北極星 後續對映選型
10K writes/sec 排程 寫吞吐要高、可水平擴展、可估算分區 NoSQL(DynamoDB / Cassandra)+ time_bucket partition
at-least-once 執行 失敗必須可觀察、不可靜默丟失、可安全重試 queue + Visibility Timeout heartbeat + DLQ + idempotent
準時觸發 查詢到期 job 不可被「執行慢」拖累 → 查詢與執行解耦 watcher / worker 分離、queue 當 buffer
recurring 不重複 / 不漏 下一次執行由 immutable event 集中產生 append-only RunEvent outbox + RecurringJobWatcher
自然語言下單 意圖 → 結構化 API 呼叫 的可靠橋接 MCP server + 嚴謹 JSON Schema + action-verb 命名
高可用 任一 worker / watcher 掛掉系統不停 無狀態多 worker、queue 重新投遞
一句話北極星

「這是一個 準時、不丟、可重試 的排程系統」——準時靠 watcher/worker 解耦,不丟靠 queue + heartbeat,可重試靠 idempotent + at-least-once。後面每個選型都能從這三個詞推回去。

3. 容量估算 (Back-of-Envelope ~4 min)

估算目的是 驅動 DB 選型與分區策略,不是算到小數點。回連 02-Distributed-Systems/05-Numbers-to-Know

寫流量(schedule QPS)

排程寫入峰值 = 10,000 jobs/sec(來源給定的 NFR 上限)
→ 單一關聯式 write node 通常難穩定吃 10K writes/sec
→ 指向「水平可擴展的 NoSQL」(DynamoDB / Cassandra)

JobRun 放大效應(recurring 的隱藏成本)

recurring job 一次預約 → 對應「多筆」未來 job runs(每次執行一筆 run)
若大量使用者排「每天早上八點」→ 同一 time_bucket 內 job runs 密集
→ 讀路徑風險不是「總量」,而是「同一時間點扎堆」(hot bucket)

讀流量(watcher 掃描)

watcher 每 ~5 分鐘掃一次「接下來到期」的 PENDING job runs
若無分區策略 → 必須 broadcast 掃所有 partition(致命)
→ 用 time_bucket 當 partition key,把「掃描」變成「定位單一 / 少數 partition」

下表數字除 10K/sec 為來源給定外,其餘為 推導值(由 recurring 放大與掃描模式推出,來源未給總 job 數)。

維度 數字 / 結論 對後續決策的意義
Schedule 寫入峰值 10K writes/sec(來源給定) 超出單機 SQL 舒適區 → 選 NoSQL
讀掃描模式 時間區間查詢(推導值) 必須避免 broadcast read → time_bucket partition key
Hot bucket 風險 同一時刻扎堆(推導值,如「每天八點」) partition key 加 shard suffix 分散
執行突發 平時 1000/5min → 峰值暴增(來源舉例) queue buffer 吸收,watcher 不直接打 worker
估算的最大收穫

「10K writes/sec」一個數字就否決了單機 SQL、指向 NoSQL;而「同一時間點扎堆」這個 讀模式 否決了 naive 全表掃描、逼出 time_bucket partition key。這兩個推論是本題從估算到選型的關鍵 hand-off。

4. 用到的積木 / Building Blocks Used

架構即目錄:下表每個決策對映既有 concept note(無 alias 連結);本表即「本案例練到哪些積木」的單一事實來源。

設計決策 積木 在本題的作用(≤1 行)
容量估算驅動選型 02-Distributed-Systems/05-Numbers-to-Know 10K writes/sec → NoSQL;時間區間讀 → time_bucket 分區
高寫吞吐 KV / 寬列儲存 06-Database-Tech/03-DynamoDB partition key + sort key 模型,吃 10K writes/sec
partition / sort key 設計 05-Database-Advanced/02-Sharding time_bucket 當 partition、(scheduled_at, job_id) 當 sort
排程屬於長時間 / 非同步任務 10-Design-Patterns/03-Long-Running-Tasks 預約 → 之後才執行,天生 async;watcher/worker 模式
queue 當 buffer 解耦 09-Messaging-Coord/02-Queue 隔離執行失敗、吸收突發、worker 獨立擴展
at-least-once 可靠投遞 12-Ops-and-Reliability/03-Reliable-Delivery Visibility Timeout heartbeat + DLQ + idempotent
重試 / 失敗處理 01-Networking/07-Fault-Handling RETRYING → FAILED → DLQ 的重試與降級鏈
突發執行高峰防護 12-Ops-and-Reliability/02-Overload-Protection queue 削峰填谷,worker 依容量消費,不被瞬時尖峰打垮
狀態更新與 outbox 一致性 05-Database-Advanced/01-Transactions 同 transaction 更新狀態 + append RunEvent,避免 race
recurring 用 LLM / connector 08-Search-and-AI/02-RAG job action 常是呼叫 LLM 摘要(如財經新聞);MCP 為其入口
無狀態 worker 水平擴展 02-Distributed-Systems/03-Scalability worker / watcher 無狀態,加機器即擴容
API endpoint 設計 03-API-Design/02-REST POST /v1/jobsGET /jobs?status=... 資源風格
viewing 排序 / GSI 04-Database-Indexing/01-Indexing-Overview user_id 當 PK、time-sortable id 當 SK;必要時建 GSI

5. API 設計 (API Design ~3 min)

由 FR 驅動,採 REST 資源風格(理據見 03-API-Design/02-REST)。對外是 REST,對 ChatGPT 則由 MCP server 把 tool call 轉成這些呼叫。

Method Path 用途 重點
POST v1/jobs 排程一個 job body 帶 actionjob_params:{type, time, schedule}
GET jobs?status=&start_time=&end_time=&pageSize=&page= 查詢 job 狀態 依狀態 / 時間範圍 / 分頁列出,回傳 Job[]

POST /v1/jobs 的 body 結構(來源原樣)

{
  "action": "呼叫某 connector / 產生摘要 / 發通知",
  "job_params": {
    "type": "immediate | one-time | recurring",
    "time": "<ISO 時間>", // 僅 one-time 需要:指定未來執行時間
    "schedule": "* 10 * * *", // 僅 recurring 需要:cron 格式
  },
}
細節話術

type 把三種觸發型態收斂成一個欄位:immediate 不需 time/schedule、one-timetimerecurring 用 cron 的 schedule。這讓 同一張表、同一條寫入路徑 處理全部型態,差異只在「下一次 run 怎麼算」。

6. 資料模型與儲存選型 (Data Model & Storage Choice ~5 min)

核心是 兩張表 + 一個 outboxJobs(job 定義)、JobRun(每一次實際執行實體)、RunEvent(append-only 執行事件)。JobRun 獨立成表是 recurring 的關鍵——一個 recurring job 對應「多筆」未來 runs。

Jobs                                  ← job 的「定義」
--------------------------------------------------
job_id        STRING  PK
user_id       STRING
action        JSON    要執行什麼
type          ENUM    immediate | one-time | recurring
schedule      STRING  cron(recurring)
created_at    TIMESTAMP   ← viewing 依此排序

JobRun                                ← 每一次「實際執行實體」(核心表)
--------------------------------------------------
time_bucket   STRING  PARTITION KEY  ← scheduled_at truncate 到「最近一小時」
scheduled_at  TIMESTAMP ┐ SORT KEY   ← 同 partition 內依執行時間排序
job_id        STRING    ┘ SORT KEY   ← (scheduled_at, job_id) 複合
status        ENUM    PENDING|RUNNING|SUCCEEDED|RETRYING|FAILED
retry_count   INT
--------------------------------------------------
partition key 範例(防 hot shard):YYYYMMDDHH#S{00..31}  ← 後綴 shard suffix

RunEvent                              ← append-only outbox(驅動 recurring)
--------------------------------------------------
event_id      STRING
job_id        STRING
status        ENUM    terminal status(SUCCEEDED / FAILED)
emitted_at    TIMESTAMP
(immutable,只 append 不 update)

為什麼 time_bucket 是整題的資料模型核心:watcher 要找「接下來 5 分鐘到期的 PENDING runs」。若以 job_id 為 partition key,這個時間區間查詢得 掃所有 partition(broadcast read)——10K/sec 規模下直接爆炸。把 scheduled_at truncate 到最近一小時當 partition key,時間區間查詢就退化成「定位 1~2 個 partition + 在 partition 內二分」,複雜度逼近 O(log n)

儲存選型:

選項 適配本題嗎 理由
NoSQL(DynamoDB / Cassandra) ✅ 主選 10K writes/sec 超出單機 SQL;partition + sort key 天生契合時間區間查詢
關聯式 DB △ 也行 viewing 排序 / 交易直覺,但 10K 寫入需 sharding,複雜度更高
append-only 表(RunEvent) ✅ 輔助 recurring 對 immutable event 反應而非輪詢 mutable state,降低 race

→ 積木:NoSQL 選型見 06-Database-Tech/03-DynamoDB;partition / sort key 設計見 05-Database-Advanced/02-Sharding;index / GSI 見 04-Database-Indexing/01-Indexing-Overview

Viewing 流程的資料模型:使用者要「列出自己所有 jobs、依建立時間排序」。

邊界

time_bucket 解決的是 執行路徑(按時間掃描)user_id PK 解決的是 viewing 路徑(按使用者列出)同一份資料、兩種存取模式 → 兩套 key 設計,這是 NoSQL 單表 / GSI 思維的典型體現。別想用一個 partition key 兼顧兩者。

7. 高階架構 (High-Level Design ~6 min)

先看 naive scheduling,再看 MCP 整合,最後是 deep dive 後的健壯版(第 8 段才引入 queue)。

(A) Naive scheduling flow(deep dive 前,executor 同時查 + 執行)

使用者 (自然語言) ─► ChatBot
        │  POST v1/jobs { action, job_params }
        ▼
   Job Scheduling Service
        │ 1. 寫 Jobs table(job 定義)
        │ 2. 寫 JobRun(status=PENDING, time_bucket, scheduled_at)
        ▼
   ┌──────────── NoSQL (Jobs / JobRun) ────────────┐
   └───────────────────┬───────────────────────────┘
                       │ 每 ~5 min 掃描
                       ▼
                 Executor(查詢 + 執行 都它做)  ◄─ 第8.1 指出此為脆弱點
                       │ 找出 time_bucket 內、接下來5分鐘、PENDING 的 runs
                       │ 在正確時間點執行 → status=SUCCEEDED
                       │ recurring → 算下一次、再寫一筆 JobRun
                       │ 失敗 → RETRYING → 耗盡 retries → FAILED
                       ▼
                  執行 action(connector / LLM 摘要 / 通知)

(B) MCP 整合 flow(自然語言 → tool call → Job Scheduler API)

使用者「每天早上八點總結財經新聞」
        ▼
   ChatGPT (model)  ──(thread 開始時) Discovery:呼叫 tools/list ──► MCP Server
        │                  └─ 回傳 tool metadata(名稱/描述/JSON Schema),快取在 thread
        │  每輪 model 看到:system instr + user msg + 快取的 tool list
        │  Tool selection:依名稱/描述/schema 選 tool(如 task.create@v1)
        │  Argument filling:依 schema 組 JSON args → 產生 mcp_tool_call
        ▼
   MCP Server ──轉發──► Job Scheduler API (POST v1/jobs) ──► 乾淨 JSON 結果
        ▲                                                         │
        └───────────────── mcp_tool_result ◄──────────────────────┘
        ▼
   model 把 JSON 整合成自然語言回覆給使用者

(C) Scaled 架構(deep dive 後,watcher / worker 用 queue 解耦)

   使用者(NL) ─► ChatGPT ─► MCP Server ─► Job Scheduling Service
                                              │ 寫 Jobs + JobRun(PENDING)
                                              ▼
                          ┌─────── NoSQL (Jobs / JobRun / RunEvent) ───────┐
                          └───┬───────────────────────────────────▲───────┘
                              │ 每 ~5min 查 PENDING                  │ 同 txn:
                              ▼ (time_bucket 定位,非 broadcast)    │ 更新狀態
                        Watcher Process                            │ + append RunEvent
                              │ 把到期 run 發佈到 queue              │
                              ▼                                     │
                        ┌──────────────┐   消費    ┌────────────────┴───┐
                        │ Message Queue │ ───────► │ Workers (N, 無狀態) │
                        │  (SQS)        │ ◄──crash │  執行 action        │
                        └──────┬───────┘ 重新可見  │  成功→刪訊息        │
                               │ retries 耗盡       │  失敗→RETRYING/重試 │
                               ▼                    └─────────┬──────────┘
                            DLQ(人工檢查)                    │ recurring 時
                                                              ▼
                                          RecurringJobWatcher 輪詢 RunEvent
                                          (terminal status) → 算下次 → 插新 JobRun

關鍵 hand-off:Scheduling Service 只負責「寫入定義 + 建 PENDING run」Watcher 只負責「查到期 → 丟 queue」Worker 只負責「消費 → 執行 → 更新狀態 + append event」RecurringJobWatcher 只負責「看 immutable event → 排下一次」。四個角色職責單一、各自獨立擴展。

8. 深入探討 (Deep Dives ~9 min)

不重複原則(link-don't-restate)

通用機制(queue 語意、replication、index 原理)只在 concept note 解釋;這裡只講「本題怎麼用、選哪個、為什麼」。本題無 home note 的新材料(Visibility Timeout heartbeat、time_bucket partition key、MCP protocol、append-only RunEvent outbox)才在此 inline 教學並登記到 new_concepts_introduced

8.1 如何確保 job 準時執行?(watcher / worker 解耦)

問題:怎麼讓 job 在「正確時間點」被執行,且系統放大後不變脆弱?
為何是瓶頸:naive 版的 executor 同時負責查詢與執行,兩件事互相拖累:

  1. 執行慢拖垮查詢:executor 每 5 分鐘要查下一批,但某個 job 本身執行 > 5 分鐘 → 下一批查不到、後續全 遲到
  2. 執行高峰打爆 executor:平時 5 分鐘 ~1000 runs,但「Black Friday 提醒」之類情境瞬間湧入大量同時段 job → executor 無法在時間窗內跑完。
方案 做法 優點 缺點
A. 單一 executor(查 + 執行) 一個 process 掃 DB 又跑 job 簡單、組件少 執行慢拖垮查詢;尖峰打爆;失敗難隔離
B. watcher / worker + queue 解耦 watcher 查到期 → 丟 queue;worker 群消費執行 隔離失敗、吸收突發、worker 獨立擴展 多一個 queue 組件;引入 at-least-once 複雜

選擇與理由:採 B。queue 作為 buffer 達成三件事:(1) 隔離 job execution failure,執行失敗不影響查詢;(2) worker 可獨立於 watcher 水平擴展;(3) queue 提供 durability——僅在成功執行後才移除訊息(如 SQS)。watcher 變得很輕(只查 + 丟),worker 多寡按負載彈性增減。
→ 積木:10-Design-Patterns/03-Long-Running-Tasks09-Messaging-Coord/02-Queue12-Ops-and-Reliability/02-Overload-Protection

新材料:time_bucket partition key(本案例首次 introduce)

watcher 的查詢是「接下來 N 分鐘、status=PENDING 的 runs」——一個 時間區間查詢。若 partition key 不含時間,DB 得 broadcast 掃所有 partition。解法:partition key = scheduled_at truncate 到最近一小時(如 2026062208),sort key = (scheduled_at, job_id)。如此查某時段只需定位 1~2 個 partition 並在內部二分,複雜度 ≈ O(log n)Hot shard 防護:若大量 job 排在同一小時(「每天八點」),在 partition key 後加 shard suffixYYYYMMDDHH#S{00..31}),把讀寫散到 32 個 sub-partition。

8.2 如何確保至少一次執行(at-least-once)?

問題:worker 取了訊息後 crash 並離線,這個 job 可能「靜默消失(silent failure)」——既沒成功、也沒回到 queue。怎麼保證它最終被執行?
為何是瓶頸:retry policy + DLQ 的前提是「失敗能被觀察到」。worker 拿了訊息就 crash,系統根本不知道它沒做完。

新材料:SQS Visibility Timeout 當 heartbeat(本案例首次 introduce)

需要一種 heartbeat 機制:worker 接手後若逾時沒做完,系統判定它掛了、把 job 交給別的 worker 重試。Amazon SQS Visibility Timeout 直接提供:

  1. worker 從 queue 取訊息後,SQS 讓該訊息在一段時間內 對其他 worker 不可見
  2. worker 需 定期延長 visibility timeout(這就是 heartbeat:我還活著、還在做)。
  3. worker 成功完成 → 刪除訊息(訊息消失 = 確認完成)。
  4. worker crash 或逾時未完成 → SQS 自動讓訊息重新可見 → 別的 worker 接手。

這套機制根除 worker crash 的 silent failure,是達成 at-least-once 的核心。

選擇與理由:採 SQS Visibility Timeout heartbeat + retry + DLQ 組合。worker 重試耗盡後把 job 送 DLQ(Dead Letter Queue) 供人工檢查,不阻塞主流程。
→ 積木:12-Ops-and-Reliability/03-Reliable-Delivery09-Messaging-Coord/02-Queue01-Networking/07-Fault-Handling

at-least-once 的代價:job 必須 idempotent

訊息重新可見 → 同一 job 可能被執行 多次。所以 job 本身必須 idempotent:重複執行多次的結果必須等同只執行一次(例如「發摘要 email」要用去重鍵避免發兩封)。這就是本題的 core_tradeoff——選 at-least-once 換取「不丟」,代價是把 idempotency 的責任推給 job 本身。若要 exactly-once,複雜度與成本會高出一個量級。

8.3 如何讓 ChatGPT 與 MCP Server 的整合更可靠?

問題:model 是機率性的,怎麼讓「自然語言 → 正確的 tool call → 正確的 API 呼叫」這條鏈穩定?
為何重要:tool 選錯 / 參數填錯 → job 排錯時間 / 排錯動作,使用者完全無感地踩雷。

新材料:MCP(Model Context Protocol)server(本案例首次 introduce)

MCP 是開源標準,讓 AI 應用(ChatGPT / Claude)連外部系統(資料來源、工具、prompts)。整合流程:(1) Discovery(一個 thread 只做一次):ChatGPT 呼叫 tools/list,tool metadata 快取在 thread;(2) 每輪 model 看到 system instructions + user message + 快取 tool list;(3) Tool selection:依名稱/描述/schema 選 tool;(4) Argument filling:依 schema 組 JSON args → mcp_tool_call;(5) MCP server 轉發 至實際 Job Scheduler API;(6) model 把回傳 JSON 整合成自然語言回覆。

可靠性原則 做法 為什麼有效
action-verb 命名 task.create@v1,附「何時用 / 何時不用」一句描述 model 把使用者動詞對應 imperative 命名更可靠
version suffix 一律加 @v1;schema 改動發 @v2,舊版續存 chat 快取 tool list,改同名 schema 會破壞長對話
嚴謹 JSON Schema required(僅標 model 無法推斷的)、enums、defaults、additionalProperties:false 降錯誤率、減 retries、讓 auto-repair 更易
極簡 system instruction 第一則 message 說明何時用哪 tool、缺輸入時的預設(~150 tokens) 一次性低成本地消歧義
結構化、可修正錯誤 {ok:false, error:{code, message, field, expected}} model 能自我修正後重試,而非卡死

選擇與理由:這五條讓「機率性 model」與「確定性 API」之間的橋接 可預測、可自癒。job action 常是呼叫 LLM(如「總結財經新聞」),MCP 正是這類 LLM-as-action 的標準入口。
→ 積木:08-Search-and-AI/02-RAG(job action 常為 LLM 摘要 / 檢索)、03-API-Design/02-REST(MCP 轉發的底層 API)

9. 擴展與瓶頸 (Scaling & Bottlenecks ~3 min)

主動找出系統會先在哪裂開並補強。

瓶頸 / 風險 補強手段 積木
10K writes/sec 壓垮單機 DB NoSQL + partition key 分散寫入 06-Database-Tech/03-DynamoDB05-Database-Advanced/02-Sharding
同一時段扎堆(hot partition) partition key 加 shard suffixYYYYMMDDHH#S{00..31})散開讀寫 05-Database-Advanced/02-Sharding
執行突發尖峰打爆 worker queue 當 buffer 削峰,worker 依容量消費;必要時 autoscale worker 09-Messaging-Coord/02-Queue12-Ops-and-Reliability/02-Overload-Protection
worker crash → 靜默丟 job Visibility Timeout heartbeat 逾時重投,DLQ 收尾 12-Ops-and-Reliability/03-Reliable-Delivery
recurring 重複 / 漏排 append-only RunEvent + idempotent insert,對 immutable event 反應 05-Database-Advanced/01-Transactions
watcher 自己掛了 多 watcher 無狀態、按 time_bucket 分工;掛掉重啟接續掃描 02-Distributed-Systems/03-Scalability
broadcast read 拖慢查詢 time_bucket partition key 把區間查詢變 O(log n),不掃全表 04-Database-Indexing/01-Indexing-Overview
drive 的關鍵句

「系統會 先在『執行突發 + worker crash』兩處裂開:突發靠 queue buffer 削峰、worker 獨立擴展;crash 靠 Visibility Timeout heartbeat 重投 + DLQ。寫入 10K/sec 與時間區間讀則靠 NoSQL + time_bucket(加 shard suffix)解決。」先點出最脆弱處再補,比逐項羅列有說服力。

10. 取捨總表與收尾 (Trade-off Recap & Wrap-up ~2 min)

決策點 選擇 放棄了什麼 為什麼值得
執行語意 at-least-once + idempotent job exactly-once 的語意簡單 換取「不靜默丟失」+ 可安全重試(招牌取捨
查詢 vs 執行 watcher / worker 用 queue 解耦 單一 executor 的組件簡單 隔離失敗、吸收突發、worker 獨立擴展
crash 偵測 SQS Visibility Timeout heartbeat 自建 heartbeat / lease 機制 直接複用託管能力,逾時自動重投
DB 選型 NoSQL(DynamoDB / Cassandra) SQL 的交易 / 排序直覺 吃 10K writes/sec、partition+sort 契合時間查詢
partition key time_bucket(+ shard suffix) 用 job_id 的直覺 避免 broadcast read、防 hot shard
recurring 排下一次 append-only RunEvent outbox + 集中 watcher worker 直接插入的簡單 對 immutable event 反應,避免重複 / race
NL 下單 MCP server + 嚴謹 schema + version suffix 直接讓 model 拼 API 機率性 model → 確定性 API 的可預測 / 自癒橋接
收尾話術

「整題的單一錨點是 at-least-once:我寧可讓一個 job 偶爾跑兩次(要求它 idempotent),也不能讓它 靜默消失一次——這就決定了整條可靠性鏈:queue 解耦 watcher/worker、Visibility Timeout 當 heartbeat 抓 crash、DLQ 收尾、recurring 走 append-only outbox。寫入 10K/sec 與時間區間讀則靠 NoSQL + time_bucket partition(加 shard suffix)。最上層用 MCP server 把自然語言意圖可靠地翻成 Job Scheduler API。」引用既有決策用連結,不重新推導。

後續追問演練 (Interviewer Follow-ups — "What if…")

為什麼要把 watcher(查詢)和 worker(執行)分開?

time_bucket 是什麼?為什麼需要它?

at-least-once 為什麼強制 job 必須 idempotent?

排程寫入從 10K 放大到 100K/sec,哪裡先撐不住?怎麼補?

一百萬使用者都排「每天早上八點」,那個 time_bucket 變超級 hot shard,怎麼辦?

worker 在執行到一半 crash 了,這個 job 會怎樣?

為什麼用 append-only RunEvent outbox 來驅動 recurring,而不是 worker 執行完直接插下一筆?

MCP 整合裡為什麼 tool 一定要加 version suffix(@v1/@v2)?

這題的 NoSQL 選型理由,跟 QR Code 那題「刻意不 sharding」為何方向相反?(一般推論,非本來源所述)

主動回想:白板重建 (Whiteboard from Scratch)

不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。

常見陷阱 (Exam Traps for This Problem)