免費完整指南

即時通訊系統

設計即時通訊系統。請涵蓋WebSocket connection management at scale: a connection registry mapping user/device to the gateway instance holding their socket, Message delivery semantics: at-least-once delivery with...

00

練習檢查點

面試節奏保持精簡,讓頁面能把注意力花在真正的設計決策上。

  1. 01
    釐清範圍
  2. 02
    需求 + 規模
  3. 03
    API + 資料模型
  4. 04
    畫出架構
  5. 05
    深入探討
  6. 06
    取捨決策
01

形塑設計的需求

不要只是陳述需求,要主動問出來。每張卡把設計限制和一句你可以在畫架構前說出口的釐清問句配成一組。

功能需求

01只有一對一聊天,還是也有群組——群組最大能到多大?

使用者在 1:1 和群組聊天中收發訊息。群組人數上限約 100 人,讓每則訊息的 fan-out 保持有界。

02我離線時送來的訊息會怎樣?

離線時送出的訊息,會在裝置重新連線時送達。它們最多保留 30 天。伺服器是一個帶有限緩衝的中繼站,不是永久封存。

03每位使用者一支手機,還是每台裝置都同步?

使用者可以附加媒體,而使用者擁有的每台裝置都保持同步——每台裝置各自追蹤自己的送達狀態。

04已讀回條與輸入中指示器——在範圍內嗎?

回條要:它們走的是跟訊息一樣的每裝置遞送路徑。輸入中的提示是暫時的。它們盡力而為,且從不儲存。

05使用者可以為所有人刪除一則已送出的訊息嗎?

為所有人刪除是一個 tombstone 事件,傳遞方式跟一般訊息一模一樣。已經顯示過該訊息的用戶端就地把它替換掉。編輯仍在範圍之外。

06有人在對話中途加入或離開群組——他們看到什麼?

加入或離開群組,是聊天裡一個有序的事件。加入的人從加入那一點開始收訊息。離開的人在離開事件處停止接收。沒有人會拿到殘缺的歷史。

範圍外語音與視訊通話 · 商業訊息與聊天機器人 · 註冊與個人檔案管理

非功能需求

01一則訊息要多快抵達一個在線的人?

端到端 500 ms 以內——聊天要嘛感覺即時,要嘛感覺壞掉。

02一則訊息有可能悄悄消失嗎?

不會。遞送性是有保證的。訊息在任何遞送嘗試之前就已持久寫入,所以斷線只會延誤它,永遠不會弄丟它。

03我們是為什麼規模設計?

200M 日活躍用戶、每人每天約 20 則訊息,任一時刻約有一半在線。

04伺服器保留訊息內容多久?

不會超過必要時間:未送達收件匣有 30 天 TTL(time-to-live),然後就沒了。

05一台聊天伺服器掛掉時會怎樣?

它的使用者重新連到另一台伺服器,並從 inbox 同步。遞送狀態住在儲存裡,不在伺服器的記憶體裡,所以不會有訊息遺失。

持續追問 —— 面試是一場對話

真實面試探得比一份整齊清單深得多。這些範圍問句,區分出真正拷問問題的人和只是背誦的人。

  • 群組大小上限——數百還是數千?fan-out 設計全看它。
  • 寄件者看到的 ack 是「已送達伺服器」還是「已送達裝置」?
  • 每個帳號要有幾台裝置保持同步?
  • 訊息歷史是永久封存,還是 30 天的中繼緩衝?
  • 端到端加密在範圍內嗎?它會改變伺服器能儲存什麼。
02

逼出架構決策的數字

把每個估算都當成一種壓力,用來合理化一個元件:快取、佇列、分片、副本、worker pool 或退路。

01

訊息寫入率

2 億活躍使用者 × 每人每日約 20 則訊息200M × 20 = 每天 4B 則;4B ÷ 86,400 秒 ≈ 每秒送出 46K 則;群組扇出 ×2-3 ≈ 每秒 100K 次寫入

Message 與 Inbox 需要為寫入最佳化的儲存(LSM log-structured merge tree/wide-column)。fan-out 的工作量隨參與者數目成長,這正是群組要設上限的原因。

02

同時在線連線數

200M 日活躍,任一時刻約一半在線200M × ~50% ≈ 1 億條開著的 WebSocket 連線

有了持久的 socket,決定 gateway 機隊規模的是連線數,而不是請求速率。

03

每個 gateway 的連線數

每台強力 gateway 機器約 100 萬條並發 WebSocket 連線100M 同時在線 ÷ 1M ≈ 100+ gateways

使用者雜湊到 gateway(consistent hashing + registry),讓訊息路由能找到正確的機器。

04

離線收件匣大小

30 天 TTL,每則訊息參照約 1KB即使 1,000 則未送達訊息 ≈ 每位休眠使用者 1 MB

TTL 讓這道後盾保持很小。長期休眠的裝置改從 Message store 重新同步它們的歷史。

決策範例

數字

每秒十萬次寫入、一億人同時在線,還有一個承諾:半秒以內,而且永遠不會悄悄弄丟任何東西。

我的選擇

讓 gateway 保持無狀態——它們只握著 socket 並轉發。每則訊息在任何推送之前,都先寫進 Message store 和每個收件人的 Inbox,所以沒有東西只活在記憶體裡。跨伺服器遞送搭在依 user ID 分區的 pub/sub 上,所以每個 gateway 只訂閱自己的使用者。順序以伺服器收到的時間為準;在聊天裡,快勝過完美排序。

避免

