Google Docs 協作編輯 (Design Google Docs)

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

設計一個讓 多人即時協作編輯同一份文件 的系統。核心難點不是 CRUD,而是 並發編輯的衝突解決:A、B 同時在同一位置插字,如何讓所有人 最終看到一致的內容,且每次按鍵都 < 100ms 回饋。
招牌取捨:採 Operational Transformation(OT)+ 中心化單一序列器(owner),而非 CRDT。OT 讓同一份文件的所有 edits 都打到 同一台 server 排序、transform 後立即回送——換來 極低輸入延遲決定性總序,代價是放棄 CRDT 的離線 / 多主 / P2P 能力,且 單文件併發上限受限(本題 ≤ 100 人)。其餘決勝點:WebSocket 雙向即時通道Consistent Hashing + Service Registry 把文件路由到 owner serversnapshot / 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)
defer 話術

OT 的轉換函式內部數學 我先標記,等到 deep dive 再展開——OT / CRDT 的演算法細節超出 1 小時 SD 面試範圍,現在先當成一個『能把並發 edits 排成一致順序』的 black box,讓我們先把端到端流程串起來。」這招把演算法細節推遲,保住架構敘事的連貫性。

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

設計北極星(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 是 高吞吐 appenddoc_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)

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

通用機制(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,再比 OT / CRDT(本案例首次 introduce,尚無 atomic note → 已登記 new_concepts_introduced)

  • 為何不送整份 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

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-Hashing09-Messaging-Coord/05-Zookeeper11-Infrastructure/01-API-Gateway

8.3 如何控制儲存成本(snapshot / compaction)

問題:數十億文件、單文件可累積 數百萬 ops。即使選 OT 已比 CRDT 省一個數量級(100KB/文件 → 100TB 總量),仍有兩個痛點:(1) 總量大;(2) 新加入的 client 必須接收並重播所有 ops,效能與體驗都不可接受。

新材料:snapshot + compaction(已登記 new_concepts_introduced)

核心想法:除版本管理需求外,不需永久保存所有 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-Kafka12-Ops-and-Reliability/03-Reliable-Delivery01-Networking/05-Realtime-Protocols

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

主動找出系統會先在哪裂開並補強(訓練 drive 而非被動回答)。

瓶頸 / 風險 補強手段 積木
單台 server 持不住數百萬連線 consistent hashing 把文件均分多台;Gateway 依 ownership 路由 02-Distributed-Systems/04-Consistent-Hashing11-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-Writes05-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-Kafka12-Ops-and-Reliability/03-Reliable-Delivery
op store 寫入吞吐 Cassandra(high write throughput):doc_id partition、ts sort 06-Database-Tech/03-DynamoDB
drive 的關鍵句

「系統會 先在連線層裂開(單台扛不住數百萬 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…")

為什麼選 OT 而不是 CRDT?

為什麼不每次都送整份文件 snapshot?

為什麼 client 端「也」要做 OT?

連線數放大 100x(數億 WebSocket),架構哪裡先撐不住?怎麼補?

某份文件突然爆紅(數百萬 ops、大量 viewer),怎麼辦?

owner server 突然掛掉,正在編輯的人怎麼辦?

既然 viewer 用 SSE,為什麼 editor 不也用 SSE 省資源?

op store 為什麼選 Cassandra 而不是關聯式 DB?強定序不是該靠 transaction 嗎?

為什麼游標 presence 不持久化,op 卻要?兩者差在哪?

什麼情況你會改用 CRDT 或 OT+CRDT hybrid?

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

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

常見陷阱 (Exam Traps for This Problem)