經典架構拆解 · 06 — Discord 百萬人頻道與 NoSQL 選型
熱門 Discord 伺服器裡,成員可能同時聊天、搜尋舊訊息、加入語音。使用量成長後,訊息儲存、即時連線和語音服務會先後遇到不同瓶頸。Discord 的工程文章提供了觀察這些取捨的材料。
這是 經典工程架構拆解 系列的最後一篇,上一篇看了 Airbnb 的搜尋與金流。

訊息只保存一份 canonical copy,Gateway fanout 只處理自己持有的訂閱連線;主鍵必須配合「按頻道依時間讀取」的查詢,而 time bucket 會限制熱門分區大小。資料庫遷移先雙寫與驗證,再逐步切讀;回滾是穩定前保留的分支,不是必經步驟。語音的延遲問題則由最近的邊緣節點處理。
為什麼值得拆解
Discord 的工程部落格在資料庫、Rust、語音這三塊都寫過夠深的長文,而且難得的是,你看得出來他們每個技術決策背後都連著一個量得出來的問題——p99 延遲多少、每天寫入幾筆——不是看到別人用什麼就跟著用。對準備 PCA 的人來說這很重要:考試指南 Section 1.1 就把「設計決策的取捨」(Design decision trade-offs)列為考量項目。
Discord 身上最值得學的,是三個「什麼時候」:什麼時候該選 NoSQL、什麼時候該換掉現在這套 NoSQL、什麼時候該用系統程式語言把熱點重寫一遍。這三題都是架構設計時會反覆遇到的取捨。
商業規模與壓力
根據 Discord 官方與公開演講:
- 月活躍用戶(MAU)超過 2 億,同時在線超過 千萬量級(Discord 官方部落格 Discord for Business Newsletter)1。
- 截至 2022 年 Discord 每天寫入訊息超過 數十億則(Discord Engineering Blog “How Discord Stores Trillions of Messages”)2。
- 單一 server(channel)曾在 Midjourney 全盛期擁有 超過 1,900 萬成員(Midjourney 公開聲明與多家媒體報導)3。
大型頻道的難處在於,一則新訊息可能要同時送到大量在線成員手上,還得保存下來供日後搜尋。寫入速度、即時扇出、儲存成本和歷史查詢會互相牽制;後面談到的技術選型,都是在處理這幾個需求之間的取捨。
流程圖暫時無法顯示。可先查看下方的流程與節點文字,或重新整理頁面後再試。
查看流程與節點文字
箭頭表示流程方向;方括號與引號內是節點名稱,可搭配下方圖解閱讀。
sequenceDiagram participant U as Sender participant G as Gateway shard participant M as Message service/store participant E as Event bus participant S as Subscriber Gateway shards U->>G: send message G->>M: 驗證並寫入 canonical copy M-->>G: message_id+sequence G-->>U: ack M->>E: publish channel event E->>S: 只送到有訂閱者的 shards S-->>S: fanout 給各自維護的連線 Note over M,S: 不為每位成員複製一份訊息資料
架構演進簡史
| 階段 | 年份 | 關鍵變化 |
|---|---|---|
| MongoDB | 2015 | 初期單庫,遇到大量寫入後 lock contention 嚴重 |
| 遷移到 Cassandra | 2017 | 改用 Cassandra,以日期 bucket 當 partition key 2 |
| Rust 重寫 Read States | 2020 | 用 Rust 改寫「最後已讀」服務,移除 Go GC 抖動 4 |
| Cassandra → ScyllaDB | 2022 | 訊息儲存全面遷到 ScyllaDB,吞吐提升、延遲降低 2 |
| 持續 Rust 化 | 2023 以後 | 多個核心服務(包含訊息查詢)改寫 Rust |
表格超出欄寬時,可左右捲動;鍵盤可聚焦表格後用左右方向鍵查看。
把上面的取捨畫成一張圖,整個訊息與語音的骨架長這樣:
Discord 百萬人頻道:Gateway 訂閱後扇出,訊息存進依 channel+時間分片的 ScyllaDB,Read States 用 Rust,語音走全球邊緣節點。

