經典架構拆解 · 06 — Discord 百萬人頻道與 NoSQL 選型
Discord 不是最大的聊天平台,但它是單一 server 最多人同時在線的聊天平台。Midjourney 的 Discord server 一度超過千萬成員,這種規模,大多數系統設計題目根本沒想過。
這是 經典工程架構拆解 系列的最後一篇,上一篇看了 Airbnb 的搜尋與金流。
為什麼值得拆解
Discord 的工程部落格在資料庫、Rust、語音這三塊都寫過夠深的長文,而且難得的是,你看得出來他們每個技術決策背後都連著一個量得出來的問題——p99 延遲多少、每天寫入幾筆——不是看到別人用什麼就跟著用。對 PCA 考生來說這很重要,因為考試考的就是這種「為什麼是這個選項,不是那個」的判斷。
Discord 身上最值得學的,是三個「什麼時候」:什麼時候該選 NoSQL、什麼時候該換掉現在這套 NoSQL、什麼時候該用系統程式語言把熱點重寫一遍。這三題在 PCA 考場上都會以不同包裝出現。
商業規模與壓力
根據 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。
壓力在哪?想像一下:單一 channel 只要有一個人發一句話,系統就得即時把它扇出(fan-out)到幾十萬個在線成員手上。而且這則訊息不能發完就丟,要永久保留、還要能往回搜尋歷史。寫入要快、扇出要即時、儲存要省、查詢要不卡——這四件事很難同時做到,後面所有的技術選型基本上都在這四個拉扯之間找平衡。
架構演進簡史
| 階段 | 年份 | 關鍵變化 |
|---|---|---|
| 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,語音走全球邊緣節點。
核心技術決策
| 決策 | 為何 | 替代方案 |
|---|---|---|
| 訊息 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 設計(用 channel_id + 時間 bucket)看起來只是一行小決定,實際上它幾乎決定了整個系統能不能撐住。我以前做過一個量級小得多的 IoT 專案,一開始圖方便把裝置時序資料的 row key 設成 timestamp 開頭,結果所有最新寫入全部擠到同一個 partition——也就是大家常說的 hotspot(熱點),讀寫全卡在那一條 range 上。後來把 key 換成 device_id 開頭、時間放後面,寫入才平均散開。Discord 那個 key 的精髓也在這:把同一個 channel 的訊息排在一起連續存,查歷史一次掃一段就好,又不會讓單一 partition 過熱。在寬列式(wide-column)NoSQL 裡,你不是先設計資料表、再想怎麼查;而是反過來,先想清楚最高頻的查詢長怎樣,再倒推 row key。 這個順序搞錯,後面再多硬體也救不回來。
如果用 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 偵測即時推論) |
關鍵取捨: GCP 沒有完全對應 ScyllaDB 的 managed 服務,最接近的就是 Bigtable(sparse columns、wide rows、LSM tree,骨子裡跟 Cassandra 模型很像)。但別急著畫上等號——Bigtable 有兩個限制你得先確認:單 cluster 的 node 數上限,還有它只保證 single-row transaction(跨 row 的原子操作它不做)。如果你的場景需要跨 region 的強一致,那 Bigtable 就不夠了,得改用 Spanner——但 Spanner 的帳單會比 Bigtable 貴上一截,這筆錢值不值得花,要看一致性對你的業務到底有多硬性。考試很愛在這個分岔上設陷阱,後面考場提點會講。
考場上會怎麼考
Discord 這篇拆下來,正好對上 PCA 三個高頻考點:NoSQL 選型、全球低延遲部署、線上遷移。我把它們整理成可以直接帶進考場的提點。
📝 考場提點:NoSQL 選型,看關鍵字反射服務
這類題的標準包裝是給你一個「每秒百萬級寫入、資料是 key-value 結構、要按時間排序、量會一直長」的場景,然後問你選哪個資料庫。心裡先建好這張反射表:
- 超大量、寬列、時間序列、要線性擴展 → Bigtable。看到「IoT telemetry」「點擊流」「聊天訊息」「每秒幾十萬寫入」這幾個詞,幾乎都是它。
- 行動 App、即時同步、文件結構、規模中等 → Firestore。規模一旦上到 Discord 這種等級,Firestore 就不是答案了。
- 需要 SQL、需要交易、是關聯式資料 → Cloud SQL 或 Spanner。把訊息硬塞進關聯式資料庫,是出題者放的誘餌。
最常見的陷阱有兩個。一個是用 Firestore 接超大寫入量——選項看起來很合理,但規模對不上。另一個更陰:題目裡偷偷塞一句「需要跨 region 強一致」,這時答案就從 Bigtable 變成 Spanner。只要看到「全球強一致」「跨區交易」這種字眼,先把 Bigtable 從候選裡劃掉。反過來,如果題目只說「高吞吐、低延遲」沒提強一致,你卻選了 Spanner,那就是花冤枉錢的過度設計,一樣會被扣分。
📝 考場提點:全球低延遲 vs. 線上遷移
全球部署題通常長這樣:「使用者分佈在亞洲、歐洲、美洲,怎麼確保延遲 < 100ms?」——對應 Discord 語音走邊緣節點的做法。關鍵字反射是 global load balancing + 多 region 部署:用 Cloud Load Balancing 的 Anycast 把流量導到最近的 region,後端用 multi-region Cloud Run 或 GKE。看到「全球使用者」「延遲敏感」就往這個組合想,別掉進「集中在單一 region 再開大頻寬」的坑。
遷移題則對應 Cassandra → ScyllaDB 那種「保留介面、抽換底層」。PCA 在 Altostrat Media(串流媒體平台遷移)、KnightMotives Automotive(telemetry / IoT 資料管線遷移)這類 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 考你的東西。想把它們套到自己的題目上,可以到 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