需求與目標#
設計一個類似 Facebook Messenger 的即時通訊服務,讓使用者能夠透過文字訊息進行一對一的對話。
功能性需求#
- 支援使用者之間的一對一對話
- 追蹤使用者的上線/離線狀態
- 支援聊天記錄的持久化儲存
非功能性需求#
- 低延遲:使用者應有即時聊天體驗
- 高一致性:使用者在所有裝置上應能看到相同的聊天記錄
- 可用性可妥協:為了一致性,可以容忍較低的可用性
延伸需求#
- 群組聊天:支援多人在群組中對話
- 推播通知:當使用者離線時,通知他們收到新訊息
容量估算與限制#
假設我們有 5 億每日活躍使用者,平均每位使用者每天傳送 40 則訊息,這表示每天有 200 億則訊息。
儲存估算#
- 假設每則訊息平均 100 bytes
- 每日儲存量:
200 億 * 100 bytes = 2 TB/天 - 五年儲存量:
2 TB * 365 天 * 5 年 ≈ 3.6 PB
以上計算未考慮使用者資訊、訊息 metadata(ID、Timestamp 等),也未納入資料壓縮與副本的影響。
頻寬估算#
- 每日 2 TB 資料量,換算為 25 MB/s 的傳入流量
- 由於每則傳入訊息都需要傳出給另一位使用者,傳出頻寬同樣為 25 MB/s
高層級估算摘要#
| 指標 | 數值 |
|---|---|
| 每日訊息總量 | 200 億 |
| 每日儲存量 | 2 TB |
| 五年儲存量 | 3.6 PB |
| 傳入頻寬 | 25 MB/s |
| 傳出頻寬 | 25 MB/s |
高層級設計#
在高層級架構中,我們需要一個 Chat Server 作為所有使用者通訊的核心中介。當使用者想要傳送訊息給另一位使用者時:
- User-A 透過 Chat Server 傳送訊息給 User-B
- Server 收到訊息後,回傳**確認(acknowledgment)**給 User-A
- Server 將訊息儲存至資料庫,並傳送給 User-B
- User-B 收到訊息後,回傳確認給 Server
- Server 通知 User-A 訊息已成功送達 User-B

圖 16.1:基本訊息流程圖(User 1 ↔ Chat Server ↔ Data Storage ↔ User 2)
詳細元件設計#
在高層級架構下,系統需要處理以下三個核心場景:
- 接收與傳遞訊息
- 從資料庫儲存與擷取訊息
- 管理使用者的上線/離線狀態,並通知相關使用者
訊息處理#
使用者傳送訊息需要連線到 Server 並發佈訊息,而接收訊息有兩種模式:
- Pull 模式:使用者定期向 Server 查詢是否有新訊息
- Push 模式:使用者與 Server 保持連線,Server 在有新訊息時主動通知
Pull 模式效率低下——使用者需要頻繁輪詢,大多數時候會得到空回應,浪費大量資源。Push 模式是更佳的選擇,能以最低延遲即時傳遞訊息。
如何維持開放連線#
可以使用 HTTP Long Polling 或 WebSockets:
- Long Polling:客戶端發送請求,Server 若無新資料則保持請求開啟,直到有新訊息才回應。收到回應後,客戶端立即發起新的請求
- 此機制大幅提升了延遲、吞吐量與效能
- 當請求逾時或斷線時,客戶端需重新建立連線
Server 如何追蹤連線#
Server 維護一個 Hash Table:
- Key:UserID
- Value:連線物件(connection object)
當 Server 收到給某位使用者的訊息時,透過 Hash Table 查找連線物件,直接在開放的連線上傳送訊息。
離線使用者的處理#
當接收者離線時:
- Server 通知傳送者送達失敗
- 若為暫時斷線(例如 Long Poll 逾時),預期使用者會很快重新連線
- 可在客戶端邏輯中嵌入自動重試機制,使用者無需重新輸入訊息
- Server 也可暫存訊息,待接收者重新連線後重新傳送
伺服器數量規劃#
- 預計同時有 5 億連線
- 假設一台現代伺服器可處理 5 萬並行連線
- 需要約 1 萬台 Chat Server
訊息路由#
在 Chat Server 前方加入 Software Load Balancer,將每個 UserID 對應到持有該使用者連線的伺服器。
訊息送達流程#
Server 收到新訊息後需執行以下步驟:
- 儲存訊息至資料庫
- 傳送訊息給接收者
- 回傳確認給傳送者
不需要等資料庫儲存完成才回傳確認——儲存可以在背景執行。Chat Server 先找到持有接收者連線的伺服器,將訊息傳遞過去,然後即可回傳確認給傳送者。
訊息排序#
使用 Server 時間戳無法保證正確的訊息順序。考慮以下情境:
- User-1 傳送訊息 M1 給 User-2
- Server 在時間 T1 收到 M1
- 同時,User-2 傳送訊息 M2 給 User-1
- Server 在時間 T2 收到 M2(T2 > T1)
- Server 將 M1 傳給 User-2,M2 傳給 User-1
結果 User-1 看到 M1 在前、M2 在後,但 User-2 看到 M2 在前、M1 在後。
解決方案是為每個客戶端的每則訊息維護一個 Sequence Number。此序號決定了每位使用者的精確訊息順序。雖然兩個客戶端看到的訊息順序可能不同,但每位使用者在所有裝置上的視圖是一致的。
資料庫儲存與擷取#
當 Chat Server 收到新訊息時,有兩種儲存方式:
- 啟動獨立執行緒與資料庫互動
- 傳送非同步請求給資料庫
設計資料庫時需考慮:
- 如何有效管理資料庫連線池
- 如何重試失敗的請求
- 在多次重試仍失敗後,如何記錄這些請求
- 問題解決後,如何重試這些記錄中的失敗請求
儲存系統選擇#
系統需要一個能支援高頻率小型更新且能快速範圍查詢的資料庫。傳統 RDBMS(如 MySQL)或一般 NoSQL(如 MongoDB)無法勝任——每次收發訊息都讀寫一行資料會造成高延遲與巨大負載。
HBase 是理想的選擇。它是一個 Column-oriented Key-Value NoSQL 資料庫,模仿 Google BigTable,運行於 HDFS 之上。HBase 將資料先寫入記憶體緩衝區,緩衝區滿後才寫入磁碟,這種方式非常適合快速儲存大量小資料以及按 Key 或範圍掃描讀取。
分頁讀取#
客戶端應使用**分頁(pagination)**方式從 Server 讀取資料。不同裝置的頁面大小可以不同——例如手機螢幕較小,需要較少的訊息數量。
使用者狀態管理#
由於 Server 為所有活躍使用者維護了連線物件,可以輕鬆判斷使用者的線上狀態。但對 5 億活躍使用者廣播每次狀態變更會消耗大量資源,因此需要以下優化策略:
- 客戶端啟動時,拉取好友列表中所有使用者的當前狀態
- 當使用者傳訊給已離線的對象時,傳送失敗通知並在客戶端更新狀態
- 使用者上線時,Server 延遲數秒才廣播狀態,以確認使用者不會立即離線
- 客戶端可以為視窗中可見的使用者向 Server 拉取狀態(低頻率操作)
- 當使用者開始新對話時,即時拉取對方的狀態