這是便於理解的即時連線分片模型:穩定路由鍵把連線與會話分散到多個 Gateway shard,事件只回到有訂閱者的分片。用戶端若保有 sequence 與 session id,短暫斷線後便有機會續傳而非全量同步。
核心技術決策
| 決策 | 為何 | 替代方案 |
|---|---|---|
| 訊息 partition key = (channel_id, 時間 bucket) | 同 channel 訊息連續儲存,查歷史快 2 | 用 message_id(會跨 shard 查) |
| Cassandra → ScyllaDB | 同一張表、同一種 CQL,但 ScyllaDB 用 C++ 寫且 shard-per-core,延遲降 40%+ 2 | 自己調 Cassandra JVM(邊際效益低) |
| Read State 用 Rust | 原本用 Go,其 GC(每約 2 分鐘強制掃描)造成 p99 延遲尖峰,Rust 以所有權模型移除整個 GC 議題 4 | 維持 Go 但調 GC(治標不治本)、C++(開發成本高、記憶體安全風險) |
| 語音走全球邊緣節點 | 語音延遲敏感,近端 relay 比中央處理好 | 全部集中(跨洋延遲致命) |
| 訊息永久保留 | 社群文化需要翻舊訊息 | LRU 清除(會損害產品體驗) |
表格超出欄寬時,可左右捲動;鍵盤可聚焦表格後用左右方向鍵查看。
ScyllaDB 遷移的關鍵: Discord 不是因為 Cassandra「不行」才換的。是 Cassandra 的 JVM GC 造成的 p99 延遲尖峰,在他們這種規模下變成了主要瓶頸。而 ScyllaDB 的 API 幾乎一模一樣,遷移成本控制得住2。這種「保留介面、換掉底層實作」的思路,很值得記起來——它讓你可以分階段搬,而不是停機重來一次。
Partition key 看起來只是欄位順序,卻會影響讀寫落在哪些節點。以 IoT 時序資料為例,如果 key 只從時間戳開始,最新寫入可能集中在相近範圍,形成熱點;改用裝置 ID 等能分散寫入的前綴,還要確認常用查詢能否有效讀取。Discord 把 channel 與時間範圍納入 key,也是為了兼顧「讀某個 channel 的近期訊息」和分散負載。設計寬列式資料庫時,先列出常用查詢與寫入分布,再決定 key 的形狀。
流程圖暫時無法顯示。可先查看下方的流程與節點文字,或重新整理頁面後再試。
查看流程與節點文字
箭頭表示流程方向;方括號與引號內是節點名稱,可搭配下方圖解閱讀。
flowchart TB
Q["最高頻查詢<br/>讀某 channel<br/>最近 N 則訊息"] --> K["Row key 前綴<br/>channel_id+time_bucket"]
K --> W["新訊息寫入目前 bucket"]
K --> R["先掃最新 bucket"]
R --> E{"已取得足夠訊息?"}
E -->|"否"| O["往前掃較舊 bucket"]
O --> E
E -->|"是"| S["依時間順序回傳"]
B["Bucket 限制熱門<br/>partition 的大小"] -.-> W 
只用 channel_id 會讓熱門頻道永遠塞進同一個膨脹分區;再加時間
bucket,就能保留頻道內的時間順序,同時限制每個分區的大小。查詢最新訊息時由新往舊串接即可。
流程圖暫時無法顯示。可先查看下方的流程與節點文字,或重新整理頁面後再試。
查看流程與節點文字
箭頭表示流程方向;方括號與引號內是節點名稱,可搭配下方圖解閱讀。
flowchart TB
A["既有 Cassandra 讀寫"] --> B["建立相容 Schema<br/>並回填歷史資料"]
B --> C["Dual-write 新舊資料庫"]
C --> D["持續比對 row count、checksum、<br/>latency 與 error rate"]
D --> E{"資料與 SLO 達標?"}
E -->|"否"| F["修正並維持舊讀取路徑"]
F --> C
E -->|"是"| G["小比例切換讀流量"]
G --> H{"新系統穩定?"}
H -->|"否"| I["讀取回滾 Cassandra"]
I --> C
H -->|"是"| J["逐步擴大並最終停用舊系統"] 如果用 GCP 重新蓋
| Discord 元件 | GCP 對應 |
|---|---|
| 訊息儲存 | Bigtable(大規模 key-value、sparse wide-column,接近 Cassandra 模型)或自建 ScyllaDB on GKE |
| Read State / 已讀游標 | Firestore 或 Memorystore for Redis |
| WebSocket gateway | GKE 跑 Rust 服務 |
| 語音 relay 邊緣 | Cloud Run 多 region 部署 + Cloud Load Balancing(Anycast)或 GKE 加 NLB |
| 訊息搜尋 | Agent Search(前身為 Vertex AI Search)或 Elasticsearch on GKE,透過 Dataflow CDC 同步 |
| 媒體(附件、圖) | Cloud Storage + Cloud CDN |
| 分析 | BigQuery + Pub/Sub 串流事件 |
| 即時反詐欺 | Gemini Enterprise Agent Platform(機器人、spam 偵測即時推論) |
表格超出欄寬時,可左右捲動;鍵盤可聚焦表格後用左右方向鍵查看。
關鍵取捨: Google Cloud 沒有與 ScyllaDB 完全相同的託管服務。Bigtable 適合大量、以 row key 為主的讀寫,但不能直接把 Cassandra 的資料模型和查詢搬過去;單列交易限制與容量規劃也要先確認。如果需求包含跨列交易或跨區域的一致性,還得比較 Spanner 等選項。這時候成本與資料模型是否合適,比「哪個產品最像 ScyllaDB」更重要。
流程圖暫時無法顯示。可先查看下方的流程與節點文字,或重新整理頁面後再試。
查看流程與節點文字
箭頭表示流程方向;方括號與引號內是節點名稱,可搭配下方圖解閱讀。
flowchart TB
A["大量聊天/時序寫入"] --> B{"需要跨 row 交易<br/>或全球強一致?"}
B -->|"否"| C{"查詢是否以 key/<br/>range 為主,<br/>且要線性擴展?"}
C -->|"是"| D["Bigtable<br/>設計正確 Row Key"]
C -->|"否,文件與即時同步為主"| E["Firestore"]
B -->|"是"| F{"需要 SQL 與全球交易?"}
F -->|"是"| G["Spanner"]
F -->|"否,單區關聯式"| H["Cloud SQL"]
D --> I["用負載測試驗證 hotspot、latency 與容量"]
E --> I
G --> I
H --> I 考場上會怎麼考
Discord 的設計可以拿來練習三類問題:NoSQL 選型、全球低延遲部署,以及不停機的資料遷移。下面用幾個情境檢查該怎麼做取捨。
📝 考場提點:先看資料模型,再選服務
如果系統有大量寫入,資料按 key 或時間查詢,先確認讀寫模式、資料模型與一致性需求,再比較服務:
- 大量寬列或時間序列資料,以 key/range 查詢為主:評估 Bigtable,並測試 row key 是否造成熱點。
- 文件結構、行動應用即時同步:評估 Firestore,同時估算讀寫量與查詢成本。
- 需要 SQL 與多筆資料交易:比較 Cloud SQL、Spanner 的擴展需求、可用性和費用。
不要只憑「高吞吐」或「全球」選服務。若交易會跨多列、跨區域,先確認產品能否提供需要的一致性;若只是大量單列讀寫,就要測試吞吐、延遲與成本。資料規模和存取方式,比單一關鍵字更能決定選型。
📝 考場提點:全球低延遲 vs. 線上遷移
全球部署要先拆開看:哪些內容能放在邊緣快取、哪些請求必須回到後端,以及使用者能接受多少延遲。Global Load Balancing 搭配多區域後端,是需要跨區域服務使用者時可評估的架構;但是否能達到特定延遲目標,仍要實測網路路徑與後端處理時間。
線上遷移則要規劃新舊系統如何並行、如何核對資料,以及何時切換讀取流量。Dual-write 是其中一種做法,但會帶來雙邊寫入失敗與順序不一致的處理成本;選用前要設計驗證和回滾方式,不能只把「同時寫兩邊」當成零風險切換。
常見誤解
- 「Discord 換 ScyllaDB 是因為 Cassandra 不夠快。」 比較準確的說法是:在他們這種規模下,是 Cassandra 的 JVM GC 延遲尖峰變成了主要瓶頸,不是單純「速度不夠」。工程部落格原文強調的是 p99 延遲的改善2。
- 「Rust 一定比現有語言快,所以該全部改寫。」 Discord 只在延遲超敏感的熱點服務(Read States、Gateway)才改成 Rust(Read States 是從 Go 改寫過來的),其他地方還是維持 Python / Elixir。做法是「哪裡是瓶頸就動哪裡」,不是整套重寫。
- 「百萬人頻道,就是同時推百萬個 WebSocket。」 實際上走的是訂閱-扇出模型:Gateway server 訂閱 channel 事件,再把事件推給自己這邊連線的使用者。邊緣 server 不會直接拉百萬條連線互連。
來源與延伸閱讀
- How Discord Handles Two and Half Million Concurrent Voice Users — Discord Engineering Blog — 語音基礎架構與全球邊緣部署。
這是 經典工程架構拆解 系列的最後一篇。六家公司看下來,你大概會發現一件事:這些撐住超大規模的架構,講穿了都在同樣幾招上反覆下功夫——把資料切好(分片)、用事件把系統解耦、哪裡是瓶頸就只動哪裡、能保留介面就別整套重寫。沒有什麼神奇銀彈,就是把這幾個原則用在對的地方。
這幾招也對得上 PCA 考試指南列的設計取捨項目。想把它們套到自己的題目上,可以到 PCA 案例研究庫 對照 Google 官方 case study,或乾脆動手走一遍 架構師設計工作坊。
🎯 換你練習
想動手設計類似系統?到 架構師設計工作坊 用這套思考步驟走一遍。
Footnotes
-
Introducing the Discord for Business Newsletter, Vol. 1 — 官方部落格揭露 200M+ MAU 數據來源。 ↩
-
How Discord Stores Trillions of Messages — Discord Engineering Blog — 從 Cassandra 遷到 ScyllaDB 的完整工程記錄。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Midjourney Community Size — Midjourney 官方 Discord 與媒體報導 — Midjourney Discord server 規模公開資訊。 ↩
-
Why Discord is switching from Go to Rust — Discord Engineering Blog — Read States 服務為何從 Go 改用 Rust(解決 GC 抖動)。 ↩ ↩2
讀完後,留一個學習紀錄
試著用自己的話說出一個重點,以及你會如何運用。實作文章請先核對結果與清理資源,再標記為讀完。
文章進度以你確認讀完的標記為準,可隨時取消。舊版造訪紀錄保留,但不計入讀完;這份自評不代表已掌握內容。