經典架構拆解 · 06 — Discord 百萬人頻道與 NoSQL 選型
Discord 不是最大的聊天平台,但它是單一 server 最多人同時在線的聊天平台。Midjourney 的 Discord server 一度超過千萬成員,這種規模,大多數系統設計題目根本沒想過。
這是 經典工程架構拆解 系列的最後一篇,上一篇看了 Airbnb 的搜尋與金流。
為什麼值得拆解
Discord 寫過很多架構文章(Engineering Blog 在資料庫、Rust、語音三塊都有夠深的長文),而且看得出來他們的技術決策都是「為了解決某個量得出來的問題」,不是跟風。
對 PCA 考生來說,Discord 最值得學的就是:什麼時候該選 NoSQL、什麼時候該換掉現在這套 NoSQL、什麼時候該用系統程式語言重寫。
商業規模與壓力
根據 Discord 官方與公開演講:
- 月活躍用戶(MAU)超過 1.5 億,同時在線超過 千萬量級(Discord 投資人資料與官方部落格)1。
- 截至 2022 年 Discord 每天寫入訊息超過 數十億則(Discord Engineering Blog “How Discord Stores Trillions of Messages”)2。
- 單一 server(channel)曾在 Midjourney 全盛期擁有 超過 1,900 萬成員(Midjourney 公開聲明與多家媒體報導)3。
壓力在哪?單一 channel 只要有 1 個人發訊息,就得即時送到幾十萬個在線成員手上;而且訊息要永久保留,還要能搜尋歷史。
架構演進簡史
| 階段 | 年份 | 關鍵變化 |
|---|---|---|
| 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 |
核心技術決策
| 決策 | 為何 | 替代方案 |
|---|---|---|
| 訊息 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。這種「保留介面、換掉底層實作」的做法,很值得記起來。
如果用 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 |
| 訊息搜尋 | Vertex AI Search 或 Elasticsearch on GKE,透過 Dataflow CDC 同步 |
| 媒體(附件、圖) | Cloud Storage + Cloud CDN |
| 分析 | BigQuery + Pub/Sub 串流事件 |
| 即時反詐欺 | Vertex AI(機器人、spam 偵測) |
關鍵取捨: GCP 沒有完全對應 ScyllaDB 的 managed 服務,最接近的就是 Bigtable(sparse columns、wide rows、LSM tree)。不過 Bigtable 單 cluster 最大 instance 數,還有 single-row transaction 的限制,這兩點要先確認清楚。如果跨 region 要強一致,那就選 Spanner,但成本比 Bigtable 貴很多。
PCA 考點映射
- NoSQL 選型:PCA 會丟給你一個「每秒百萬寫入、訊息是 key-value 結構、要按時間排序」的場景。答案不是 Firestore(規模不夠),也不是 Cloud SQL(這不是關聯式的用法),而是 Bigtable。考的就是你懂不懂 Bigtable 的 row key 設計跟 schema-less 特性。
- 全球部署與延遲:語音服務的邊緣部署,對應的是 PCA「global load balancing + multi-region Cloud Run」這個考點。題目會問「使用者分佈在亞洲、歐洲、美洲,怎麼確保語音延遲 < 100ms」。
- 遷移策略:Cassandra → ScyllaDB 就是經典的「保留介面、抽換實作」案例。PCA 在 Mountkirk Games、TerramEarth 這些 case study 都出過類似的遷移題,重點是先做 dual-write,再切讀流量,而且要留好回滾路徑(rollback path)。
常見誤解
- 「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 案例資料庫 對照 Google 官方 case study,或乾脆動手走一遍 架構師設計工作坊。
🎯 換你練習
想動手設計類似系統?到 架構師設計工作坊 用這套思考步驟走一遍。
Footnotes
-
Discord Official Newsroom — 官方揭露的 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