WebSocket 的可靠性工程:heartbeat、SSE 備援、load balancer、sticky session、message broker
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(9)
1. Outline
- 起點 · WebSockets Aren’t as Reliable as You Think.. Here's Why
從「一顆 server、一條長連線」的基準出發,逐一拆掉「server 永遠活著、只有一台、client 都在同一台」三個假設,每個解法補上前一個解法留下的洞。
2. YouTuber 的思維推導
WebSockets Aren’t as Reliable as You Think.. Here's Why
Software Developer Diaries · 13m01s · 字幕 en · vision=on (使用者指定 vision=true。影片大量以白板圖與程式碼講解,作者多次說「I depicted here」「look at the code」「as you can see」。) · YouTube
預估 vs 實際耗時
| 階段 | 預估 | 實際 |
|---|---|---|
| fetch | 6s | 2s |
| segment | 56s | 4m00s |
| shot | 29s | 7s |
| analyze | 5m00s | 8m48s |
| render | 5s | – |
作者的出發點是一個反直覺的宣稱:WebSocket 比大家想的複雜,不是因為協定難,而是因為「一條長期開著的連線」會撞上一連串 HTTP 一問一答時不存在的邊界情況。他先用白板畫出最單純的基準——一顆 server、幾個 client、一條雙向長連線——然後每一步都問「這個假設被打破會怎樣」,並在每個解法落地後,指出這個解法本身製造出的下一個問題。第一步:server 悄悄崩潰時 client 不會被通知(崩潰的 server 送不出正常的關閉訊號),所以 client 必須自己定期探活——heartbeat:每 5 秒送 ping、10 秒沒 pong 就關閉重連,並用 localStorage 暫存斷線期間的資料、重連後補送;他用「把 pong 改成 pongo」在本機證明判斷會觸發。
第二步:heartbeat 失敗後若只會無止盡重連,client 就永遠拿不到資料,所以要有退路——連續失敗幾次就切到 Server-Sent Events;他先釐清 SSE 與 WebSocket 不是競爭關係(雙向 vs 單向推送),再示範 EventSource 的 API 與切換 demo。第三步:以上都假設只有一台 server,它整台死掉就無處可連,所以往上抬一層放 load balancer、水平擴展成多台,並選 least connection 而非 round robin,因為長連線的負載不能用請求數輪流分。第四步:多台同時活著又製造新問題——重連時 load balancer 憑什麼把 client 送回原本那台?
於是需要 sticky session(nginx 的 ip_hash 或 cookie 式黏性)。第五步:sticky session 讓每台只握有自己的 client,一台想廣播給所有人時碰不到別台的連線,所以最後在 server 之間放 message broker(RabbitMQ),讓一台的事件經 broker 轉給其他台、各自送給自己的 client。整條推理鏈的形狀是「每個解法都在補上一個解法留下的洞」,最後得到一套從單機容錯一路到多機廣播的完整拼圖,並在每一步都給出可跑的 Node.js 程式碼或 nginx 設定。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- WebSocket 是什麼、兩種典型用法 → 白板上的基準架構假設 server 永遠活著、連線永遠開著。可是這條長連線一旦建立,若 server 端悄悄死掉,client 手上那條 socket 會發生什麼事?它有辦法知道嗎?這是第一個要拆的邊界情況。
- 問題一:server 崩潰時 client 不知道 → 規則講完了,但「每隔一段時間送 ping、10 秒沒 pong 就重連」到底寫成什麼樣的程式碼?server 端要做什麼、client 端要做什麼?還有一個作者順口提到但沒展開的點:崩潰的 server「到不了正常關閉的程式碼」——在程式裡指的是哪一段?
- Heartbeat 程式碼:server 與 client → 程式碼寫好了,但還沒看到它真的動起來:ping/pong 在瀏覽器裡實際上多久來回一次?更重要的是,要怎麼在本機「模擬 server 沒回 pong」來證明那個 10 秒判斷真的會觸發重連?
- Demo:把 pong 改成 Pongo 觸發重連 → demo 結尾暴露了 heartbeat 的極限:只要 server 一直回錯,client 就一直重連,永遠拿不到資料。如果 WebSocket 這條路一直走不通,client 有沒有另一條路可以退而求其次、至少先把 server 推來的資料接到?
- 問題二:WebSocket 掛了改用 SSE 備援 → 切換規則有了,但 SSE 在程式裡怎麼接?瀏覽器端的 API 跟 WebSocket 長得像不像、server 要多開什麼端點?以及「失敗兩次就切換」跟第 3 段那份 heartbeat 程式碼要怎麼接在一起——實際跑起來會看到什麼?
- SSE 程式碼與 fallback demo → 到目前為止所有解法都假設只有一台 server:heartbeat 探的是它、SSE 連的也是它。如果它整台死掉,client 再聰明也沒有地方可以連。那麼下一個層次的問題是:能不能讓「server 死掉」這件事本身不再是單點——多準備幾台,由誰來決定 client 該連哪一台?
- 問題三:多台 server 前面放 load balancer → load balancer 解決了「連哪台活著的」,卻製造了一個新問題:現在有兩台以上同時活著,client 第一次連上 server1,斷線後重連時 load balancer 憑什麼會把它送回 server1 而不是 server2?如果送錯台,第 3 段那份「重連後補送 pending data」還接得上嗎?
- Sticky session:重連要回到同一台 → 有了 sticky session,每個 client 都穩穩黏在自己那台 server 上。但這反過來製造了最後一個問題:如果 API 1 上的某個 client 發了一則要給「所有人」的訊息,而其他人黏在 API 2 上——API 1 根本碰不到他們的連線,這則訊息要怎麼送過去?
- Broadcasting:用 RabbitMQ 跨 server 廣播
3. 逐段說明
WebSockets Aren’t as Reliable as You Think.. Here's Why
1. WebSocket 是什麼、兩種典型用法 0:00–1:40
作者開場說 WebSocket 比大家想的複雜,因為有幾個邊界情況要處理,而業界已有對應解法。先快速複習:client 與 server 建立一條 socket 連線後不再反覆發 request,雙方可即時互傳資料。舉兩種典型場景:多個 client 連到同一台 server 的 messenger(server 把訊息轉送給特定 client),以及 server 主動推送即時資料的天氣追蹤 app。

