【從一台 Server 到分散式架構】第 17 篇:即時通知——WebSocket 與 Pub/Sub

課程平台做大之後,小明他們的需求開始變得「不只是查資料」。

某天,產品經理小雯丟來一個新需求:

「學生上傳作業後,老師要立刻收到通知;老師回覆後,學生也要馬上看到。不要再像現在一樣,過幾分鐘才在頁面上刷新出來。」

小明的第一個反應是:

「那就每 5 秒打一次 API 問『有沒有新通知』?」

小傑搖頭:「這就是輪詢(Polling)。一開始能用,但用戶多了會把後端打爆,而且『即時』的體驗也很假。」

這篇,小明要學兩件事:

  1. WebSocket:讓伺服器能主動把訊息推給瀏覽器(不是等瀏覽器來問)。
  2. Pub/Sub:當後端不只一台、長連線分散在多台機器上時,如何把通知送到「連著該用戶的那一台」。

為什麼輪詢不適合「即時」?

輪詢的做法很簡單:

  • 前端每隔一段時間呼叫一次 GET /notifications?since=...
  • 後端回傳最新通知

問題在於,它會把「偶爾才有」的事件,變成「永遠不停」的流量。

假設:

  • 10 萬個同時在線使用者
  • 每個人每 5 秒輪詢一次

那就是 (100,000 / 5 = 20,000) QPS 的「純詢問」流量——就算大多數回傳都是空的,後端、快取、DB、log 都要為此付出成本。

更糟的是「即時感」也不真的即時:你輪詢間隔是 5 秒,平均延遲就是 2.5 秒;要更即時就得更頻繁輪詢,QPS 又更高。

所以輪詢的本質是:用資源換時間,但換不到真正的即時。


生活化的比喻:等外送 vs 店家打電話叫你取餐

輪詢像什麼?

像你每 30 秒打電話問店家:「我的餐好了嗎?」大部分時間店家都會回:「還沒。」

WebSocket 像什麼?

像你下單後就把電話留給店家,餐一做好店家直接打給你:「可以來拿了。」你不用一直問。

核心差異:誰主動


WebSocket:讓伺服器主動推送

HTTP 的典型互動是「請求 → 回應」:客戶端不問,伺服器就不會說話。

WebSocket 則建立一條長連線(long-lived connection)

  • 一開始仍然走 HTTP(Upgrade 握手)
  • 之後升級成 WebSocket
  • 兩端可以隨時互相傳訊息(full-duplex)

對「即時通知」而言,這代表:

  • 使用者打開網站後建立 WebSocket 連線
  • 後端有新事件(作業上傳、老師回覆)時,可以直接 push 給該使用者

只用 WebSocket 還不夠:多台後端時通知會「迷路」

小明他們早就不是單台後端了(第 03、09 篇的水平擴充與狀態問題)。

長連線的世界裡,有一個很現實的問題:

一個使用者的 WebSocket 連線,只會連到某一台 WebSocket Server。

假設:

  • 老師 A 的 WebSocket 連在 WS-1
  • 學生 B 的 WebSocket 連在 WS-2
  • 學生 B 上傳作業,這個請求被負載平衡打到 API-3 去處理

此時 API-3 想要通知老師 A,但老師 A 的連線在 WS-1API-3 根本不知道該找誰推送,甚至連推送的「管道」都不在自己身上。

這就是多機環境下「通知路由」的本質困難:事件產生在某台機器,連線卻在另一台機器。


Pub/Sub:把事件廣播出去,讓握有連線的那台去推送

解法是把「事件」和「推送」拆開:

  • 事件:作業上傳、老師回覆、留言新增……由任何服務產生
  • 推送:由握有 WebSocket 連線的伺服器負責送出

中間需要一個「把事件送到該去的地方」的機制:Pub/Sub(Publish/Subscribe)

概念很直覺:

  • Publisher:發佈事件(例如:homework_submitted, comment_replied
  • Subscriber:訂閱事件並做處理(例如:WebSocket Server 收到後推送給在線使用者)

兩種常見做法:廣播頻道 vs 用戶頻道

做法一:廣播(所有 WS Server 都訂閱同一個 Topic)

事件發出後,所有 WebSocket Server 都會收到:

  • 如果這台 WS Server 剛好握有該用戶連線 → 推送
  • 沒有握有 → 忽略

用圖來看,會像這樣(事件「廣播」到所有 WS,再由握有連線的那台負責推送):

優點:簡單。

缺點:事件量大時很浪費(所有 WS Server 都吃到全部事件)。

做法二:以用戶/群組為 Topic(只讓「可能有連線」的那群 WS Server 看到)

例如:

  • user:{user_id}(個人通知)
  • course:{course_id}(課程聊天室 / 群組通知)

用圖來看,會像這樣(事件發到「用戶/群組 Topic」,只有訂閱者會收到):

優點:效率高。

缺點:Topic 數量多、需要更有系統的設計(尤其是群組/聊天室的訂閱關係)。

小明他們一開始會用廣播起步;等事件量大到浪費明顯,再進化成用戶/群組 Topic


架構長什麼樣子?

🏗️ 本篇結束後的架構 — API 產生事件,Pub/Sub 分發,WS Server 推送給使用者

你可以把 WebSocket Server 想成「外送員」:它手上握著地址(連線),負責把通知送到正確的客戶端;Pub/Sub 則是「廣播電台」,事件一出現就通知所有外送員。


Redis Pub/Sub、Kafka、還是其他?

小明他們前面已經有 Message Queue(第 08 篇)。但 Pub/Sub 和 MQ 在「需求特性」上有差異:

  • 即時推送更像「事件流」:事件一來就要立刻發出去
  • 通知事件通常可接受「至少一次(at-least-once)」:偶爾重複推送可以用去重或前端 idempotency 扛住

實務上常見選擇:

Redis Pub/Sub(最快上手)

  • ✅ 延遲低、設定簡單
  • ✅ 適合「通知型」即時事件(聊天室、在線狀態、提醒)
  • ❌ 不是持久化訊息:訂閱者離線時容易漏訊(如果你用的是純 Pub/Sub,而不是 Streams)

Kafka(更工程化、更重)

  • ✅ 持久化、可回放、可水平擴充
  • ✅ 訂閱者可以自己決定從哪個 offset 開始
  • ❌ 成本與複雜度高,不是每個團隊都需要

Redis Streams / 其他(折衷)

  • 兼具「延遲低」與一定程度的「可回放」
  • 但要管理 consumer group、ack、重試等機制

小明的選型原則仍然是那一句:

只在現有工具真的無法勝任時,才引入新的工具。

因此「第一版」通常會是:WebSocket + Redis Pub/Sub。如果後來遇到「不能漏通知」或「要回放」的強需求,再評估 Streams 或 Kafka。


真正的坑:連線狀態、登入態、以及「這個用戶到底連在哪?」

做即時通知時,常見的麻煩不是 WebSocket 本身,而是周邊配套:

1) 連線要驗證身分(Authentication)

