Google Docs 協作編輯 (Design Google Docs)
設計一個讓 多人即時協作編輯同一份文件 的系統。核心難點不是 CRUD,而是 並發編輯的衝突解決:A、B 同時在同一位置插字,如何讓所有人 最終看到一致的內容,且每次按鍵都 < 100ms 回饋。
招牌取捨:採 Operational Transformation(OT)+ 中心化單一序列器(owner),而非 CRDT。OT 讓同一份文件的所有 edits 都打到 同一台 server 排序、transform 後立即回送——換來 極低輸入延遲 與 決定性總序,代價是放棄 CRDT 的離線 / 多主 / P2P 能力,且 單文件併發上限受限(本題 ≤ 100 人)。其餘決勝點:WebSocket 雙向即時通道、Consistent Hashing + Service Registry 把文件路由到 owner server、snapshot / compaction 控制 op 累積成本、游標 presence 只放記憶體、viewer mode 用 Kafka 解耦 + SSE 廣播。
配速(35 min 預算):~5 min 釐清需求 → ~4 min 容量估算 → ~3 min API + ~3 min 資料模型 → ~6 min 高階架構 → ~9 min deep dive(OT / WebSocket 擴展 / compaction / viewer)→ ~3 min 擴展瓶頸 → ~2 min 收尾。開場先一句話複述題目、宣告配速、把「OT 演算法內部」標記為「待會 deep dive」——宣告配速本身就是高分動作,讓面試官知道你有限時面試的心智模型,也替你爭取「先框架、後細節」的許可。
1. 釐清需求與範圍 (Clarify & Scope ~5 min)
開場後先問清楚邊界,別急著畫架構(弱候選人最常見死因就是沒釐清就開始畫)。建議問題:
| 釐清問題 | 為何要問 / 影響到哪個決策 | 本題假設(依來源) |
|---|---|---|
| 核心是純文字還是富文本 / 圖片 / 表格? | 決定 op 模型複雜度與儲存 | 聚焦純文字 + 樣式的 insert / delete |
| 單份文件最多幾人同時編輯? | 直接決定 OT vs CRDT、是否能中心化 | ≤ 100 人/文件(OT 中心化可行) |
| 一致性要求多強?要立即一致還是最終一致? | 決定衝突解決策略 | eventual consistency(最終所有人看到一致) |
| 延遲要求?哪條路徑最敏感? | 決定走 WebSocket 還是輪詢 | 每按鍵 ~50–100ms 低延遲 |
| 規模多大?文件數 / 同時在線數? | 驅動 sharding / connection 擴展 | 數十億文件、數百萬同時在線 |
| 需要游標 / presence 即時顯示嗎? | 決定是否要 ephemeral 通道 | 需要(協作者游標即時顯示) |
| 需要唯讀 viewer mode 嗎? | 決定是否要獨立 fan-out cluster | 需要(deep dive 展開) |
| 需要離線編輯 / 版本歷史嗎? | 直接影響 OT vs CRDT 的選擇 | 本題不主打離線(→ 偏向 OT) |
「OT 的轉換函式內部數學 我先標記,等到 deep dive 再展開——OT / CRDT 的演算法細節超出 1 小時 SD 面試範圍,現在先當成一個『能把並發 edits 排成一致順序』的 black box,讓我們先把端到端流程串起來。」這招把演算法細節推遲,保住架構敘事的連貫性。
2. 功能性與非功能性需求 (FR / NFR 與設計北極星)
-
功能性需求 (FR)
- 使用者可 建立文件(CRUD)。
- 多位使用者可 同時線上編輯同一份文件(collaborative editing)。
- 修改 即時同步 給其他正在編輯的協作者(real-time updates)。
- 即時看到其他協作者的 游標位置 / presence。
-
非功能性需求 (NFR)
- 最終一致性:所有使用者最終看到的內容必須一致。
- 單文件併發上限 ≤ 100 人。
- 低延遲更新:每按鍵 ~50–100ms。
- 規模:數十億文件、數百萬同時在線使用者。
設計北極星(NFR 直接導出後續所有選型的判準):
| NFR | 導出的設計北極星 | 後續對映選型 |
|---|---|---|
| 並發編輯 + 最終一致 | 需要一個能定序並 transform edits 的機制 | OT + 中心化 owner(同文件 edits 進同一台) |
| 低延遲 + 雙向即時 | 不能輪詢;要長連線、server 主動推 | WebSocket |
| 單文件 ≤ 100 人 | 中心化序列器可行(不必為極端併發選 CRDT) | OT 而非 CRDT |
| 數百萬連線 | 單台 server 扛不下;要把文件均分到多台並可重路由 | Consistent Hashing + Service Registry |
| 數十億文件 / op 無限累積 | 不能讓 op 無限長;新加入者不能重播全歷史 | snapshot / compaction + cold storage / GC |
| 游標只關心「當下」 | 過去位置無價值、離線即丟 | ephemeral in-memory presence |
「這是一個 低延遲、最終一致、中心化定序的並發編輯 系統」——記住這三個詞,後面每個選型(OT、WebSocket、consistent hashing、compaction)都能從它推回去。
3. 容量估算 (Back-of-Envelope ~4 min)
估算的目的不是算到精確,而是 驅動 connection 擴展 / 儲存 / compaction 決策(估算 → 選型的 hand-off)。回連 02-Distributed-Systems/05-Numbers-to-Know。
連線數(WebSocket fan-out 的主壓力)
數百萬同時在線使用者 ≈ 1,000,000+ 並發 WebSocket 連線
單台 server 約可撐 萬級長連線(受 fd / 記憶體 / CPU 限制)
→ 需 數十~數百台 Document Service 才能承載 → 必須水平擴展 + 路由
儲存(op store / 文件本體)
數十億(1B+)份文件
若每份壓縮後 ≈ 100KB → 1B × 100KB ≈ 100 TB(採 OT、做 compaction 後的量級)
若不做 compaction:單份文件 1000 次編輯 × 1KB/snapshot ≈ 1GB → 全量爆炸
單文件 op 累積(compaction 觸發點)
熱門文件可累積 數百萬 ops
若新 client 進入要重播全部 ops → 載入延遲與頻寬不可接受
→ 必須定期 snapshot + compaction,新 client 只收 snapshot + tail ops
下表連線數 / 儲存量皆為 推導值(由「同時在線數」與「文件數 × 每份大小」算出,來源僅給出數十億文件 / 數百萬在線 / ≤100 人/文件 / 50–100ms 等輸入)。
| 維度 | 數字 | 對後續決策的意義 |
|---|---|---|
| 並發連線 | 1M+ WebSocket(來源輸入) | 單台扛不下 → consistent hashing 路由 + 多 server |
| 文件總儲存 | ≈ 100 TB(推導值,採 OT+compaction) | 比「存 snapshot」省一個數量級;仍需 sharding + cold storage |
| 不做 compaction | 單份可達 GB 級(推導值,1000×1KB) | 印證 只送 edits 而非 snapshot + compaction 的必要性 |
| 單文件 op 數 | 可達數百萬 | 觸發 snapshot/compaction,避免新 client 重播全歷史 |
兩個結論:(1) 數百萬連線 → 必須把文件路由到多台 server,這直接導出 deep dive #1(consistent hashing);(2) op 無限累積 + 100TB → 必須 compaction,這導出 deep dive #3。先算出「會在哪爆」再選型,比硬塞元件更高分。
4. 用到的積木 / Building Blocks Used
架構即目錄:先給導覽圖,再展開。下表每個決策都對映一個既有 concept note(無 alias 連結);本表即「本案例練到哪些積木」的單一事實來源(single source of truth)。
| 設計決策 | 積木 | 在本題的作用(≤1 行) |
|---|---|---|
| 容量估算驅動選型 | 02-Distributed-Systems/05-Numbers-to-Know | 算出 1M+ 連線 / 100TB,決定路由與 compaction |
| 雙向即時編輯通道 | 01-Networking/05-Realtime-Protocols | WebSocket 讓 edits 雙向低延遲;viewer 用 SSE 單向廣播 |
| 即時更新與廣播模式 | 10-Design-Patterns/05-Real-time-Updates | edit 持久化後 broadcast 給同文件其他協作者 |
| 文件 → owner server 路由 | 02-Distributed-Systems/04-Consistent-Hashing | 把文件均分到多台,server 增減時最小化重路由 |
| 成員偵測與 owner 查找 | 09-Messaging-Coord/05-Zookeeper | Service Registry 註冊 / 偵測節點,推 ring_config 給 Gateway |
| 連線入口與升級 | 11-Infrastructure/01-API-Gateway | 依 ownership 把 client 導到對應 doc server 再升級 WebSocket |
| op store 高寫入吞吐 | 06-Database-Tech/03-DynamoDB | Cassandra/寬列:doc_id 為 partition key、ts 為 sort key |
| op store 寫入擴展 | 05-Database-Advanced/02-Sharding | 依 doc_id 分片,分散數十億文件的寫入 |
| 並發路徑可擴展 | 10-Design-Patterns/02-Scaling-Writes | 同文件單序列器 + 跨文件水平分片,吸收高寫入 |
| 一致性模型 | 02-Distributed-Systems/02-CAP-Theorem | 接受 eventual consistency 換可用 / 低延遲 |
| viewer 廣播解耦 | 09-Messaging-Coord/03-Kafka | edit owner 把 ops 發到 durable queue,viewer cluster 消費 |
| 有序耐久投遞 | 12-Ops-and-Reliability/03-Reliable-Delivery | queue 保證 viewer 看到的 op 順序 = 持久化順序、可 replay |
| 文件本體 / cold storage | 07-Caching-Storage/02-Blob-Storage | snapshot 與舊 ops 沉到 object store 降成本 |
5. API 設計 (API Design ~3 min)
由 FR 驅動:建立走一般 REST,編輯走 WebSocket 事件(理據:每按鍵需雙向低延遲,見 01-Networking/05-Realtime-Protocols)。
| 通道 | Method / 事件 | 用途 | 重點 |
|---|---|---|---|
| HTTP REST | POST v1/docs |
建立文件 | body { title } → 回 { doc_id } |
| HTTP REST | GET v1/docs/:doc_id |
開始編輯(取得 owner) | Gateway 依 ownership 回對應 socket server 位址 |
| WebSocket | SEND { type:"insert", position, content } |
插入操作 | 在指定位置插入內容;server 排序 + OT 後廣播 |
| WebSocket | SEND { type:"delete", range } |
刪除操作 | 刪除指定範圍;同樣經 OT |
| WebSocket | SEND { type:"updateCursor", position } |
回報游標 | presence;只進記憶體、不持久化 |
「建立文件是普通 CRUD,但 真正的協作 API 是 WebSocket 事件流——insert / delete 是會被 transform 的 op,updateCursor 是 ephemeral presence。把這兩類資料分開,是後面 op store 走 Cassandra、游標走純記憶體的根據。」
6. 資料模型與儲存選型 (Data Model & Storage Choice ~3 min)
兩類資料,命運完全不同:op(要持久、要定序) vs 游標(ephemeral、純記憶體)。
Documents(metadata,CRUD)
--------------------------------------------------
doc_id UUID PK
title VARCHAR
owner_id BIGINT
created_at TIMESTAMP
Operations / Operation Store(高寫入,定序 append)
--------------------------------------------------
doc_id partition key ← 同文件的 ops 聚在同一分片
ts (seq) sort key ← server 指派的 server_seq,保證總序
op_type insert / delete
payload position + content / range
--------------------------------------------------
採 Cassandra / 寬列:針對 high write throughput 最佳化
按 doc_id 分片 → 數十億文件可水平擴展
Snapshot(compaction 產物,存 blob / cold storage)
--------------------------------------------------
doc_id, snapshot@seq, blob_location ← 新 client 從這裡載入再補 tail ops
Cursor / Presence(不入 DB)
--------------------------------------------------
只存 doc server 記憶體:{ user_id → cursor_position },離線即移除
儲存選型:
| 選項 | 適配本題嗎 | 理由 |
|---|---|---|
| 寬列 NoSQL(Cassandra)做 op store | ✅ 主選 | op 是 高吞吐 append;doc_id partition + ts sort 天生契合,寫入水平擴展 |
| 關聯式 DB 做 op store | △ | metadata 可,但極高寫入下不如 Cassandra;同文件強定序由 server 序列器 負責 |
| Object Store(Blob)做 snapshot / 冷資料 | ✅ 輔助 | snapshot 與舊 ops 是大型不可變物件,沉到 cold storage 降成本 |
| 游標放 DB | ❌ | 游標 只有當下重要、離線即丟 → 持久化純屬浪費,放記憶體即可 |
→ 積木:op store 寬列模型見 06-Database-Tech/03-DynamoDB、分片見 05-Database-Advanced/02-Sharding;snapshot 走 07-Caching-Storage/02-Blob-Storage。
op store「強定序」不是靠 DB transaction,而是靠 同文件所有 edits 都進同一台 owner server、由 server 指派遞增 server_seq。DB 只負責耐久 append;定序責任在序列器。這正是選 OT 中心化的直接後果。
7. 高階架構 (High-Level Design ~6 min)
把前面的積木串成端到端流程。先看核心 flow,再看 scaled 架構。
(A) 連線 / 開始編輯 flow
Client
│ GET v1/docs/:doc_id (想開始編輯)
▼
API Gateway ──查 ownership(本地用 hash(doc_id, healthy_nodes) 算 owner)
│ 回傳對應 Document Service 的 socket server 位址
▼
Client ──建立 WebSocket──► Document Service (該文件的 owner server)
│ owner 從 operation store 載入 snapshot + tail ops
▼
先把 snapshot + ops push 給 client → client 重建當前文件狀態,可開始編輯
(B) 並發編輯 flow(OT 核心)
User A ──SEND insert(...)──┐
User B ──SEND insert(...)──┤
▼
Document Service (single owner / 序列器)
│ 1. 決定唯一順序(指派 server_seq)
│ 2. 對後到的 edit 做 OT transform(依當下文件狀態)
│ 3. append 到 operation store (Cassandra: doc_id|ts)
▼
broadcast transformed op 給同文件其他 client
│
▼
其他 client 收到後,client 端也須再做一次 OT
(server 順序 Ea→Eb,但 B 本地已套 Eb,故 B 需對 Ea 再 transform)
→ 所有人最終收斂到一致狀態(eventual consistency)
(C) Scaled 架構(端到端)
┌─────────────────────────────┐
│ Service Registry (Zookeeper) │ ← 偵測節點上/下線
│ push online nodes+ring_config │ quorum 強一致
└───────────┬─────────────────┘
│ (節點清單 / ring 配置)
Client ──HTTP──► API Gateway ─────┤ 本地 hash(doc_id, healthy_nodes)→owner
(依 ownership 路由)│
▼
┌────────────────────────────────────────┐
│ Document Service 叢集(多台,按文件分片) │
│ 每台負責一部分文件:序列器 + WebSocket fan-out│
│ 記憶體:游標 presence(離線即移除) │
└───────┬───────────────────────┬─────────┘
(持久化 ops)│ │(發佈 ops 給 viewer)
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ Operation Store │ │ Durable Queue (Kafka) │
│ Cassandra: doc_id|ts │ └───────────┬──────────────┘
│ + Snapshot → Blob/Cold │ │ (有序 / 可 replay)
└──────────────────────┘ ▼
┌──────────────────────────┐
│ Viewer Cluster (SSE 單向) │
│ viewer-owner 消費並廣播 │
└──────────────────────────┘
關鍵 hand-off:Gateway 用 consistent hashing 算 owner → client 連到正確的 doc server(升級 WebSocket)→ owner 序列 + OT + 持久化 → broadcast 給編輯者、並發佈到 Kafka 給 viewer cluster(SSE)。游標與主資料路徑解耦(純記憶體)。
8. 深入探討 (Deep Dives ~9 min)
通用機制(WebSocket 細節、consistent hashing 原理、Kafka 投遞語意、分片)只在 concept note 解釋;這裡只講「本題怎麼用、選哪個、為什麼」。任一機制超過 ~3 行就砍掉改連結。本題無 home note 的新材料(OT、CRDT 取捨、snapshot/compaction、ephemeral presence)才在此 inline 教學並登記到 new_concepts_introduced。
8.1 並發編輯如何收斂一致:OT vs CRDT(招牌取捨)
問題:A、B 同時看到 "This is a doc",A 在尾端插 "ument"、B 在尾端插 "!"。誰先到決定結果——若不處理,兩人最終看到不同內容。
為何是瓶頸:edits 是 contextual 的——一個 op 的正確性取決於 當下文件狀態與套用順序。若以「最新 snapshot 蓋一切」會直接遺失其他人的修改。
- 為何不送整份 snapshot:每次 edit 都傳整份文件 → (1) 儲存爆炸(1000 次編輯 × 1KB ≈ 1GB/單文件);(2) 衝突解決失敗(只留「最後送達的 snapshot」會吃掉別人的改動)。故 只送 edits。
- OT(Operational Transformation):需要一個 central server 接收同文件所有 edits,先決定 唯一順序,再對 後到的 edit 做 transform 後套用。例:server 已套 A 的
INSERT(13,"ument"),B 原本的INSERT(13,"!")要被 transform 成INSERT(18,"!"),結果才一致為"This is a document!"。限制:同文件 edits 必須進同一台 server,對併發數有上限。Google Docs 採此法。 - CRDT(Conflict-free Replicated Data Types):特製資料結構,允許多 replica 本地獨立修改(甚至離線),只要互相同步 update 就 自動收斂(Strong Eventual Consistency)。代價:通常需 tombstones / 較多 metadata,且要等「所有 replica 都看過」才能安全 GC。
- 經驗法則:強離線 / 多主 / P2P / 邊緣節點多 / 偶爾連線 → 傾向 CRDT;有穩定中心服務、要可控文字意圖、極高併發 → 多採 OT 中心化(或 OT+CRDT hybrid)。
| 方案 | 定序方式 | 優點 | 缺點 |
|---|---|---|---|
| A. OT(中心化) | 單一 owner server 指派總序 | 極低延遲、決定性總序、metadata 少 | 同文件須進同一台 → 併發上限、無原生離線 |
| B. CRDT | 各 replica 本地收斂 | 離線 / 多主 / P2P 友善 | tombstone/metadata 多、GC 複雜、延遲不必然更低 |
選擇與理由:採 A(OT + 中心化序列器)。因為 (1) 單文件併發上限 ≤ 100 人,中心化序列器完全 hold 得住;(2) 極低輸入延遲——WebSocket 直達單一 owner,排序 + transform 後立即回送,省去 CRDT 的多主同步開銷;(3) 決定性總序——變更建議、註解錨點、樣式套用都仰賴穩定順序。這就是本題的 core_tradeoff:用中心化 OT 換低延遲與總序,放棄離線 / P2P。
→ 積木:02-Distributed-Systems/02-CAP-Theorem(接受 eventual consistency)、10-Design-Patterns/05-Real-time-Updates
因為 client 在收到 server 廣播前,已先樂觀套用了自己的本地 edit。設 A、B 都基於狀態 S1 送 edit;server 決定的順序是 Ea → Eb,但從 B 的視角,本地早已套了 Eb,server 又廣播來 Ea——B 必須對 Ea 再做一次 transform 才能維持與 server 一致的本地狀態。樂觀本地套用 = 低延遲手感的來源,但代價就是 client 端也要參與 OT。
8.2 如何擴展到數百萬個 WebSocket 連線
問題:單台 server 無法同時持有數百萬連線。必須把 Document Service 水平擴展到多台,每台負責 一部分文件與使用者。三個子問題:(1) 怎麼把 user 均分到各 server?(2) 連線時怎麼確保被正確導向?(3) server 增減時怎麼最小化重路由?
| 子問題 | 手段 | 為什麼 |
|---|---|---|
| 均分 + 增減最小擾動 | Consistent Hashing | hash(doc_id, healthy_nodes) 算 owner;節點增減只動相鄰區段 |
| 連線時正確導向 | Service Registry(Zookeeper) | 偵測節點上/下線、push ring_config 給 Gateway / servers |
| ownership 變更交接 | 每台維護 membership | owner 變動時做 文件 handoff(轉移該文件的序列責任) |
連線流程:(1) Registry 把 online nodes + ring_config 推給 Gateway/servers,雙方 本地 用 hash(doc_id, healthy_nodes) 算 owner;(2) client 發 HTTP 到 Gateway,Gateway 依 ownership 導向對應 doc server;(3) 連線升級為 WebSocket;(4) owner 從 operation store 載入 snapshot + ops 先傳給 client,再讓他開始編輯。
選擇與理由:為什麼一定要把同一文件釘在同一台——因為 OT 要求單一序列器;consistent hashing 正好保證 同 doc_id 永遠映到同一 owner,又能在擴縮容時把重路由壓到最低。Registry 背後用 quorum consensus 確保節點視圖強一致,避免兩台同時自認某文件 owner(雙序列器 = 一致性災難)。
→ 積木:02-Distributed-Systems/04-Consistent-Hashing、09-Messaging-Coord/05-Zookeeper、11-Infrastructure/01-API-Gateway
8.3 如何控制儲存成本(snapshot / compaction)
問題:數十億文件、單文件可累積 數百萬 ops。即使選 OT 已比 CRDT 省一個數量級(100KB/文件 → 100TB 總量),仍有兩個痛點:(1) 總量大;(2) 新加入的 client 必須接收並重播所有 ops,效能與體驗都不可接受。
核心想法:除版本管理需求外,不需永久保存所有 ops,可把多個 op 合併 成較少操作。例:insert("H") + insert("i") → insert("Hi")。在 server_seq = S 時,對「上次 snapshot 與 S 之間的 ops」做 compaction;完成後新 client 只需 snapshot@S + ops(S+1…head),不必重播完整歷史。舊 ops(≤ S)可移到 cold storage 或短期保留後 GC。
| 觸發條件 | 範例門檻 | 執行位置 |
|---|---|---|
| 每 N 個 ops | 2k–10k | online doc server:同 client 小批次每 50–100ms 輕量壓 |
| 每 T 分鐘 | 5–15 min | offline worker:週期性 durable snapshot + 負責 GC |
| delta 大小超門檻 | 1–4 MB | hybrid:兩者並用 |
| ownership handoff | owner 變更時順手 compact | online |
選擇與理由:採 hybrid——online server 對小批次做輕量 compaction(即時降低 tail 長度),offline worker 定期做 durable snapshot 並 GC(控制總量、把舊 ops 沉到 cold storage)。這同時解決「總儲存」與「新 client 載入延遲」兩個問題。
→ 積木:snapshot 沉冷見 07-Caching-Storage/02-Blob-Storage、寫入分片見 05-Database-Advanced/02-Sharding
8.4 游標 presence 與 viewer mode
問題 A(游標):協作者游標要即時顯示,但 (1) 只有「當下」位置重要;(2) 離線後不需回報;(3) latency 要極低。
選擇:純記憶體 ephemeral state——在線時每 100ms 把游標位置回報給 WebSocket server,server broadcast 給其他人;使用者離線時 直接從記憶體移除。完全不入 DB(持久化純屬浪費)。
問題 B(viewer mode):唯讀使用者 不上傳 edits,但仍需即時接收更新。若把 viewer 也塞進 edit owner,會與編輯者爭搶資源、且擴展受限。
| 手段 | 做什麼 | 為什麼 |
|---|---|---|
| 獨立 viewer cluster | 專給唯讀者的 Document Service 叢集 | viewer 不需與 editor 同 server → 可獨立擴展,互不干擾 |
| Kafka 解耦 | edit owner 持久化後把 ops 發到 durable queue,viewer-owner 消費 | 解耦(viewer 故障/backpressure 不影響 edit servers) |
| queue 有序 + 耐久 | viewer 看到的順序 = 持久化順序;重連可從 offset replay | viewer 不必做本地 OT,只需依序套用 ops |
| SSE 取代 WebSocket | viewer 只需 server→client 單向廣播 | SSE 開銷比 WebSocket 低,單節點可服務更多 client |
viewer 重連:client 提供最後看到的 resume_seq,server 回 + ops(S+1…head);若 gap 過大,直接回 snapshot@head + 一小段 tail ops。概念上 viewer 感覺像「只有一位作者在對他們廣播」。
→ 積木:09-Messaging-Coord/03-Kafka、12-Ops-and-Reliability/03-Reliable-Delivery、01-Networking/05-Realtime-Protocols
因為 viewer 不產生任何並發 edit——沒有「本地樂觀套用」與「server 順序」衝突的問題。它只是被動接收一條 已經定好序 的 op 流,依序套用即可。editor 則因樂觀套用了自己的本地 edit,才需要對 server 廣播來的 op 再 transform。少了寫入方,就少了衝突源。
9. 擴展與瓶頸 (Scaling & Bottlenecks ~3 min)
主動找出系統會先在哪裂開並補強(訓練 drive 而非被動回答)。
| 瓶頸 / 風險 | 補強手段 | 積木 |
|---|---|---|
| 單台 server 持不住數百萬連線 | consistent hashing 把文件均分多台;Gateway 依 ownership 路由 | 02-Distributed-Systems/04-Consistent-Hashing、11-Infrastructure/01-API-Gateway |
| owner server 掛掉 / 擴縮容 | Registry 偵測 → 重算 ring → 文件 handoff 到新 owner(load snapshot+ops 續上) | 09-Messaging-Coord/05-Zookeeper |
| 同文件單序列器成熱點(≤100 人但寫密集) | 單文件天花板由 OT 中心化決定;跨文件 分片 吸收總寫入 | 10-Design-Patterns/02-Scaling-Writes、05-Database-Advanced/02-Sharding |
| op 無限累積、新 client 重播全歷史 | snapshot/compaction + cold storage + GC | 07-Caching-Storage/02-Blob-Storage |
| viewer 暴增拖垮 edit servers | 獨立 viewer cluster + Kafka 解耦 + SSE | 09-Messaging-Coord/03-Kafka、12-Ops-and-Reliability/03-Reliable-Delivery |
| op store 寫入吞吐 | Cassandra(high write throughput):doc_id partition、ts sort | 06-Database-Tech/03-DynamoDB |
「系統會 先在連線層裂開(單台扛不住數百萬 WebSocket)→ 所以用 consistent hashing 把文件均分多台、Registry 管成員與 handoff;接著是 op 累積(單文件數百萬 ops)→ compaction 撐住。單文件併發雖被 OT 限在 ≤100,但這是刻意的設計取捨,不是 bug——用這上限換到低延遲與總序。」先指出最可能裂開處再補強,比逐項羅列更有說服力。
10. 取捨總表與收尾 (Trade-off Recap & Wrap-up ~2 min)
| 決策點 | 選擇 | 放棄了什麼 | 為什麼值得 |
|---|---|---|---|
| 衝突解決 | OT + 中心化序列器 | CRDT 的離線 / 多主 / P2P / 高併發 | 換極低延遲 + 決定性總序(招牌取捨,≤100 人夠用) |
| 傳輸內容 | 只送 edits(非 snapshot) | snapshot 的實作簡單 | 避免 GB 級爆炸與衝突遺失 |
| 即時通道 | WebSocket(viewer 用 SSE) | 輪詢的簡單 | 雙向低延遲;viewer 單向用 SSE 更省 |
| 文件路由 | Consistent Hashing + Registry | 簡單取模 hash | 同文件釘同 owner、擴縮容最小擾動 |
| op 儲存 | Cassandra(doc_id|ts)+ snapshot 沉冷 | 關聯式的 transaction 便利 | 高寫入吞吐;定序交給 server 而非 DB |
| op 生命週期 | snapshot/compaction + GC | 永久保存全歷史的簡單 | 控成本、新 client 免重播全歷史 |
| 游標 presence | 純記憶體 ephemeral | 持久化的「完整」 | 只關心當下、離線即丟,省儲存與延遲 |
| viewer fan-out | 獨立 cluster + Kafka 解耦 | 與 editor 共用 server 的簡單 | 互不干擾、可獨立擴展、有序可 replay |
「整題的單一錨點是 OT + 中心化序列器:因為單文件併發只有 ≤100 人,我刻意選中心化 OT 而非 CRDT,換到 每按鍵 50–100ms 的低延遲與決定性總序——這是核心取捨。其餘都是支撐這個決定的標準打法:WebSocket 直達 owner、consistent hashing 把文件釘到同一台、Cassandra 吸高寫入、compaction 控 op 累積、viewer 用 Kafka+SSE 解耦。若題目改成『要強離線 / P2P』,我才會重新評估 CRDT。」引用既有決策用連結,不重新推導。
後續追問演練 (Interviewer Follow-ups — "What if…")
因為單文件併發 ≤ 100 人,中心化序列器完全可行;OT 給 極低輸入延遲(WebSocket 直達單一 owner,排序+transform 立即回送)與 決定性總序(變更建議 / 註解錨點 / 樣式都靠穩定順序)。CRDT 的強項是離線 / 多主 / P2P,本題用不到,且它要 tombstone/較多 metadata、GC 複雜。Google Docs 即採 OT。見 02-Distributed-Systems/02-CAP-Theorem。
兩個原因:(1) 儲存爆炸——1000 次編輯 × 1KB/snapshot ≈ 1GB/單文件;(2) 衝突遺失——只留「最後送達的 snapshot」會蓋掉其他人的並發修改。故只送 edits,再用 OT 定序 transform。
因為 client 收到 server 廣播前已 樂觀套用了本地 edit。server 決定順序 Ea→Eb,但 B 本地早套了 Eb,再收到 Ea 時必須對它 transform 才能與 server 一致。樂觀本地套用是低延遲手感的來源,代價就是 client 也要參與 OT。
連線層先裂。依序:(1) 擴 Document Service 台數,consistent hashing 自動把更多文件分到新節點、重路由最小;(2) Registry(09-Messaging-Coord/05-Zookeeper)管成員與 handoff;(3) viewer 流量走 獨立 cluster + SSE(單節點服務更多 client);(4) Gateway 層本身也要水平擴展。單文件 ≤100 的上限不變(OT 設計使然)。見 02-Distributed-Systems/04-Consistent-Hashing。
編輯側上限 ≤100 不變;壓力在 op 累積 與 viewer fan-out。(1) 對該文件 積極 compaction,新進者只收 snapshot@head + tail ops,免重播百萬歷史;(2) viewer 走 Kafka 解耦 + 獨立 cluster + SSE,viewer 暴增不影響 edit servers,且可獨立擴;(3) 舊 ops 沉 cold storage。見 07-Caching-Storage/02-Blob-Storage、09-Messaging-Coord/03-Kafka。
Registry 偵測到節點下線 → 推新的 ring_config → consistent hashing 把該文件 重映到新 owner(文件 handoff)。新 owner 從 operation store 載入 snapshot + tail ops 重建狀態,client 重連續編。期間已持久化的 ops 不丟(耐久在 Cassandra)。見 09-Messaging-Coord/05-Zookeeper。
SSE 是 單向(server→client)。editor 必須 雙向:要 SEND insert/delete/updateCursor 給 server。viewer 只接收,故能用更省的 SSE。把兩者分流(editor=WebSocket、viewer=SSE)正是為了 讓單向廣播用更輕的協定服務更多 client。見 01-Networking/05-Realtime-Protocols。
op 是 極高吞吐的 append,Cassandra 針對 high write throughput 最佳化,doc_id partition + ts sort 天生契合「同文件 ops 聚一起且有序」。關鍵:強定序不靠 DB transaction,而是靠 server 序列器——同文件所有 edits 進同一台 owner,由它指派遞增 server_seq。DB 只負責耐久,定序在應用層。見 06-Database-Tech/03-DynamoDB、05-Database-Advanced/02-Sharding。
游標 只有當下重要、離線即無意義——持久化純屬浪費,放記憶體、離線即移除。op 是 文件內容的事實來源,必須耐久、定序、可重建(新 client / 重連都靠重放)。判準是 這份資料的歷史是否有價值:op 有(版本 / 收斂),游標沒有。見 10-Design-Patterns/05-Real-time-Updates。
當需求出現 強離線編輯 / 多主 / P2P / 邊緣節點多 / 偶爾才連線 時。例如行動端要離線改、無中心服務、或併發數遠超中心序列器負荷。屆時 CRDT 的本地獨立收斂(SEC)才划算,代價是 tombstone/metadata 與 GC 複雜度。本題有穩定中心服務 + ≤100 併發,故 OT 中心化勝出。
主動回想:白板重建 (Whiteboard from Scratch)
不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。
常見陷阱 (Exam Traps for This Problem)
What:client 每次 edit 都把整份 document 傳回 backend。
Why:(1) 儲存爆炸(1000×1KB ≈ 1GB/單文件);(2) 衝突遺失——只留最後送達的 snapshot 會蓋掉別人的並發修改。
正確做法:只送 edits(insert/delete),用 OT 定序 transform。見 10-Design-Patterns/05-Real-time-Updates。
What:一開口就「我們用 CRDT 自動收斂」。
Why:本題單文件 ≤100 人,中心化 OT 完全夠且延遲更低、metadata 更少;CRDT 的離線/P2P 強項本題用不到,反增 tombstone/GC 複雜度。
正確做法:先確認併發上限與離線需求,說明「≤100 + 有中心服務 → 選 OT 中心化」。見 02-Distributed-Systems/02-CAP-Theorem。
What:client 收到 server op 就直接 apply,不做本地 transform。
Why:client 已 樂觀套用本地 edit,與 server 順序衝突——直接 apply 會造成本地狀態漂移、最終不一致。
正確做法:client 端也要做 OT,對 server 廣播的 op 依本地已套順序再 transform。
What:每次游標移動都 INSERT/UPDATE DB。
Why:游標 只有當下重要、離線即無意義,高頻寫入(每 100ms)會無謂壓垮 DB。
正確做法:游標走 純記憶體 ephemeral,離線即移除;只有 op 才持久化。
What:server = hash(doc_id) % N 路由。
Why:server 一增減,N 改變 → 幾乎所有文件重映,大量連線被迫重連、owner 交接風暴。
正確做法:用 Consistent Hashing,增減節點只動相鄰區段,最小化重路由。見 02-Distributed-Systems/04-Consistent-Hashing。
What:新加入者從第一個 op 重放到最新。
Why:熱門文件可累積 數百萬 ops,重播延遲與頻寬不可接受,且總儲存爆炸。
正確做法:定期 snapshot/compaction,新 client 只收 snapshot + tail ops,舊 ops 沉 cold storage / GC。見 07-Caching-Storage/02-Blob-Storage。
Related Notes
- 02-Distributed-Systems/05-Numbers-to-Know
- 02-Distributed-Systems/02-CAP-Theorem
- 02-Distributed-Systems/04-Consistent-Hashing
- 01-Networking/05-Realtime-Protocols
- 10-Design-Patterns/05-Real-time-Updates
- 10-Design-Patterns/02-Scaling-Writes
- 09-Messaging-Coord/05-Zookeeper
- 09-Messaging-Coord/03-Kafka
- 11-Infrastructure/01-API-Gateway
- 06-Database-Tech/03-DynamoDB
- 05-Database-Advanced/02-Sharding
- 07-Caching-Storage/02-Blob-Storage
- 12-Ops-and-Reliability/03-Reliable-Delivery