推理因為作者一開頭就宣稱「WebSocket 比大家想的複雜,因為有幾個邊界情況」,所以他必須先建立一個「正常運作的 WebSocket 長什麼樣」的基準,之後每個邊界情況才有東西可以對照。他用兩種用法把基準畫出來:多個 client 連同一台 server 的 messenger(server 負責轉送),以及 server 主動推資料的天氣 app(單向推送)。白板上畫的是一顆綠色 Server 圓圈,兩個 Client 用虛線雙向箭頭連著它——這張圖就是後面所有問題的起點:連線是長期存在的、server 是唯一的中心。
- C瀏覽器與 server 之間建立一次、之後雙方都能隨時互傳訊息的長連線→ 畫地圖
- RWebSocket 怎麼從 HTTP 升級來的→ 存+回想
- AWebSocket 升級像先打總機轉接,接通後就是直線熱線→ 批判類比
AI 補充作者沒說清楚的一點:WebSocket 一開始仍是一個普通的 HTTP request(帶 Upgrade 標頭),server 同意後這條 TCP 連線才「升級」成雙向通道,之後就不再有 HTTP 的 request/response 邊界。這解釋了為什麼它能省掉反覆發 request——省的是每次 HTTP 交握與標頭的成本,不是省掉 TCP 連線本身。另外,白板上 server 只有一顆,這是刻意簡化:後面所有「複雜」都來自這個假設被打破(server 會死、會有多台)。作者說 messenger 的 client「maybe they can even talk to each other, I'm not sure」——在標準 WebSocket 模型裡 client 之間永遠不直接相連,一定經過 server 轉送,這點在他自己的圖上其實已經畫對了。
術語:socket connectionpush
2. 問題一:server 崩潰時 client 不知道 1:40–2:36
第一個難題:client 已連上 WebSocket,但 server 突然崩潰。最好的情況是 client 能偵測到,但多數時候不行,因為崩潰的 server 來不及正常關閉連線。解法是 heartbeat:client 定期送 ping,server 回 pong;若 10 秒內沒收到 pong,client 就主動關閉連線並嘗試重連。