圖 16.2:詳細系統架構圖(Users → Load Balancer → Chat Servers → DB Shards + Cache)
設計摘要#
- 客戶端與 Chat Server 建立連線以傳送訊息,Server 將訊息轉發給目標使用者
- 所有活躍使用者與 Server 保持開放連線以接收訊息
- 新訊息到達時,Chat Server 透過 Long Poll 請求推送給接收者
- 訊息儲存於 HBase,支援快速小型更新與範圍搜尋
- Server 可向相關使用者廣播上線狀態
- 客戶端以較低頻率拉取視窗中可見使用者的狀態更新
資料分區#
五年需儲存 3.6 PB 的資料,必須分散到多台資料庫伺服器。
基於 UserID 的分區#
- 以 UserID 的 Hash 值進行分區,將同一使用者的所有訊息存放在同一台資料庫
- 若每個 DB Shard 為 4 TB,五年需要
3.6 PB / 4 TB ≈ 900個 Shard - 簡化為 1,000 個 Shard,分區公式:
hash(UserID) % 1000 - 此方案可以非常快速地擷取任何使用者的聊天記錄
初期可以用較少的實體伺服器,每台伺服器上運行多個 DB 實例(多個邏輯分區)。隨著儲存需求增長,再新增實體伺服器來分散邏輯分區。
基於 MessageID 的分區#
若將同一使用者的不同訊息存放在不同的 Shard 上,擷取某段對話的範圍訊息會非常緩慢,因此不應採用此方案。
快取#
可以快取每位使用者最近幾次對話(約 5 個)中的最新幾則訊息(約 15 則)。由於同一使用者的所有訊息都在同一個 Shard 上,該使用者的快取也應完全存放在同一台機器上。
負載平衡#
- 在 Chat Server 前方放置 Load Balancer,將每個 UserID 導向持有其連線的伺服器
- Cache Server 前方也需要 Load Balancer
容錯與副本#
Chat Server 故障處理#
當 Chat Server 故障時,要將 TCP 連線轉移到其他伺服器是非常困難的。更簡單的方式是讓客戶端在連線中斷時自動重連。
資料副本#
不能只保留一份使用者資料——若持有資料的伺服器永久故障,將無法恢復資料。解決方案:
- 將資料的多份副本儲存在不同的伺服器上
- 或使用 Reed-Solomon 編碼等技術進行分散式副本
延伸需求#
群組聊天#
- 系統中建立獨立的 Group Chat 物件,以 GroupChatID 識別,並維護成員清單
- Load Balancer 根據 GroupChatID 導向處理該群組的伺服器
- 伺服器遍歷群組中所有使用者,找到各使用者的連線伺服器來傳遞訊息
- 資料庫中以獨立的表格儲存群組聊天,基於 GroupChatID 分區
推播通知#
在目前的設計中,只能傳送訊息給線上使用者。推播通知可將訊息傳送給離線使用者:
- 使用者可在裝置或瀏覽器上訂閱通知
- 系統需建立 Notification Server,接收離線使用者的訊息
- Notification Server 將訊息傳送給各設備製造商的推播通知服務(如 APNs、FCM)
- 推播服務再將通知傳送到使用者的裝置