我不會做的事:把還沒遞送的訊息留在 gateway 記憶體裡。一次崩潰它們就沒了,這違背核心承諾。我也不會依 chat ID 來分區 pub/sub。一個熱鬧的群組就會變成熱分區,所以改成依使用者分區。而且我不會在量出真正的每分區訊息速率之前就加分片層。太早分片只會加故障模式,不會加容量。

何時改變

如果端對端加密進入範圍,伺服器就只能當一個對密文視而不見的路由器。Inbox 與多裝置同步這時要改成每裝置一份訊息副本,加上由用戶端持有的金鑰。

03

架構路徑

先看一張完整的圖,再把每條路徑各自畫成一張圖 —— 寫入路徑與讀取路徑承載不同流量,合理化不同的元件。

完整全貌

總覽 —— 每個元件

Client(WebSocket)ConnectionGatewayMessage ServiceMessage + InboxStoreRecipientGatewaysPub/Sub (peruser)路由到各 gatewayConnectionRegistryuser → gateway 查詢PushNotification離線裝置

先持久寫入,再遞送。gateway 是無狀態的 socket 持有者。registry 知道誰連在哪裡,離線裝置拿到的是推送,而不是一個 socket 訊框。

路徑 1

傳送路徑——持久優先,再 fan out

Sender ClientGatewayMessage ServiceMessage + InboxStorePub/Sub →Gateways

傳送者的 ack 在持久寫入之後才發出。接著 pub/sub 送達每個握有收件裝置的 gateway。一次漏掉的 publish 是安全的——inbox 裡已經有它了。

路徑 2

重新連線路徑——排空收件匣

Client(reconnect)GatewayInbox(undelivered)Message StoreClient synced

重新連線時,用戶端把自己的 inbox 抽乾,並比對序號。任何在傳輸途中漏掉的都會被拉回來,接著即時遞送恢復。

04

API 與資料模型

在最佳化之前,先讓契約可被檢視:端點、實體、擁有權、重試與狀態。

WSsendMessage { chat_id, body, attachments[] }

回應{ message_id, status } — ack after durable write

這個 ack 的意思是「已儲存」,不是「已送達」——持久性優先,送達其次。

WSnewMessage → client { chat_id, sender, body }

回應client replies RECEIVED

透過持久連線推送給每位參與者的每台在線裝置。

POST/attachments

請求{ body: presigned upload }

回應200 { attachment_id, url }

媒體經由 presigned URL 進到 blob 儲存;訊息只帶那個不透明的參照。

WSheartbeat every 10-30 s (piggybacks last sequence)

回應client compares and syncs gaps

心跳偵測死掉的連線,同時兼作缺口偵測的通道。

核心實體

Chat

chat_id (PK) · participants (≤100) · name

Message

message_id (PK) · chat_id · sender_id · body · attachments · server_ts

依伺服器收到的時間戳排序與顯示——使用者寧可快點看到訊息,也不要完美的送出順序。

Inbox

user_id (PK) · message_id · ttl_30d

持久性的後盾:在任何 push 嘗試之前就寫入,重新連線時排空。

Client

user_id (PK) · client_id · last_seen

一位使用者,數台裝置(上限約 3)——送達是依 client 追蹤,不是依 user。

05

深入方向

在面試最後三分之一挑一條路線。每條路線給你主題、它該回答的面試官問題,以及要避免的失敗模式。

重點

一億條開著的 socket

數億使用者如何持有持久連線,一則訊息又如何找到持有其收件者的那一個 gateway?

回答

每台 gateway 握約 1M 條 socket;registry 記錄 user → gateway 對應,以 user ID 分區的 pub/sub 只把訊息送到握著收件者的那台 gateway。

避免

把每則訊息廣播給每個 gateway——只讓 gateway 訂閱自己連上的使用者。

重點

收件者離線

訊息在哪裡等、等多久,他們重新連線的那一刻又會發生什麼?

回答

訊息在收件者的持久 inbox 裡等(30 天 TTL);重連時客戶端把積壓拉完,其間 push 通知服務會提醒閒置裝置。

避免

靠 pub/sub 來保證持久性——它是 at-most-once;收件匣寫入必須先發生。

重點

兩支手機,一個帳號

使用者在筆電上讀了一則訊息。他們的手機需要知道什麼,每裝置狀態又如何追蹤?

回答

投遞與已讀狀態按裝置追蹤、不是按使用者;每個裝置從自己的 cursor 同步,手機剛好拉到它還沒看過的部分。

避免

依 user 而非依 client 追蹤送達——第二台裝置會悄悄漏掉訊息。

重點

訊息亂序抵達

兩則訊息穿過不同 gateway 賽跑。收件者看到什麼順序,為什麼那可以接受?

回答

順序以每個對話的伺服器收件時間為準,客戶端照該時間戳渲染;gateway 之間的短暫競速實際上看不出來,為全域順序而阻塞反而付出使用者有感的延遲。

避免

為了強制全域順序而阻塞送達——使用者偏好快,勝過完美排序。

重點

一個 gateway 在對話中途掛掉

一萬名使用者同時掉線。他們失去什麼,又多快恢復完整?

回答

什麼都不會丟——未送達訊息存在持久 inbox、不在 gateway 記憶體。客戶端重連到別台 gateway,10-30 秒的心跳帶著最新序號,讓每個客戶端發現缺口並補拉。

避免

任何讓 gateway 記憶體是未送達訊息唯一副本的設計。

準備好練習了嗎?

把 即時通訊系統 大聲講一遍,讓 AI 為你的說明評分。

用 AI 練習這題 →