推理因為上一段建立的基準是「一條長期開著的 socket 連線」,所以作者接著問:這條連線的另一端如果崩潰了呢?他先區分兩種情況:最好的情況是 client 偵測得到;但多數情況偵測不到,理由是「server 崩潰時沒辦法正常關閉連線」——正常的關閉需要 server 送出關閉訊號,崩潰的 server 送不出來。既然對方不會主動報死訊,就只能由 client 定期去探:這就是 heartbeat。白板上畫的規則很具體:client 每隔一段時間送 ping,WebSocket API 回 pong;若 10 秒內沒等到 pong,client 就自己關掉連線並重連。圖上 WebSocket API 下面打了一個叉,表示這正是在模擬 server 死掉的情境。
- Cclient 定期送小訊息要求 server 回覆,用有沒有準時回判斷連線是否還活著→ 畫地圖
- Rhalf-open 連線與 TCP keepalive→ 存+回想
- P替現有 WebSocket 專案加上 client 端 heartbeat→ 練習
AI 補充作者說「server 崩潰時沒辦法正常關閉連線」,補上被跳過的那一步:正常關閉 WebSocket 要交換 Close 幀、底層 TCP 要送 FIN;程序被 kill、機器斷電或網路中斷時這些都不會發生,作業系統也不會主動通知遠端。TCP 本身沒有內建的即時存活偵測(TCP keepalive 預設通常要兩小時才會起作用),所以 client 的 socket 會一直看起來「還開著」,這就是所謂的 half-open 連線。另外要注意作者這裡的 ping/pong 是應用層自己定義的訊息(送字串 ping、收字串 pong),跟 WebSocket 協定內建的 Ping/Pong 控制幀是兩回事——瀏覽器的 JavaScript API 沒有暴露協定層的 ping,所以自己在訊息層做一套是常見做法。「10 秒」不是標準值,是作者選的門檻;門檻越短偵測越快,但誤判(網路暫時抖動就重連)也越多。transcript 裡的「PK」是語音辨識把 pong 聽錯,白板上寫的是 pong。
術語:ping / pong
3. Heartbeat 程式碼:server 與 client 2:36–4:08
用 Node.js 範例說明。Server 端有 health check endpoint,client 連上時記錄,每收到訊息就回 pong;雖有 close 處理,但錯誤發生時常根本跑不到那段程式碼。Client 端在 socket 開啟時把先前存在 localStorage 的 pending 資料送回 server(斷線期間先存本地);每收到 pong 就更新 last pong 時間;每次送 ping 前比對上一個 pong 是否在 10 秒內,超過就 close 並 reconnect。


推理因為上一段把規則定成「client 送 ping、server 回 pong、10 秒沒 pong 就重連」,所以作者接著把兩端的職責分開實作。server.js 很短:express 提供 /health-check 端點,WebSocketServer 掛在同一個 http server 上,connection 事件時 log,message 事件時一律 ws.send("pong"),close 事件時 log。作者指著 close 那一行說:如果邏輯裡出錯,「我們有時候根本到不了這段程式碼」——這就回答了上一段的問題:正常關閉會觸發 close,崩潰不會。client 端則多做了一件白板上沒畫的事:socket 打開時先把 pending 資料送回去,因為斷線期間 client 可以把更新先存進 localStorage,重連後補送。heartbeat 本體在 sendPing():if (Date.now() - lastPongTime > 10000) 就 log「No pong received in 10s. Reconnecting...」並 socket.close(),否則 socket.send("ping");onmessage 收到 pong 時更新 lastPongTime。
AI 補充程式碼裡有兩個作者沒特別講、但值得看的細節。第一,server 是「收到任何訊息就回 pong」,沒有檢查內容是不是 ping——這在 demo 沒問題,但真實應用裡一般訊息和心跳會混在同一條連線上,需要區分。第二,截圖裡還有一個 checkServerHealth() 函式:先 fetch("/health-check"),成功才 connectWebSocket(),失敗就 setTimeout 隔 RECONNECT_INTERVAL 再試。也就是說作者的「重連」不是直接重開 WebSocket,而是先用普通 HTTP 探一下 server 活了沒——這比盲目重開 socket 省資源,也解釋了 server.js 為什麼一開始就要有 health check endpoint。另外「先存 localStorage、重連後補送」是一種最簡單的離線佇列;要注意它只保證「不遺失」,不保證順序與不重複(如果送出後 server 收到但 client 沒收到確認,重連後會再送一次),這些是作者沒處理的邊界。
術語:close eventpending data
4. Demo:把 pong 改成 Pongo 觸發重連 4:08–5:31
實際啟動 server.js 與 HTML,在 Network tab 看到 WebSocket 連線每 5 秒交換一次 ping/pong。接著故意把 server 回的 pong 改成「Pongo」,client 收到的不是預期的 pong,10 秒後判定沒收到 pong 而重連;因為 server 一直回錯的字,前端就不斷斷線、重連。作者說這就是讓 WebSocket 更容錯的方式,程式碼會放上 GitHub。

