需求與目標#

設計一個類似 Facebook Messenger 的即時通訊服務,讓使用者能夠透過文字訊息進行一對一的對話。

功能性需求#

  1. 支援使用者之間的一對一對話
  2. 追蹤使用者的上線/離線狀態
  3. 支援聊天記錄的持久化儲存

非功能性需求#

  • 低延遲:使用者應有即時聊天體驗
  • 高一致性:使用者在所有裝置上應能看到相同的聊天記錄
  • 可用性可妥協:為了一致性,可以容忍較低的可用性

延伸需求#

  • 群組聊天:支援多人在群組中對話
  • 推播通知:當使用者離線時,通知他們收到新訊息

容量估算與限制#

假設我們有 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 作為所有使用者通訊的核心中介。當使用者想要傳送訊息給另一位使用者時:

  1. User-A 透過 Chat Server 傳送訊息給 User-B
  2. Server 收到訊息後,回傳**確認(acknowledgment)**給 User-A
  3. Server 將訊息儲存至資料庫,並傳送給 User-B
  4. User-B 收到訊息後,回傳確認給 Server
  5. Server 通知 User-A 訊息已成功送達 User-B

圖 16.1:基本訊息流程圖(User 1 ↔ Chat Server ↔ Data Storage ↔ User 2)

詳細元件設計#

在高層級架構下,系統需要處理以下三個核心場景:

  1. 接收與傳遞訊息
  2. 從資料庫儲存與擷取訊息
  3. 管理使用者的上線/離線狀態,並通知相關使用者

訊息處理#

使用者傳送訊息需要連線到 Server 並發佈訊息,而接收訊息有兩種模式:

  • Pull 模式:使用者定期向 Server 查詢是否有新訊息
  • Push 模式:使用者與 Server 保持連線,Server 在有新訊息時主動通知

Pull 模式效率低下——使用者需要頻繁輪詢,大多數時候會得到空回應,浪費大量資源。Push 模式是更佳的選擇,能以最低延遲即時傳遞訊息。

如何維持開放連線#

可以使用 HTTP Long PollingWebSockets

  • 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 收到新訊息後需執行以下步驟:

  1. 儲存訊息至資料庫
  2. 傳送訊息給接收者
  3. 回傳確認給傳送者

不需要等資料庫儲存完成才回傳確認——儲存可以在背景執行。Chat Server 先找到持有接收者連線的伺服器,將訊息傳遞過去,然後即可回傳確認給傳送者。

訊息排序#

使用 Server 時間戳無法保證正確的訊息順序。考慮以下情境:

  1. User-1 傳送訊息 M1 給 User-2
  2. Server 在時間 T1 收到 M1
  3. 同時,User-2 傳送訊息 M2 給 User-1
  4. Server 在時間 T2 收到 M2(T2 > T1)
  5. Server 將 M1 傳給 User-2,M2 傳給 User-1

結果 User-1 看到 M1 在前、M2 在後,但 User-2 看到 M2 在前、M1 在後。

解決方案是為每個客戶端的每則訊息維護一個 Sequence Number。此序號決定了每位使用者的精確訊息順序。雖然兩個客戶端看到的訊息順序可能不同,但每位使用者在所有裝置上的視圖是一致的

資料庫儲存與擷取#

當 Chat Server 收到新訊息時,有兩種儲存方式:

  1. 啟動獨立執行緒與資料庫互動
  2. 傳送非同步請求給資料庫

設計資料庫時需考慮:

  • 如何有效管理資料庫連線池
  • 如何重試失敗的請求
  • 在多次重試仍失敗後,如何記錄這些請求
  • 問題解決後,如何重試這些記錄中的失敗請求

儲存系統選擇#

系統需要一個能支援高頻率小型更新且能快速範圍查詢的資料庫。傳統 RDBMS(如 MySQL)或一般 NoSQL(如 MongoDB)無法勝任——每次收發訊息都讀寫一行資料會造成高延遲與巨大負載。

HBase 是理想的選擇。它是一個 Column-oriented Key-Value NoSQL 資料庫,模仿 Google BigTable,運行於 HDFS 之上。HBase 將資料先寫入記憶體緩衝區,緩衝區滿後才寫入磁碟,這種方式非常適合快速儲存大量小資料以及按 Key 或範圍掃描讀取

分頁讀取#

客戶端應使用**分頁(pagination)**方式從 Server 讀取資料。不同裝置的頁面大小可以不同——例如手機螢幕較小,需要較少的訊息數量。

使用者狀態管理#

由於 Server 為所有活躍使用者維護了連線物件,可以輕鬆判斷使用者的線上狀態。但對 5 億活躍使用者廣播每次狀態變更會消耗大量資源,因此需要以下優化策略:

  1. 客戶端啟動時,拉取好友列表中所有使用者的當前狀態
  2. 當使用者傳訊給已離線的對象時,傳送失敗通知並在客戶端更新狀態
  3. 使用者上線時,Server 延遲數秒才廣播狀態,以確認使用者不會立即離線
  4. 客戶端可以為視窗中可見的使用者向 Server 拉取狀態(低頻率操作)
  5. 當使用者開始新對話時,即時拉取對方的狀態

圖 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)
  • 推播服務再將通知傳送到使用者的裝置