HTTP 有 Cookie / Header;WebSocket 也是一樣要驗證,只是驗證通常發生在「握手」或「連線建立後的第一個訊息」。

小明他們會採用一個簡化規則:

  • 連線建立後,客戶端立刻送上 token(或握手時帶 token)
  • WS Server 驗證 token,取得 user_id
  • 後續推送都以 user_id 為索引

2) 多開分頁、手機/電腦同時登入

同一個 user_id 可能有多條連線(多裝置、多分頁)。推送時要送到「全部」還是「最新」?這是產品決策:

  • 私訊/通知:通常送到全部裝置
  • 直播聊天室:通常每個裝置都要收到

3) 使用者離線時怎麼辦?

WebSocket 只能推給「在線」的人。離線的人通常要靠:

  • 進站後拉取未讀通知(回到 HTTP API)
  • 或者走 push notification / email(第 07、08 篇的背景任務/佇列)

也就是:即時推送不是取代通知中心,而是加速在線通知。


小明的落地版本:先把「通知」拆成兩條路

小傑幫小明把需求拆得很清楚:

  1. 可靠儲存(Notification Inbox):每個通知都要寫入資料庫(或快取 + DB),讓使用者「之後」能看到,不會漏。
  2. 即時推送(Realtime Push):如果使用者在線,就透過 WebSocket 立刻提醒。

這兩條路可以同時做,彼此互補。

這樣就算 Pub/Sub 有一瞬間抖動或 WebSocket Server 重啟,通知也不會消失:最多是「沒即時跳出」,但使用者一刷新通知中心就看得到。


小結與預告

這篇小明學到的重點是:

  1. 輪詢的天花板:用戶一多,空回應也會變成巨量 QPS,而且不可能真正即時。
  2. WebSocket 的價值:建立長連線,讓伺服器主動推送,打造「現在就要知道」的體驗。
  3. 多機環境需要 Pub/Sub:事件產生在哪台不重要,重要的是「握有連線的那台」要能收到事件並完成推送。
  4. 可靠通知與即時通知要分層:通知一定要落庫(收件匣),WebSocket 只是加速在線體驗。

做到這裡,小明的系統開始出現另一個「分散式必考題」:當有很多台服務一起跑背景任務、一起做排程、一起處理同一種事件時,怎麼保證某些事情「只會被做一次」?怎麼避免重複發信、重複結算、重複推送?

下一篇,我們來談多機世界裡的「值日生」問題——分散式協調與領導者選舉(Zookeeper / etcd 的直覺)

相關推薦

2026-04-07
【從一台 Server 到分散式架構】第 31 篇:從面試題反推架構——用本系列思維拆解系統設計考題
「設計一個 URL 短網址服務」「設計一個訊息系統」——系統設計面試問的不是背答案,而是思考方式。這篇整理了一套可以重複使用的面試框架,並用兩道例題示範如何從需求出發,一步步推導出架構、說清楚取捨。
2026-04-06
【從一台 Server 到分散式架構】第 30 篇:限流、排隊與降級實戰——開賣與直播場景
平常流量是 1000 QPS,但「開賣」的那一刻,瞬間湧入 50 萬個請求——系統要怎麼活下來?這篇用課程平台的限量課程開賣和直播開播場景,把第 13 篇學到的限流、降級、熔斷,落地到一個真實的高流量設計,走過每個防護層是怎麼工作的。
2026-04-05
【從一台 Server 到分散式架構】第 29 篇:用同樣的思維看 ChatGPT——AI 聊天系統架構
ChatGPT 看起來像一個聊天視窗,背後卻有幾個特殊的設計挑戰:回應是串流的、推理非常耗資源、每輪對話要記住上下文、系統要支援幾千萬用戶同時使用。這篇用熟悉的架構思維,拆解 AI 聊天系統的關鍵設計。
2026-04-04
【從一台 Server 到分散式架構】第 28 篇:用同樣的思維看 Twitter——社群動態與時序設計
Twitter 的核心功能看起來很簡單:發推文、看動態、按讚留言。但「動態牆」背後藏著一個棘手的設計問題:你追蹤 200 個人,每個人都可能隨時發文——你打開 App 時,那條時序動態要怎麼快速組出來?這篇來看社群動態系統的兩種策略:推(Fan-out on Write)與拉(Fan-out on Read)。