推理因為上一段的判斷邏輯是「onmessage 收到 pong 才更新 lastPongTime」,所以作者想到一個不用真的把 server 殺掉的驗證法:讓 server 回一個 client 不認得的字。他先啟動 node server.js 與 live server,在 Network tab 看到 WebSocket 連線每 5 秒交換一次 ping/pong(這是 sendPing 的間隔,白板上沒寫)。接著把 server 的 ws.send("pong") 改成 "pongo",重開後 DevTools console 依序印出「Connected to WebSocket」「Received: pongo」——因為 client 只在收到 "pong" 時更新時間,pongo 不算數,10 秒後 lastPongTime 過期,觸發「No pong received in 10 seconds, reconnecting」,重連成功;但 server 還是一直回 pongo,所以前端就進入「連上→10 秒→斷→重連」的循環。作者的結論:這就是讓 WebSocket 更容錯(fault tolerant)的方式。
AI 補充這個 demo 證明的其實比「server 死掉」更廣:heartbeat 偵測的是「server 沒有以我預期的方式回應」,不只是連線層的斷線。server 活著但邏輯壞掉(回錯格式、卡住不回)一樣會被當成死掉而重連——這是優點也是要注意的地方,因為重連解決不了邏輯錯誤,只會一直循環,就像 demo 裡看到的。兩個 client 端的數字現在都出現了:ping 每 5 秒送一次,等 pong 的門檻是 10 秒,也就是「連續錯過兩次 pong 才重連」,這個 2:1 的比例讓單次網路抖動不會誤判。transcript 裡的「Pam」「punk」「PK」都是語音辨識錯誤,指的是 ping / pong;「false tolerant」是 fault tolerant。
5. 問題二:WebSocket 掛了改用 SSE 備援 5:31–7:03
第二個用例:WebSocket API 掛掉時,改用 Server-Sent Events(SSE)當 fallback。SSE 跟 WebSocket 很像,兩者都能把資料推到瀏覽器,但不是競爭技術:WebSocket 是雙向(例如聊天室),SSE 只能 server 推到瀏覽器(例如股票報價、Twitter 時間軸),client 不能透過同一條連線送資料回去(可另開 API route)。策略:送 ping 沒收到 pong,再試一次,兩三次都失敗就暫時放棄 WebSocket,切到 SSE;server 當然也要支援。

推理因為上一段的 demo 顯示 heartbeat 失敗後只會無止盡重連,所以作者接著要一個「放棄 WebSocket 之後的去處」。他先解釋 SSE 是什麼:Google 會看到很多「WebSocket vs SSE」的比較文,兩者都能把資料推到瀏覽器,但不是競爭關係——WebSocket 是雙向(聊天室),SSE 只能 server → 瀏覽器(股票報價、Twitter 時間軸),client 無法透過同一條連線送資料回去,要送只能另開 API route。然後定義切換規則,白板上畫得很清楚:ping 沒等到 pong,這條線就打叉;過一陣子再送一次 ping;兩次都失敗就「暫時放棄」WebSocket,改連旁邊那顆 SSE 圓圈。當然 server 也要提供 SSE 端點。
- Cserver 透過一條持續開著的 HTTP 回應,單向不斷把事件推給瀏覽器→ 畫地圖
- C主要方法失敗到某個程度後,自動改用的次要方法→ 畫地圖
- REventSource 內建自動重連→ 存+回想
- P替即時功能設計一個 SSE fallback 的熔斷門檻→ 練習
AI 補充作者沒說的重點:為什麼 SSE 適合當備援,而不只是「另一個推送技術」?因為 SSE 就是一個普通的、永不結束的 HTTP GET 回應(Content-Type: text/event-stream),它走的是 HTTP 那條路——所以凡是 WebSocket 會被擋、被代理伺服器切斷、或 Upgrade 交握失敗的環境,SSE 通常還能通;反過來說,如果是 server 本身完全死掉,SSE 也一樣連不上,這時備援只是換一種方式失敗。另一個作者略過的優點:瀏覽器的 EventSource 內建自動重連(斷線後預設約 3 秒重試,還會帶上 Last-Event-ID 讓 server 補送漏掉的事件),這是 WebSocket 沒有、上一段得自己手寫的功能。至於「送資料回 server 要另開 API route」——這其實是很正常的組合:下行走 SSE、上行走普通 POST,很多產品就是這樣做的,只是失去了 WebSocket 那種單一連線的低延遲上行。切換規則裡的「兩次」和「暫時」都是作者的選擇:試幾次、放棄多久之後再回頭試 WebSocket,都要依應用調整。
術語:Server-Sent Events (SSE)bidirectional vs unidirectional
6. SSE 程式碼與 fallback demo 7:03–8:50
SSE 範例:server 一樣故意送 Pongo 觸發錯誤,另外提供 /events endpoint 送資料給 client,也能關閉。Client 除了 WebSocket 之外多了 connectSSE:連到 /events,API 跟 WebSocket 很像(onmessage、onerror)。實際跑:先連上 WebSocket,收到 Pongo,ping 失敗 1、2、3 次後切到 SSE fallback,開始收到 server 推來的事件。作者稱這是處理不穩定 WebSocket 的另一個可靠方法。


