題幹與適用場景
設計一個文字聊天服務:日活躍使用者 5000 萬、尖峰並行連線 500 萬、每天訊息 20 億則。它支援私聊、最多 200 人的群聊、每位使用者多台裝置、離線補齊、送達與已讀回執、線上狀態和正在輸入提示。附件、公開頻道、跨地區雙活寫入、全文搜尋和端對端加密留作追問範圍。
伺服器只在訊息持久化以後確認傳送成功。任何已確認訊息都不得靜默遺失。對於正常負載下已經在線上的接收裝置,從持久化接收到到達裝置的 p99 應低於 1 秒。傳輸層可以重複投遞,但用戶端去重後只能看到一則邏輯訊息。只要求同一會話內有序,不要求互不相關的會話共享全域順序。
三篇在 2026 年發布或更新的公開系統設計指南都直接以聊天系統為題,並反覆考察長連線、連線路由、會話內順序、離線同步、回執和線上狀態。WebSocket 標準定義雙向傳輸和控制訊框,Matrix 用戶端—伺服器規範則給出用戶端交易 ID、增量同步權杖、已讀回執和短暫輸入事件的正式實例。這些資料證明題型與故障邊界具有當前代表性和技術依據;本題的規模與 SLO 是虛構的面試輸入。
面試官考察點
第一,看候選人能否精確定義契約。「已傳送」「已送達」和「已讀」是三件事。伺服器確認表示已經持久化接收;送達回執表示指定接收裝置拿到了訊息;已讀回執還需要產品規則確認使用者實際顯示了會話。把三者合成一個狀態,會產生虛假的可靠性結論和錯誤的未讀數。
第二,看候選人是否會迴避端對端「恰好一次」承諾。伺服器提交後、確認到達前,用戶端可能逾時;閘道也可能在重新連線後重複投遞。可實現的契約是至少一次傳輸,加上穩定的用戶端訊息 ID、伺服器冪等記錄,以及用戶端按伺服器訊息 ID 去重。
第三,看排序邊界。全域順序會讓無關會話共用串行瓶頸。更有用的產品契約是:每則已接收訊息在所屬會話內取得單調遞增序號。因此,同一會話的寫入必須到達目前唯一擁有者或定序器;不同會話仍可獨立擴展,同時也清楚暴露單一熱門會話的上限。
第四,看是否拆分持久狀態和短暫狀態。訊息、成員資格區間和已讀游標需要承受故障;線上狀態與正在輸入可以過期和捨棄。如果輸入提示也進入持久訊息日誌,不但浪費儲存,裝飾性流量還可能拖慢真實訊息。
最後,看復原思維。答案應涵蓋傳送確認遺失、閘道故障、重新連線風暴、過時會話擁有者、慢速裝置、成員變更、重複扇出和熱門群聊。容量數字要從題幹推導,再透過負載測試校準,不能假裝是所有伺服器都適用的極限。
回答前需要釐清的問題
- 哪些內容和會話類型在範圍內? 本答案處理文字、私聊和最多 200 人的群聊。附件使用物件儲存加中繼資料訊息;公開頻道需要不同的扇出與歷史讀取策略。
- 每類回執代表什麼? 伺服器接收、裝置收到、使用者已讀是獨立的單調事實。「已讀」只在用戶端顯示相關會話時發生,收到推播不算已讀。
- 需要什麼順序? 本題要求會話內穩定順序,不承諾按使用者裝置時鐘排序,也不讓兩個不同會話共用序號。
- 使用者能否從多台裝置傳送? 可以。每台裝置有自己的驗證連線和同步游標;傳送方重試時重複使用同一個用戶端訊息 ID。
- 成員變化時如何處理? 接收訊息時檢查權限。產品還要定義新成員能否讀取舊歷史,以及被移除成員可在本機保留什麼。本設計按會話序號儲存成員資格區間。
- 訊息與冪等記錄保留多久? 訊息保留期由產品與法遵決定。冪等對映必須涵蓋最長用戶端重試窗口;更早刪除會重新製造重複訊息。
- 使用哪些地區? 基礎答案為每個會話分配主要地區,並執行受控故障移轉。多地區同時可寫需要更複雜的衝突與排序模型。
- 哪些時間不計入 1 秒目標? 離線推播顯示、重新連線補齊、使用者閱讀時間和跨地區災難復原,要與已在線上裝置的即時傳送分別計量。
30 秒回答框架
「我會把連線管理與持久訊息鏈路拆開。用戶端透過驗證後的 WebSocket 傳送訊息,並攜帶穩定的用戶端訊息 ID。按會話路由的服務重新檢查成員資格,分配下一個會話內序號,在同一交易裡儲存訊息和冪等結果,然後才回傳確認。非同步扇出工作查找接收方的線上裝置,並把訊息送到對應閘道;離線或失敗裝置透過基於游標的同步介面復原。送達和已讀游標只能單調前進,線上狀態與正在輸入走獨立的過期鏈路。傳輸層是至少一次,因此伺服器和用戶端都要去重。按題目規模,平均約每秒 23000 則、尖峰按十倍約每秒 230000 則;若每則儲存 1 KB,每天邏輯資料約 2 TB。我會針對長連線、熱門群聊、重新連線風暴、重試、成員變化和擁有者故障移轉做負載與故障測試。」
分步驟深入解答
步驟一:凍結產品契約,計算第一版容量範圍。
每天 20 億則訊息,平均約為每秒 23148 則。若尖峰是平均值的十倍,入口應先按每秒 230000 則規劃。假設每則持久化訊息連同一般中繼資料和索引平均占 1 KB,每天邏輯寫入約 2 TB;三副本約 6 TB,還沒有包含壓縮、檔案系統餘量、備份和回執寫入。這些規劃假設仍需透過硬體實測校準。
500 萬條並行連線決定閘道規模。假設在選定執行個體、TLS 設定、心跳週期和目標 p99 下,負載測試證明每台閘道可健康維持 50000 條連線,最低下限是 100 台。若只運行在實測上限的 70%,需要約 143 台,可取整為 150 台,再增加可用區故障餘量。測試必須包含訊息、重新連線和慢速用戶端;只數閒置通訊端會嚴重失真。
步驟二:定義用戶端協定與各類身分。
每台裝置與地區閘道建立驗證後的 WebSocket。閘道在適用時驗證來源、限制訊框大小和傳送頻率、更新授權,並透過 ping/pong 與租約識別失效連線。RFC 6455 只提供連線機制;驗證、確認、序號、重試和背壓仍由應用程式協定負責。
核心訊框可以寫成:
SEND {
conversation_id, client_message_id, body, client_sent_at
}
ACK {
client_message_id, message_id, conversation_seq, accepted_at
}
MESSAGE {
conversation_id, message_id, conversation_seq, sender_id, body, accepted_at
}
SYNC {
device_sync_cursor, limit
}clientmessageid 只產生一次,同一傳送裝置的每次重試都重複使用它。messageid 是伺服器身分,供接收方去重。conversationseq 決定單一會話內的顯示和補齊順序。accepted_at 是診斷所用的伺服器時間,不能充當排序權威。
步驟三:每則邏輯傳送只提交一次,提交後才確認。
閘道把 SEND 轉送給聊天服務。服務定位目前會話分片,驗證傳送者,按目前成員版本檢查資格,驗證大小,並執行使用者級與會話級限流。分片擁有者串行處理該會話內已接收的寫入。
在同一個持久交易裡,它分配下一個序號、寫入訊息,並寫入類似 (senderdeviceid, clientmessageid) → message_id 的唯一對映。重複請求回傳既有結果,不再追加訊息。只有完成法定副本提交的記錄才收到 ACK。確認遺失時可以安全重試;儲存在提交前失敗時不傳送確認。
訊息按 conversationid 分割,並按 conversationseq 排序。成員變更也要進入順序,或參考生效序號邊界,避免授權只依賴最終一致的目前成員快取。帶任期的擁有者柵欄會阻止故障移轉後的舊擁有者繼續寫入。
步驟四:持久提交後再扇出。
已提交日誌產生送達工作。連線目錄把 (userid, deviceid) 對映到閘道與連線租約。對於私聊或最多 200 人的群聊,扇出工作器讀取在該訊息序號生效的成員快照,按閘道合併線上裝置,再批次下發閘道命令。訊息本文只持久化一次;扇出攜帶參考或精簡事件,不為每個接收方複製一份持久本文。
閘道把事件放入每條連線的有界出站佇列。裝置在本機接收事件後回傳單調送達游標。如果連線過慢,閘道不能無限快取;它會標記該裝置需要重新同步,並帶應用程式原因關閉連線。持久日誌仍是真相,所以捨棄一次記憶體即時投遞不會遺失訊息。
對於離線使用者,系統可以傳送不暴露敏感內容的行動推播提示。推播只負責喚醒;持久服務儲存訊息,裝置回執證明送達。使用者開啟應用程式後,由用戶端驗證並從持久服務同步。
步驟五:明確設計重新連線與多裝置同步。
每台裝置持久化一個不透明的 devicesynccursor。GET /sync?after=cursor 或等價訊框回傳有序的會話增量、成員變更、回執增量及新游標。伺服器不能把游標推進到本次回應遺漏的事件之後。游標過期或缺口過大時,伺服器回傳有界快照和續頁權杖,不在一個連線裡嘗試無限重播。
即時傳送與同步可能重疊,因此用戶端按 messageid 合併,再按 (conversationid, conversation_seq) 排序。新裝置只取得策略允許的歷史,並建立自己的游標。已讀狀態按使用者和會話單調前進;送達狀態可以保留到裝置層級。彙總規則必須說明「已送達」是任一裝置已收到,還是所有活躍裝置都收到。
步驟六:如實處理回執、線上狀態與正在輸入。
已讀更新攜帶用戶端已經顯示的最大 conversation_seq;伺服器使用條件最大值,避免遲到請求讓游標倒退。Matrix 規範中的回執模型說明了為什麼已讀是取代舊位置的增量,也說明僅僅收到事件不足以證明使用者看過內容。
線上狀態與正在輸入走另一條鏈路。閘道更新短期線上租約。輸入事件要通過授權、限流,只作用於一個會話,進行合併,並在數秒後過期。過載時可以捨棄,永遠不寫入持久訊息日誌。「最後上線」需要明確隱私策略,並使用粗粒度更新,避免心跳流量演變成寫入風暴。
步驟七:先設計故障路徑,再聲稱可靠。
ACK遺失:用戶端用同一個clientmessageid重試,伺服器回傳已儲存結果。- 閘道崩潰:裝置帶抖動重新連線並從游標復原;原連線租約到期。
- 扇出工作器崩潰:持久送達工作重試;閘道與用戶端去重。
- 會話擁有者崩潰:透過新任期與法定副本狀態選出接替者;舊擁有者被柵欄阻止。
- 慢速裝置:有界佇列觸發重新同步,不讓它耗盡記憶體。
- 熱門會話:單一定序器限制寫入速率。批次提交、隔離熱門分片並執行產品級限流;增加一般雜湊分割無法平行化同一條嚴格順序。
- 成員競態:把成員變化與訊息一起排序,並按生效區間授權。
- 地區故障:只有在柵欄隔離舊主要地區後才提升副本;復原時間和未確認訊息的可能遺失,與「已確認訊息不遺失」分開描述。
重新連線採用帶抖動的指數退避和准入控制。否則一次地區閘道重新啟動,可能把 500 萬個健康用戶端變成交握風暴,反過來阻斷復原。
步驟八:用分層測試與可觀測性證明不變量。
屬性測試產生重試、亂序送達、成員變化和擁有者任期,再斷言每個用戶端 ID 只有一則邏輯訊息、會話序號唯一、游標單調,以及移除邊界之後沒有越權訊息。整合測試分別在提交前、提交後確認前、扇出期間終止處理程序。混沌測試移除閘道、工作器、分片擁有者、可用區和一部分連線目錄。
負載測試模擬 500 萬長連線、每秒 230000 則尖峰傳送、群聊扇出、慢速接收方和大規模重新連線。觀測接收延遲、即時傳送延遲、同步落後、用戶端去重前後的重複率、序號缺口、熱門分片飽和度、出站佇列位元組數、重新連線准入和越權讀取拒絕。漸進發布只在持久性與授權不變量保持正常時擴大。
高品質示範回答
「我會先定義三個不同結果:伺服器持久化接收、裝置送達和使用者已讀。只有法定副本儲存訊息後,服務才回傳確認。網路投遞仍是至少一次,因此傳送方產生的用戶端訊息 ID 讓重試冪等,接收方按伺服器訊息 ID 去重。
用戶端連線到地區 WebSocket 閘道。連線目錄告訴扇出工作器每台使用者裝置在哪個閘道,但閘道不持有歷史。聊天服務按會話 ID 把每次寫入路由到目前帶柵欄的擁有者。擁有者重新檢查成員資格,分配會話內序號,並不可分割地寫入訊息與冪等結果。無關會話可以平行;單一超熱會話則保留為明確的串行瓶頸。
提交後,工作器向線上裝置扇出。失敗或離線投遞透過基於游標的同步介面修復。即時與重播可能重疊,因此訊息 ID 吸收重複,會話序號恢復局部順序。已讀游標按最大值前進。線上狀態與正在輸入使用會過期的盡力而為狀態,不能阻塞持久訊息。
每天 20 億則訊息,平均入口約每秒 23000 則;我會按十倍尖峰規劃每秒 230000 則。按題目假設的每則 1 KB,邏輯訊息儲存約每天 2 TB,三副本約 6 TB。對於 500 萬連線,實測每台 50000 條的閘道上限意味著最低 100 台;按 70% 使用率約需 150 台,之後再加可用區餘量。
我會驗證確認遺失、閘道與擁有者故障、過時任期、重複扇出、慢速裝置、成員競態、熱門群聊和重新連線風暴。發布門檻是:已確認訊息零遺失、去重後無重複邏輯訊息、序號不倒退、成員區間外沒有越權存取,以及代表性負載下即時傳送 p99 達標。」
常見錯誤
- 錯誤:把伺服器接收、送達和已讀稱作同一狀態 → 後果:可靠性與未讀指標無法驗證 → 修正:分別定義單調狀態及其責任方。
- 錯誤:承諾網路端對端恰好一次 → 後果:確認遺失或重新連線會產生無法解釋的重複 → 修正:使用至少一次傳輸、穩定傳送 ID 和去重。
- 錯誤:全域排序所有訊息 → 後果:無關會話共用一個瓶頸 → 修正:只在會話內分配序號。
- 錯誤:持久提交前就確認 → 後果:處理程序或可用區故障會靜默抹掉已接收訊息 → 修正:只確認完成法定副本提交的記錄。
- 錯誤:把歷史存在 WebSocket 閘道 → 後果:連線遷移等於資料遷移,閘道遺失還會威脅歷史 → 修正:閘道只保留有界連線狀態。
- 錯誤:為慢速裝置無限快取 → 後果:單一用戶端就能耗盡閘道記憶體 → 修正:限制佇列、斷開連線並從持久儲存重新同步。
- 錯誤:把線上狀態和輸入提示寫入持久日誌 → 後果:短暫裝飾流量增加成本並拖慢訊息 → 修正:使用經過授權、限流且帶 TTL 的盡力而為鏈路。
- 錯誤:只按過時的目前成員快取授權 → 後果:被移除使用者可能跨越競態傳送或接收 → 修正:排序成員邊界,並按其生效區間柵欄接收。
- 錯誤:只按閒置通訊端數量給閘道定容 → 後果:TLS、心跳、扇出和重新連線會擊穿方案 → 修正:以目標 p99 壓測完整負載並保留故障餘量。
追問及應對
追問一:如何增加端對端加密?
在傳送裝置上加密訊息本文,伺服器只存密文和路由所需中繼資料。每台裝置需要身分金鑰、簽署裝置清單及會話金鑰分發;新增或移除裝置、成員時,要按策略輪換或重新分發金鑰。伺服器仍能給密文排序和路由,但伺服器搜尋、內容審核、復原、預覽及濫用處理都會受限。送達中繼資料、參與者、時間和大小可能仍然可見,所以加密不會消除中繼資料隱私工作。
追問二:能否讓多個定序器拆分一個熱門群聊?
如果仍要求一條嚴格連續的順序,就不能在沒有另一個排序權威的情況下直接拆分。給會話增加一般雜湊分片,最終仍需要合併或共識點。先批次提交和隔離分片,再執行會話級限流。若產品接受局部順序,可以按話題或傳送方分割並暴露因果關係,但這會改變契約和用戶端複雜度。
追問三:傳送方已看到 ACK,但接收方離線一週,這則訊息算送達嗎?
它已被接收,但尚未送達該裝置。持久訊息在保留策略內繼續存在,推播提示可以嘗試喚醒裝置。裝置重新連線後從游標同步,再回報送達。產品介面和指標應分別保留伺服器已接收、任一裝置已送達、所有活躍裝置已送達和已讀。
追問四:編輯和刪除如何與排序結合?
把它們表示為參考原訊息的新有序事件,保留可追蹤的歷史。用戶端折疊事件流得到目前檢視。接收編輯或刪除時重新檢查權限;產品策略定義時間限制,以及刪除是移除內容、留下墓碑,還是非同步清除次要儲存。
追問五:被移除成員帶著舊游標重新連線怎麼辦?
同步服務檢查該使用者的成員資格區間,不能只看其是否持有游標。它只回傳該使用者有權讀取的事件,並包含成員移除邊界。游標僅表示不透明位置;存取權仍取決於身分與成員區間。已快取附件與本機副本還需要獨立的撤銷和保留預期,因為伺服器無法抹除已經儲存到裝置的資料。
追問六:如何支援超大型公開頻道?
面向 200 人群聊的寫入時扇出不再適用。頻道日誌只存一份,追蹤者按游標拉取;只扇出精簡的未讀或推播提示,快取熱門片段,並放寬個人化送達回執。分割、審核、探索和明星頻道熱點都成為一等問題,需要獨立的工作負載模型;沿用同一方案並擴大數字會失真。