推理因為上一段定義了「WebSocket 失敗幾次就切到 SSE」的規則,所以作者接著在第 3 段的 heartbeat 範例上加一層。server 端沿用第 4 段的招數——故意回 pongo 來觸發失敗——並多開一個 /events 端點持續送資料給 client,也能關閉。client 端則多了一個 connectSSE():usingSSE = true、new EventSource("http://localhost:3000/events")、把畫面狀態改成「Connected via SSE」,然後 onmessage 印收到的資料、onerror 印錯誤並關閉後隔 RECONNECT_INTERVAL 再連——作者強調這個 API 跟 WebSocket「長得很像」。實際跑:console 先印 WebSocket Connected、Received: pongo、No pong received in 10s,Failed Attempts 一路數到 3,接著印「Switching to SSE fallback」,畫面上出現橘點「Connected via SSE」,terminal 那邊也印 SSE Client connected,client 開始收到 server 推來的事件。作者總結:這是處理不穩定 WebSocket 的另一個可靠方法。
AI 補充幾個看程式碼才看得到的地方。第一,白板上說「兩次」,程式實際上數到 Failed Attempts: 3 才切換——門檻是個變數,白板和程式不一致無傷大雅,但提醒你這數字沒有標準答案。第二,作者的 connectSSE 在 onerror 裡自己 close() 再 setTimeout 重連,這其實蓋掉了 EventSource 內建的自動重連(見第 5 段);這樣做的好處是重連間隔跟 WebSocket 那邊共用同一個 RECONNECT_INTERVAL、行為一致,代價是失去瀏覽器幫你帶 Last-Event-ID 續傳的功能。第三,console 裡夾了一行紅字「WebSocket is closed before the connection is established」——這是重連太快、上一條 socket 還在 CONNECTING 狀態就被 close() 的競態,demo 裡無害,但暗示重連邏輯需要退避與狀態檢查。第四,這個 demo 之所以能「切換成功」,是因為 server 本身活著、只是 WebSocket 那條路的回應壞掉;如果 server 整台死掉,/events 一樣連不上——上一段已提過,這裡在 demo 裡得到印證。最後,onmessage 裡的 JSON.parse(event.data) 提醒你 SSE 只傳文字,結構化資料要自己編碼。
術語:failed attempts counter
7. 問題三:多台 server 前面放 load balancer 8:50–9:52
另一個常見做法是在多台 server 前放 load balancer。單台 WebSocket server 雖常見(避免複雜度),但要水平擴展就會有多個 WebSocket API,其中一台掛了,load balancer 可以再起一台。做法:用 least connection 而非 round robin;load balancer 發現某台掛了會先容許幾次失敗,偵測到失敗持續 10 秒才起新副本讓 client 重連。Client 只知道 load balancer 的 URL,無法直接連 WebSocket API。

推理因為前面所有解法都只能在單台 server 的前提下自救,所以作者接著把架構往上抬一層。他先承認「只放一台 WebSocket server」也很常見(為了避開接下來的複雜度),但若要水平擴展,就會有多個 WebSocket API,其中一台掛了要能補上。白板左邊是 Client → Load Balancer → 兩顆 WebSocket API,其中一顆打了叉;右邊貼的是 nginx 設定:upstream websocket_servers 裡寫 least_conn(作者說要用 least connection、不要 round robin),兩台 server 各帶 max_fails=3 fail_timeout=10s——這對應他口中的「先容許幾次失敗、偵測到失敗持續 10 秒才處理」;下面 location /ws 用 proxy_pass 轉到 upstream,並設 Upgrade / Connection 標頭。最後一點:client 只知道 load balancer 的 URL,不能直接連到後面的 API,作者說這「也很方便」。
- C站在多台後端 server 前面,接收所有 client 請求並依策略分配給某一台→ 畫地圖
- C把新連線分給目前開著連線數最少的那台 server,適合長連線的負載特性→ 畫地圖
- Aleast connection 像看哪個窗口排隊人少就去哪,不是固定輪流排→ 批判類比
- Enginx 設定:least_conn 加 max_fails=3 fail_timeout=10s→ 存+演練
- R為什麼要 proxy_http_version 1.1 加 Upgrade/Connection 標頭→ 存+回想
- P寫一份 nginx 多台 WebSocket server 的負載均衡設定→ 練習
AI 補充為什麼 WebSocket 要用 least connection 而不是 round robin?作者沒解釋,但這是關鍵:round robin 是「每個新請求輪流分」,適合短命的 HTTP 請求;WebSocket 連線一開就是幾分鐘到幾小時,round robin 只看請求數不看誰還掛著,久了會有一台累積一堆長連線、另一台空著。least connection 看的是「現在誰身上開著的連線最少」,剛好對應長連線的負載特性。設定檔裡的 proxy_http_version 1.1 加 Upgrade / Connection 標頭也不是裝飾:WebSocket 交握(見第 1 段)需要 HTTP/1.1 的 Upgrade 機制,nginx 預設用 1.0 轉發、也不會轉這兩個 hop-by-hop 標頭,少了它們 WebSocket 根本連不上。max_fails=3 fail_timeout=10s 的實際語意是「10 秒內失敗 3 次就把這台標成不可用 10 秒,之後再試」——這是把流量導開,不是造新 server。「client 只認得 load balancer 的 URL」之所以方便,是因為 client 端第 2、3 段寫的 reconnect 邏輯完全不用改:重連一樣打同一個 URL,由 load balancer 決定這次落到哪台活著的機器。
「the load bouncer is going to create another one」
8. Sticky session:重連要回到同一台 9:52–11:08
當同時有兩個 API 在跑(或自動水平擴展後),client 連上其中一台,下次重連怎麼保證打到同一台?用 sticky session。以 nginx 為例,設定檔用 ip_hash:nginx 會把 hash 放進 cookie,client 每次都帶著這個 cookie,load balancer 就知道「這個 client 之前連的是這台」,把請求送回同一台。作者順帶展示 cookie 範例:可設過期時間,hash 值他認為是依使用者 IP 算出。

推理因為上一段引入了多台同時運作的 server(不論是自動水平擴展出來的,或本來就有兩台),所以作者接著問:client 跟 API 1 建立了連線,下次重連時怎麼確保還是打到 API 1、不是 API 2?他的解法是 sticky session,並用 nginx 示範:把上一段那份設定檔 upstream 區塊裡的策略換成 ip_hash(截圖上用筆圈起來,旁邊註解寫「Ensures requests from the same IP go to the same backend」)。作者對機制的描述是:nginx 會把 hash 放進 cookie,client 之後每次都帶著這個 cookie,load balancer 一看就知道「這個 client 之前在這台」,把請求送回同一台。他順帶展示了一個 cookie 範例,說可以設過期時間,hash 值「我相信是依使用者 IP 算的」。
AI 補充先補作者沒說清楚的「為什麼要回到同一台」:因為 WebSocket server 是有狀態的——它記得這個 client 的連線物件、可能還有訂閱了哪些頻道、未送完的訊息;換一台就等於從零開始,第 3 段那份「重連後補送 pending data」也可能送到一台不認得你的 server。sticky session 就是把「client ↔ server」這個對應關係記在 load balancer 層。至於作者描述的機制,要修正一處:nginx 的 ip_hash 完全不用 cookie,它就是拿 client 的 IP(IPv4 取前三個 octet)做 hash 決定後端,client 不需要記任何東西——這也是它的弱點:同一個 NAT 後面的所有使用者會被打到同一台,而使用者換網路(Wi-Fi 切到 4G)就會被送到別台。真正「靠 cookie」的黏性是另一種做法:load balancer 在第一次回應時 Set-Cookie 一個帶後端識別的值,之後依 cookie 路由——nginx 開源版要用 hash $cookie_xxx 這類寫法自己拼,NGINX Plus 才有現成的 sticky cookie 指令;作者展示的那個「可設過期、帶 hash 值」的 cookie 應該是這一類的例子,跟他圈起來的 ip_hash 是兩套不同的機制。另外要注意:sticky session 只是「盡量」送回同一台,那台真的死了(上一段的情境)還是會被分到別台,這時 server 端的狀態一樣會丟——這個問題 sticky session 不解決。
「engine X is going to actually connect the hash in the cookies when talking to this client and the client is Al always going to send a cookie」
9. Broadcasting:用 RabbitMQ 跨 server 廣播 11:08–13:01
最後一個例子是 broadcasting:某台 API 想把訊息送給所有 client,但有些 client 連在另一台 API 上(想像聊天室)。解法是 message broker,例如 RabbitMQ(Redis 等也可以,作者偏好 RabbitMQ):一台 API 透過 RabbitMQ 把事件送給其他 server,其他 server 收到後再轉發給自己連著的 client,達到近乎即時的廣播。程式碼只有兩台 server:建立 AMQP 連線、開 channel,收到訊息就把內容 broadcast 給自己的 WebSocket client,第二台 server 做一樣的事。


推理因為上一段的 sticky session 讓每個 client 固定黏在某一台,所以作者接著面對「一台 server 只看得到自己的 client」這個後果:broadcasting 時,廣播的 server 想送給所有 client,但有的 client 連在別台 API 上,一台 API 要怎麼把訊息廣播到所有 API?他再次拿聊天室當例子。解法是 message broker——RabbitMQ、Redis 都可以,作者個人偏好 RabbitMQ。白板上畫的是三顆 WebSocket API 中間放一個 RabbitMQ 圖示:一台 API 透過 RabbitMQ 連線把事件送給其他 server,其他 server 接到後把同一則訊息送給自己連著的 client;結果是另一台上的 client 也收到一模一樣的訊息,「也許不是即時,但接近即時」。程式碼只有兩台 server、沒有 client:amqp.connect 之後 createChannel,assertQueue 一個 broadcast_queue,channel.consume 收到 msg 就取 msg.content.toString(),然後 wss.clients.forEach 對每個 readyState === OPEN 的 client 做 client.send(message);server2 做的事一樣。
- C一則訊息要送給所有連線中的 client,不只是某一個→ 畫地圖
- C站在多個服務之間,收下訊息並轉交給訂閱者的獨立元件→ 畫地圖
- Amessage broker 像快遞轉運中心,不是每站互相寄送→ 批判類比
- E程式碼陷阱:兩台 server 共用同一個具名 queue,其實是工作佇列不是廣播→ 存+演練
- RAMQP 的定義→ 存+回想
AI 補充先補「為什麼需要 broker、不能 API 之間直接互連」:直接互連要每台都知道其他所有台的位址,台數變動(上一段的自動擴展)時要跟著改,是 N×N 的關係;broker 把它變成每台只認得 broker 一個地方,N×1,新加的 server 只要連上 broker 就自動加入廣播。「接近即時」的代價來自多了一跳(API 1 → broker → API 2 → client),通常是毫秒等級。接著是看程式碼才看得出的一個關鍵細節:截圖裡兩台 server 都 consume 同一個具名的 broadcast_queue。在 RabbitMQ 裡,一個 queue 有多個 consumer 時是「工作佇列」語意——每則訊息只會被其中一個 consumer 拿走,兩台 server 會輪流收到,而不是各收一份。真正要廣播,publisher 應該發到一個 fanout exchange,每台 server 各自宣告一個自己專屬(通常是 exclusive、auto-delete)的 queue 綁到那個 exchange 上,這樣每台都會收到每一則。影片沒有展示 publish 端,所以無法確定作者實際上有沒有這樣做,但照畫面上這段 consume 程式碼直接抄,會得到「一半的 client 收不到」的結果。另外,wss.clients.forEach 只送給「這台」的 client——這正是 broker 存在的理由:每台各自負責自己那一份。這裡也把整支影片收攏了:heartbeat 讓 client 發現 server 死了、SSE 給它退路、load balancer 讓 server 不是單點、sticky session 讓重連回到原台、broker 讓多台之間仍能像一台一樣廣播——每個解法都在補上一個解法製造出來的新洞。
「broadcast it to the client to the websocket client like this okay nothing complicated that was very simple」
4. 總結
作者先建立基準:client 與 server 之間一條長期開著的雙向 socket(第 1 段)。這條連線的第一個弱點是 server 悄悄崩潰時 client 不會知道,於是用應用層 heartbeat(每 5 秒 ping、10 秒沒 pong 就重連)來偵測(第 2–4 段)。heartbeat 只能偵測、不能救援,所以引入 Server-Sent Events 當單向推送的備援:WebSocket 連續失敗幾次就切到 SSE(第 5–6 段)。但只要仍是單台 server,整台死掉就無路可走,因此在多台 WebSocket API 前面放 load balancer,用 least connection 分配並容忍短暫失敗(第 7 段)。多台同時運作又帶來「重連要回到原本那台」的問題,靠 sticky session 解決(第 8 段)。最後,client 分散在不同台之後,一台 server 看不到其他台的 client,廣播要透過 RabbitMQ 這類 message broker 在 server 之間轉告(第 9 段)。整條推理鏈是:每一層解法都製造出下一層要處理的新邊界。
勘誤總整理
確定錯誤/已過時見仁見智(取決於版本或情境)
| 段落 | 原話(transcript 逐字) | 說明 |
|---|---|---|
| 8. Sticky session:重連要回到同一台 10:37 |
「engine X is going to actually connect the hash in the cookies when talking to this client and the client is Al always going to send a cookie」 | nginx 的 ip_hash 不使用 cookie。它直接以 client 的 IP(IPv4 取前三個 octet)計算 hash 來選後端,client 端不需要、也不會收到任何 cookie。作者把「ip_hash」和「cookie 式 sticky session」混在一起了:靠 cookie 的黏性在 nginx 開源版要用 hash $cookie_name 之類的寫法自己拼,NGINX Plus 才有 sticky cookie 指令。他展示的那個帶過期時間與 hash 值的 cookie 屬於後者,跟他圈起來的 ip_hash 那一行無關。後半句「hash 是依使用者 IP 算的」對 ip_hash 而言是對的。 依據: nginx ngx_http_upstream_module 文件的 ip_hash 指令說明(依 client 位址決定 server,IPv4 用前三個 octet);sticky 指令僅列於 NGINX Plus(commercial subscription)。 |
| 7. 問題三:多台 server 前面放 load balancer 9:08 |
「the load bouncer is going to create another one」 | load balancer(nginx)本身不會「建立」新的 server 副本,它只能把壞掉的後端暫時剔除、把流量導到活著的那台(max_fails / fail_timeout 的語意就是這樣)。真正「起一台新的」是 autoscaler 或容器編排(Kubernetes、AWS Auto Scaling Group 等)的工作,它們依 health check 結果補足副本數。作者在雲端平台的語境下把兩者合在一起講可以理解,但看 nginx 設定時不要以為那幾行會自動生出新 server。 依據: nginx ngx_http_upstream_module 文件:max_fails / fail_timeout 只影響「該 server 被視為不可用的時間」;nginx 沒有啟動後端程序的功能。 |
| 9. Broadcasting:用 RabbitMQ 跨 server 廣播 12:34 |
「broadcast it to the client to the websocket client like this okay nothing complicated that was very simple」 | 畫面上兩台 server 都用 channel.consume 訂閱同一個具名的 broadcast_queue。RabbitMQ 對「一個 queue、多個 consumer」的語意是工作佇列:每則訊息只會投遞給其中一個 consumer(輪流),所以照這段程式碼,一則訊息只會有一台 server 收到、只有那台的 client 會被廣播到,另一台的 client 收不到。要真正廣播應由 publisher 發到 fanout exchange,每台 server 各自宣告一個專屬 queue(exclusive)綁上去。影片沒展示 publish 端與 exchange 設定,可能有其他設定補上這點,所以標為存疑而非確定錯誤。 依據: RabbitMQ 官方教學 Tutorial 2 (Work Queues):同一 queue 的多個 consumer 採 round-robin dispatch;Tutorial 3 (Publish/Subscribe):廣播需 fanout exchange 與每個 consumer 各自的 queue。 |
5. 推薦三個下一步
1. 往下挖深:RabbitMQ exchange 型態與真正的 fanout 廣播
第 9 段的程式碼讓兩台 server consume 同一個 queue,在 RabbitMQ 裡是工作佇列語意而非廣播;要正確廣播得懂 fanout exchange 與每台各自綁 queue 的做法。
YouTube 搜尋:RabbitMQ fanout exchange tutorial RabbitMQ publish subscribe Node.js AMQP exchange types explained
2. 往旁邊對照:Socket.IO 與 Redis adapter 如何把這五件事包起來
影片手寫了 heartbeat、fallback、重連與跨 server 廣播;Socket.IO 內建 ping/pong、long-polling fallback 與 Redis adapter,對照後能看出哪些是通用需求、哪些是框架已解決的。
YouTube 搜尋:Socket.IO vs WebSocket Socket.IO Redis adapter scaling Socket.IO reconnection heartbeat explained
3. 往上應用:WebSocket 擴展到百萬連線的系統設計
影片停在兩三台 server 加 nginx;真實聊天/即時系統要處理連線數、記憶體、跨區域與 presence,這是把 load balancer、sticky session、broker 三招放大到生產規模的下一步。
YouTube 搜尋:scaling WebSockets to millions of connections WebSocket system design interview chat WebSocket load balancing sticky sessions production
- 接著看 ←How Web Sockets work | Deep Dive · 先懂一條 WebSocket 連線怎麼建立,再看它在 server 崩潰、多台時會怎麼壞
- 接著看 ←Keep Those WebSocket Connections Alive! · 那站專講 heartbeat;這站從 heartbeat 出發再加 SSE 備援、load balancer、broker
- 接著看 ←15 Fullstack Concepts Every Frontend Should Know · 那站帶過 WebSocket、EventSource、round robin 三個概念,這站把它們組成一套可靠性設計
- 接著看 →How to scale WebSockets to millions of connections · 這站停在兩三台 server 加 nginx 與 broker,那站放大到百萬連線
- 接著看 →Message Queues in System Design Interviews w/ Meta Staff Engineer · 這站把 RabbitMQ 當黑盒子做跨 server 廣播,那站解釋 queue 為什麼存在、怎麼選
- 相關 —Real Frontend System Design (from a Senior Engineer) · 都談 load balancer、round robin、horizontal scaling,一個在系統設計面試、一個在 WebSocket 實作