WebSocket 的可靠性工程:heartbeat、SSE 備援、load balancer、sticky session、message broker

文字解析 · 1 支影片 · 產生於 2026-10-01 00:54

🎧 語音解析📝 筆記✍️ 練習

🎧 語音解析 28m29s · TTS 講解與作者原聲交錯;點章節可跳。
邊聽邊看逐句講稿,點任一句從那裡開始
章節(9)

1. Outline

  1. 起點 · 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 設定。

推理鏈:每段留給下一段的線索

每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。

  1. WebSocket 是什麼、兩種典型用法 → 白板上的基準架構假設 server 永遠活著、連線永遠開著。可是這條長連線一旦建立,若 server 端悄悄死掉,client 手上那條 socket 會發生什麼事?它有辦法知道嗎?這是第一個要拆的邊界情況。
  2. 問題一:server 崩潰時 client 不知道 → 規則講完了,但「每隔一段時間送 ping、10 秒沒 pong 就重連」到底寫成什麼樣的程式碼?server 端要做什麼、client 端要做什麼?還有一個作者順口提到但沒展開的點:崩潰的 server「到不了正常關閉的程式碼」——在程式裡指的是哪一段?
  3. Heartbeat 程式碼:server 與 client → 程式碼寫好了,但還沒看到它真的動起來:ping/pong 在瀏覽器裡實際上多久來回一次?更重要的是,要怎麼在本機「模擬 server 沒回 pong」來證明那個 10 秒判斷真的會觸發重連?
  4. Demo:把 pong 改成 Pongo 觸發重連 → demo 結尾暴露了 heartbeat 的極限:只要 server 一直回錯,client 就一直重連,永遠拿不到資料。如果 WebSocket 這條路一直走不通,client 有沒有另一條路可以退而求其次、至少先把 server 推來的資料接到?
  5. 問題二:WebSocket 掛了改用 SSE 備援 → 切換規則有了,但 SSE 在程式裡怎麼接?瀏覽器端的 API 跟 WebSocket 長得像不像、server 要多開什麼端點?以及「失敗兩次就切換」跟第 3 段那份 heartbeat 程式碼要怎麼接在一起——實際跑起來會看到什麼?
  6. SSE 程式碼與 fallback demo → 到目前為止所有解法都假設只有一台 server:heartbeat 探的是它、SSE 連的也是它。如果它整台死掉,client 再聰明也沒有地方可以連。那麼下一個層次的問題是:能不能讓「server 死掉」這件事本身不再是單點——多準備幾台,由誰來決定 client 該連哪一台?
  7. 問題三:多台 server 前面放 load balancer → load balancer 解決了「連哪台活著的」,卻製造了一個新問題:現在有兩台以上同時活著,client 第一次連上 server1,斷線後重連時 load balancer 憑什麼會把它送回 server1 而不是 server2?如果送錯台,第 3 段那份「重連後補送 pending data」還接得上嗎?
  8. Sticky session:重連要回到同一台 → 有了 sticky session,每個 client 都穩穩黏在自己那台 server 上。但這反過來製造了最後一個問題:如果 API 1 上的某個 client 發了一則要給「所有人」的訊息,而其他人黏在 API 2 上——API 1 根本碰不到他們的連線,這則訊息要怎麼送過去?
  9. 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。

白板上「多個 client 連同一台 server、server 轉送訊息」的示意圖,是後面所有問題(crash、多台 server、broadcast)的基準架構。
1:00 · 白板上「多個 client 連同一台 server、server 轉送訊息」的示意圖,是後面所有問題(crash、多台 server、broadcast)的基準架構。
承上 前情提要裡的「HTTP 是一問一答的 request/response」被用上:作者用「不再反覆發 request,而是只建一條 socket 連線」來對比,讀者要先有 HTTP 的印象,才會懂 WebSocket 省下的是什麼。

推理因為作者一開頭就宣稱「WebSocket 比大家想的複雜,因為有幾個邊界情況」,所以他必須先建立一個「正常運作的 WebSocket 長什麼樣」的基準,之後每個邊界情況才有東西可以對照。他用兩種用法把基準畫出來:多個 client 連同一台 server 的 messenger(server 負責轉送),以及 server 主動推資料的天氣 app(單向推送)。白板上畫的是一顆綠色 Server 圓圈,兩個 Client 用虛線雙向箭頭連著它——這張圖就是後面所有問題的起點:連線是長期存在的、server 是唯一的中心。

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

WebSocket

網頁通訊端(全雙工長連線協定)

瀏覽器與 server 之間建立一次、之後雙方都能隨時互傳訊息的長連線協定。

WebSocket 由一個帶 Upgrade: websocket 標頭的 HTTP request 開始,server 回 101 Switching Protocols 後,同一條 TCP 連線就改走 WebSocket 框架格式,沒有 request/response 的配對概念。常見誤解是「WebSocket 比 HTTP 快」——其實省的是每次請求的交握與標頭開銷,以及讓 server 能主動推送;單次資料傳輸速度並沒有差別。它跟輪詢(polling)的差別在於不用 client 一直問「有新資料嗎」。

相關術語: socket connection (建立於)、request/response (相反)

出處:第 1 段「WebSocket 是什麼、兩種典型用法」

socket connection

通訊端連線

兩端之間一條持續開著的 TCP 通道,資料可以隨時往兩個方向流。

作者說「we establish one socket connection and can exchange the data between it」,指的就是這條長期存在的通道。在作業系統層面,socket 是程式用來收發網路資料的端點;WebSocket 是在這條 TCP socket 上再定義一層訊息框架。理解「連線是有狀態、會斷」這件事,是後面所有邊界情況(server 崩潰、重連、多台 server)的根源。

相關術語: WebSocket (被它使用)

出處:第 1 段「WebSocket 是什麼、兩種典型用法」

request/response

請求/回應(一問一答)

傳統 HTTP 的溝通方式:client 發一個請求,server 回一個回應,之後這次交換就結束。

作者用「we're not making multiple requests back and forth anymore」對比 WebSocket。在 request/response 模型裡 server 無法主動開口,client 想拿新資料只能再問一次;這就是為什麼即時類應用(聊天、天氣、股價)會需要 WebSocket 或其他推送機制。

相關術語: WebSocket (對照組)

出處:第 1 段「WebSocket 是什麼、兩種典型用法」

push

推送(server 主動送資料)

由 server 在有新資料時主動送到 client,而不是等 client 來問。

作者的天氣 app 例子就是 push:server 從別處取得即時資料,「push this data into the client」。push 是 WebSocket 最常被需要的能力,但不是 WebSocket 獨有的——這一段先記住「推送」和「雙向」是兩個可以分開看的性質,之後比較其他技術時會用到。

相關術語: request/response (相反)、WebSocket (一種實作方式)

出處:第 1 段「WebSocket 是什麼、兩種典型用法」

留給下一段 白板上的基準架構假設 server 永遠活著、連線永遠開著。可是這條長連線一旦建立,若 server 端悄悄死掉,client 手上那條 socket 會發生什麼事?它有辦法知道嗎?這是第一個要拆的邊界情況。

2. 問題一:server 崩潰時 client 不知道 1:40–2:36

第一個難題:client 已連上 WebSocket,但 server 突然崩潰。最好的情況是 client 能偵測到,但多數時候不行,因為崩潰的 server 來不及正常關閉連線。解法是 heartbeat:client 定期送 ping,server 回 pong;若 10 秒內沒收到 pong,client 就主動關閉連線並嘗試重連。

白板上 heartbeat 的 ping/pong 來回示意,標出「10 秒沒 pong 就重連」的判斷點,是本段解法的核心。
2:15 · 白板上 heartbeat 的 ping/pong 來回示意,標出「10 秒沒 pong 就重連」的判斷點,是本段解法的核心。
承上 承接上一段留下的問題「server 悄悄死掉,client 手上那條 socket 會怎樣?」——這段直接回答:多數情況 client 根本不會知道,因為連線是長期開著的,沒有人來告訴它對方已經不在了。

推理因為上一段建立的基準是「一條長期開著的 socket 連線」,所以作者接著問:這條連線的另一端如果崩潰了呢?他先區分兩種情況:最好的情況是 client 偵測得到;但多數情況偵測不到,理由是「server 崩潰時沒辦法正常關閉連線」——正常的關閉需要 server 送出關閉訊號,崩潰的 server 送不出來。既然對方不會主動報死訊,就只能由 client 定期去探:這就是 heartbeat。白板上畫的規則很具體:client 每隔一段時間送 ping,WebSocket API 回 pong;若 10 秒內沒等到 pong,client 就自己關掉連線並重連。圖上 WebSocket API 下面打了一個叉,表示這正是在模擬 server 死掉的情境。

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

heartbeat

心跳(定期探活)

client 定期送一個小訊息給 server、要求對方回覆,用「有沒有準時回」來判斷連線是否還活著的機制。

heartbeat 解決的是「連線看起來開著、其實另一端已經不在」的問題。它的兩個參數是送的間隔和等待逾時(作者用 10 秒),逾時就當作斷線、由 client 主動關閉並重連。這是 client 端能做的唯一可靠偵測手段,因為崩潰的 server 不會主動通知。常見誤解是以為 WebSocket 會自己處理這件事——協定有定義 Ping/Pong 控制幀,但瀏覽器端無法主動送,所以實務上仍要在應用層自己做。

相關術語: ping / pong (由它組成)、socket connection (用來監看)

出處:第 2 段「問題一:server 崩潰時 client 不知道」

ping / pong

探測/回應訊息

heartbeat 裡的一問一答:client 送 ping,server 必須回 pong,收不到 pong 就代表對方可能死了。

作者這裡的 ping/pong 是應用層訊息(普通的 WebSocket 文字訊息,內容是 "ping" / "pong"),不是 WebSocket 協定內建的 Ping (0x9) / Pong (0xA) 控制幀。Node.js 的 ws 套件在 server 端可以送協定層 ping,但瀏覽器的 WebSocket API 沒有這個方法,所以跨端一致的做法是自己定義訊息。判斷邏輯是「上一次 pong 的時間距現在是否超過門檻」,而不是「這次 ping 有沒有立刻回」。

相關術語: heartbeat (組成它)

出處:第 2 段「問題一:server 崩潰時 client 不知道」

reconnect

重連

client 判定連線已死後,自己關閉舊 socket 並重新建立一條新的 WebSocket 連線。

作者說「close the connection manually on the client and try to reconnect」。重連是 client 的責任,因為 server 死了不可能幫你做。實務上重連通常會加退避(backoff,例如 1、2、4 秒遞增)避免 server 剛恢復就被大量 client 同時打爆,作者的簡化版本沒有這一步。重連還帶出另一個問題:斷線期間 client 產生的資料怎麼辦——這一段先留著。

相關術語: heartbeat (被它觸發)、socket connection (重新建立)

出處:第 2 段「問題一:server 崩潰時 client 不知道」

留給下一段 規則講完了,但「每隔一段時間送 ping、10 秒沒 pong 就重連」到底寫成什麼樣的程式碼?server 端要做什麼、client 端要做什麼?還有一個作者順口提到但沒展開的點:崩潰的 server「到不了正常關閉的程式碼」——在程式裡指的是哪一段?

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。

server.js:connection / message 回 pong / close 處理的完整結構,作者說「錯誤時常到不了這個 block」,要看到程式碼才知道指的是哪段。
2:55 · server.js:connection / message 回 pong / close 處理的完整結構,作者說「錯誤時常到不了這個 block」,要看到程式碼才知道指的是哪段。
client 端 ping 迴圈:比對 last pong 時間是否超過 10 秒、否則再送 ping,是 heartbeat 判斷的實際實作。
3:53 · client 端 ping 迴圈:比對 last pong 時間是否超過 10 秒、否則再送 ping,是 heartbeat 判斷的實際實作。
承上 承接上一段留下的兩個問題:heartbeat 規則在程式裡長什麼樣、以及「崩潰的 server 到不了正常關閉的程式碼」指的是哪一段。這段把 server.js 與 index.html 攤開來,逐一對應到上一段白板上的 ping / pong / 10 秒 / 重連。

推理因為上一段把規則定成「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

health check endpoint

健康檢查端點

一個只回 200 的普通 HTTP 路由(這裡是 /health-check),讓別人能用最便宜的方式確認 server 活著。

在作者的 client 程式裡,重連流程是先 fetch("/health-check") 成功才重開 WebSocket,失敗就過一段時間再試。這樣做的好處是:HTTP 一次請求就結束,不會留下一堆半死的 socket;而且 server 剛重啟時通常 HTTP 會比 WebSocket 早可用。health check 也是後面基礎設施(例如負載平衡)判斷後端狀態的標準手段,這裡先記住它存在。

相關術語: reconnect (前置檢查)

出處:第 3 段「Heartbeat 程式碼:server 與 client」

close event

連線關閉事件

WebSocket 正常關閉時兩端都會收到的事件(server 端 ws.on("close")、client 端 socket.onclose)。

作者指著 server.js 的 ws.on("close") 說「有時候根本到不了這段程式碼」——因為 close 事件是正常握手關閉(交換 Close 幀)才會觸發,程序被 kill、斷電、網路中斷都不會有。這就是為什麼不能只靠 onclose 來偵測斷線,必須配合 heartbeat(見第 2 段)。client 端的 onclose 也一樣:對方崩潰時它可能很久之後才被觸發,甚至永遠不會。

相關術語: heartbeat (被它補足)

出處:第 3 段「Heartbeat 程式碼:server 與 client」

localStorage

瀏覽器本機儲存

瀏覽器提供的 key-value 儲存,資料留在使用者機器上,重新整理或關閉分頁後仍在。

作者用它當斷線期間的暫存:連線斷了時把要送的資料先寫進 localStorage,socket 重新 open 時再讀出來當 pending data 送給 server。這是最簡單的離線佇列。限制是容量小(一般約 5MB)、同步 API、只存字串(要 JSON.stringify),而且只保證資料不丟,不保證送達順序或不重複。

相關術語: pending data (存放處)

出處:第 3 段「Heartbeat 程式碼:server 與 client」

pending data

待送資料

斷線期間 client 累積、等連線恢復後要補送給 server 的資料。

在 index.html 裡,socket.onopen 一觸發就先送出 pending data。這個設計把「連線會斷」當成常態而不是例外:client 不因為斷線而丟資料,只是延後送。它和 reconnect(見第 2 段)合起來才是完整的容錯:reconnect 負責把通道接回來,pending data 負責把斷線期間的內容補上。

相關術語: localStorage (存放於)、reconnect (在其之後送出)

出處:第 3 段「Heartbeat 程式碼:server 與 client」

留給下一段 程式碼寫好了,但還沒看到它真的動起來:ping/pong 在瀏覽器裡實際上多久來回一次?更重要的是,要怎麼在本機「模擬 server 沒回 pong」來證明那個 10 秒判斷真的會觸發重連?

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。

DevTools console 顯示「receiving Pongo → no pong received in 10 seconds → reconnecting」的實際訊息序列,證明 heartbeat 判斷真的觸發。
5:06 · DevTools console 顯示「receiving Pongo → no pong received in 10 seconds → reconnecting」的實際訊息序列,證明 heartbeat 判斷真的觸發。
承上 承接上一段留下的「要怎麼在本機模擬 server 沒回 pong、證明 10 秒判斷真的會觸發重連」:這段作者實際跑起來,先看正常的 ping/pong 節奏,再故意把 server 回的字改掉。

推理因為上一段的判斷邏輯是「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。

fault tolerant

容錯的

系統某個部分出錯(例如 server 崩潰或回錯訊息)時,整體仍能繼續運作或自行恢復,而不是卡死。

作者用「this is how we can make it more fault tolerant」總結 heartbeat + reconnect 的效果:server 出問題時 client 不會停在一條死掉的連線上,而是自己偵測、自己接回去。容錯不等於「不會出錯」,而是「出錯後有定義好的恢復路徑」。demo 也示範了容錯機制的極限——如果錯誤是持續的(server 一直回 pongo),恢復路徑會一直重跑,這時需要的是別的策略,而不是更努力地重連。

相關術語: heartbeat (實現手段之一)、reconnect (實現手段之一)

出處:第 4 段「Demo:把 pong 改成 Pongo 觸發重連」

Network tab

瀏覽器開發者工具的網路分頁

Chrome DevTools 裡列出頁面所有網路請求的面板,WebSocket 連線會顯示為一筆持續的項目,點進去可看每一則收發的訊息。

作者用它證明「只建立一條連線、在上面交換 ping/pong」:Network tab 裡 WebSocket 只有一筆(狀態 101),不像 HTTP 每次請求一筆;點進 Messages 分頁可以看到每 5 秒一組 ping 上行、pong 下行。這是除錯 WebSocket 最直接的工具,能看到原始訊息內容與方向。

相關術語: WebSocket (用來觀察)

出處:第 4 段「Demo:把 pong 改成 Pongo 觸發重連」

留給下一段 demo 結尾暴露了 heartbeat 的極限:只要 server 一直回錯,client 就一直重連,永遠拿不到資料。如果 WebSocket 這條路一直走不通,client 有沒有另一條路可以退而求其次、至少先把 server 推來的資料接到?

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 當然也要支援。

白板上「ping 失敗兩次 → 放棄 → 切到 SSE」的流程圖,是本段切換策略的視覺總結。
6:52 · 白板上「ping 失敗兩次 → 放棄 → 切到 SSE」的流程圖,是本段切換策略的視覺總結。
承上 承接上一段留下的「WebSocket 一直走不通時,client 有沒有另一條路至少先把 server 推來的資料接到」:作者的答案是 Server-Sent Events——一條只能 server 推、client 收的連線,剛好對應第 1 段就分開看的「推送」與「雙向」兩個性質:放棄雙向,保住推送。

推理因為上一段的 demo 顯示 heartbeat 失敗後只會無止盡重連,所以作者接著要一個「放棄 WebSocket 之後的去處」。他先解釋 SSE 是什麼:Google 會看到很多「WebSocket vs SSE」的比較文,兩者都能把資料推到瀏覽器,但不是競爭關係——WebSocket 是雙向(聊天室),SSE 只能 server → 瀏覽器(股票報價、Twitter 時間軸),client 無法透過同一條連線送資料回去,要送只能另開 API route。然後定義切換規則,白板上畫得很清楚:ping 沒等到 pong,這條線就打叉;過一陣子再送一次 ping;兩次都失敗就「暫時放棄」WebSocket,改連旁邊那顆 SSE 圓圈。當然 server 也要提供 SSE 端點。

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

Server-Sent Events (SSE)

伺服器推送事件

一種讓 server 透過一條持續開著的 HTTP 回應、單向不斷把事件推給瀏覽器的標準技術。

SSE 的本質是一個永不結束的 HTTP GET,回應標頭 Content-Type: text/event-stream,內容是一行行 data: ... 的文字,每個空行代表一個事件結束。瀏覽器端用 EventSource 物件接收。跟 WebSocket(見第 1 段)比:SSE 只能下行、只能傳文字、但走純 HTTP 所以更容易穿過代理與防火牆,而且內建自動重連與事件 ID 續傳。作者把它定位為「不是競爭者」很準確:需要雙向就 WebSocket,只需要下行推送就 SSE,兩者也能像本段這樣一主一備。

相關術語: WebSocket (對照組)、push (一種實作方式)、fallback (在此用作)

出處:第 5 段「問題二:WebSocket 掛了改用 SSE 備援」

fallback

備援(退而求其次的方案)

主要方法失敗到某個程度後,自動改用的次要方法。

作者的規則是:ping 失敗兩次就「abandon this for some time and switch to server-sent events」。fallback 設計有三個要決定的事:什麼條件算失敗(這裡是連續兩次沒 pong)、退到哪裡(SSE)、以及什麼時候回頭再試主要方案(作者只說「暫時」)。好的 fallback 會降級功能而不是全部失效——這裡降的是「上行」能力,保住的是「即時接收」。

相關術語: heartbeat (由它觸發)、Server-Sent Events (SSE) (退到)

出處:第 5 段「問題二:WebSocket 掛了改用 SSE 備援」

bidirectional vs unidirectional

雙向 vs 單向

雙向指同一條連線兩端都能主動送資料(WebSocket);單向指只有一端能送(SSE 只有 server → 瀏覽器)。

這是作者區分兩種技術的核心軸線。雙向的代價是協定較特殊(需要 Upgrade 交握、部分中介設備不支援);單向的好處是就是 HTTP,哪裡都能走。判斷應用要哪一種的簡單問法:client 是否需要頻繁、低延遲地把小訊息送上去?聊天、協作編輯、遊戲要;報價、通知、時間軸不要。要注意「單向」指的是那條連線,不代表 client 不能用別的 HTTP 請求送資料。

相關術語: WebSocket (雙向的例子)、Server-Sent Events (SSE) (單向的例子)

出處:第 5 段「問題二:WebSocket 掛了改用 SSE 備援」

留給下一段 切換規則有了,但 SSE 在程式裡怎麼接?瀏覽器端的 API 跟 WebSocket 長得像不像、server 要多開什麼端點?以及「失敗兩次就切換」跟第 3 段那份 heartbeat 程式碼要怎麼接在一起——實際跑起來會看到什麼?

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 的另一個可靠方法。

client 端 connectSSE 函式:new EventSource('/events') 加 onmessage / onerror,看到程式碼才知道 SSE 的 API 形狀跟 WebSocket 有多像。
7:40 · client 端 connectSSE 函式:new EventSource('/events') 加 onmessage / onerror,看到程式碼才知道 SSE 的 API 形狀跟 WebSocket 有多像。
console 顯示 failed attempts 1→2→3 後「switch to SSE fallback」並開始收到事件,是整個切換流程成功的證據。
8:32 · console 顯示 failed attempts 1→2→3 後「switch to SSE fallback」並開始收到事件,是整個切換流程成功的證據。
承上 承接上一段留下的三個問題:SSE 在瀏覽器端的 API 長什麼樣、server 要多開什麼端點、以及「失敗幾次就切換」怎麼接到第 3 段那份 heartbeat 程式碼上。這段把 sse-example 攤開來跑一遍。

推理因為上一段定義了「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

EventSource

瀏覽器接收 SSE 的物件

瀏覽器內建的 JavaScript 物件,new EventSource(url) 就會對該 URL 建立一條 SSE 連線並持續收事件。

它的介面刻意跟 WebSocket 相似:onopen、onmessage、onerror、close(),所以作者說「API 看起來很像 WebSocket」。差別是沒有 send()(單向,見第 5 段),而且預設會在斷線後自動重連,並用 Last-Event-ID 標頭告訴 server 上次收到哪一筆。作者的程式在 onerror 裡手動 close 再重連,等於接管了這個內建行為。要注意 EventSource 只能發 GET、不能自訂標頭,需要認證時通常靠 cookie 或把 token 放在 query string。

相關術語: Server-Sent Events (SSE) (客戶端 API)、WebSocket (介面相似)

出處:第 6 段「SSE 程式碼與 fallback demo」

failed attempts counter

失敗次數計數器

client 端記錄連續幾次 heartbeat 失敗的變數,達到門檻就觸發 fallback 而不是再重連。

這是把第 2 段的 heartbeat 和第 5 段的 fallback 接起來的那個零件:每次「10 秒沒 pong」就 +1 並重連,達到門檻(程式裡是 3)就改呼叫 connectSSE()。它讓系統區分「偶爾抖一下」和「這條路真的不通」。實務上還要決定什麼時候歸零——成功收到 pong 就歸零、以及切到 SSE 一段時間後要不要回頭再試 WebSocket(作者說「暫時放棄」但程式沒實作回頭)。

相關術語: heartbeat (累計其失敗)、fallback (達門檻時觸發)

出處:第 6 段「SSE 程式碼與 fallback demo」

留給下一段 到目前為止所有解法都假設只有一台 server:heartbeat 探的是它、SSE 連的也是它。如果它整台死掉,client 再聰明也沒有地方可以連。那麼下一個層次的問題是:能不能讓「server 死掉」這件事本身不再是單點——多準備幾台,由誰來決定 client 該連哪一台?

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。

白板上「client → load balancer → 多台 WebSocket API,其中一台 down 後起新副本」的架構圖,是本段與下一段的共同基礎。
9:20 · 白板上「client → load balancer → 多台 WebSocket API,其中一台 down 後起新副本」的架構圖,是本段與下一段的共同基礎。
承上 承接上一段留下的「能不能讓 server 死掉不再是單點——多準備幾台,由誰決定 client 連哪一台」:作者的答案是在多台 WebSocket API 前面放一個 load balancer,client 只認得它的 URL。

推理因為前面所有解法都只能在單台 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,作者說這「也很方便」。

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 決定這次落到哪台活著的機器。

load balancer

負載平衡器

站在多台後端 server 前面、接收所有 client 請求並依策略分配給某一台的中介元件。

作者用 nginx 當例子:client 只連 nginx(listen 80 的 /ws),nginx 再 proxy_pass 到 upstream 裡的 server1:3001 或 server2:3002。它同時做兩件事:分流(依 least_conn 等策略)與健康判斷(max_fails / fail_timeout 把壞掉的後端暫時剔除)。對 WebSocket 而言,load balancer 必須支援 HTTP Upgrade 轉發,並且要注意它自己也會有閒置逾時(nginx 的 proxy_read_timeout 預設 60 秒),長時間沒訊息的連線會被它切斷——這又是 heartbeat(見第 2 段)的另一個用處。

相關術語: horizontal scaling (前提)、least connection (使用策略)、health check endpoint (用來判斷後端)

出處:第 7 段「問題三:多台 server 前面放 load balancer」

horizontal scaling

水平擴展

靠增加更多台同樣的 server 來承接更多負載(相對於把單台機器升級的垂直擴展)。

作者說「let's say you're trying to scale horizontally, so you're going to create another WebSocket API」。水平擴展帶來的新問題正是這段開始要處理的:多台之後誰來分流、壞掉的那台怎麼剔除、client 怎麼不用知道有幾台。對 WebSocket 這種有狀態的長連線來說,水平擴展比無狀態 HTTP 難,因為「這個 client 連在哪台」變成了一個需要被記住的狀態。

相關術語: load balancer (需要它)、fault tolerant (提升)

出處:第 7 段「問題三:多台 server 前面放 load balancer」

least connection

最少連線數策略

load balancer 把新連線分給目前開著連線數最少的那台 server 的分配策略。

nginx 裡寫 least_conn。作者特別說「不要 round robin」:round robin 是不看狀態地輪流分配,對短請求公平,但 WebSocket 連線壽命長,輪流分久了各台身上掛著的連線數會失衡。least connection 直接以「現在還掛著幾條」為依據,天然適合長連線。缺點是它假設每條連線的負擔差不多;如果有些 client 特別吵,還會需要更進階的加權策略。

相關術語: round robin (相反)、load balancer (實作於)

出處:第 7 段「問題三:多台 server 前面放 load balancer」

round robin

輪詢分配

load balancer 把請求依序輪流分給每台 server 的最簡單策略,不考慮各台目前的負載。

這是 nginx upstream 沒有指定策略時的預設。它適合每個請求都很短、彼此差不多的 HTTP 服務。作者在 WebSocket 情境下排除它,理由是長連線會讓「輪流分配」跟「實際負載」脫鉤:第 1、3、5 條連線可能都還活著掛在 server1 上,第 2、4 條早就斷了,server2 反而閒著。

相關術語: least connection (相反)

出處:第 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 沒有啟動後端程序的功能。
留給下一段 load balancer 解決了「連哪台活著的」,卻製造了一個新問題:現在有兩台以上同時活著,client 第一次連上 server1,斷線後重連時 load balancer 憑什麼會把它送回 server1 而不是 server2?如果送錯台,第 3 段那份「重連後補送 pending data」還接得上嗎?

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 算出。

nginx 設定檔 upstream 區塊裡的 ip_hash 一行,是 sticky session 的實際設定,聽描述不知道長什麼樣。
10:30 · nginx 設定檔 upstream 區塊裡的 ip_hash 一行,是 sticky session 的實際設定,聽描述不知道長什麼樣。
承上 承接上一段留下的「兩台都活著時,client 重連憑什麼回到原本那台」:作者的答案是 sticky session——讓 load balancer 記得「這個 client 屬於哪台」,重連時送回去。

推理因為上一段引入了多台同時運作的 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 不解決。

sticky session

黏性工作階段(固定後端)

load balancer 記住某個 client 曾被分到哪台後端,之後它的請求(包括重連)都盡量送回同一台。

對 WebSocket 這種有狀態長連線很重要:server 上掛著這個 client 的連線物件與相關狀態,換台就得重建。實作上有兩大類:依 client 屬性算 hash(例如 ip_hash,不需要 client 配合)或發 cookie 記後端識別(需要 client 帶回,但更精確、不受 NAT 影響)。要記住它是「盡量」不是「保證」——後端死掉時 load balancer 還是會改派,所以真正重要的狀態不該只放在單一台 server 的記憶體裡。

相關術語: load balancer (實作於)、ip_hash (一種實作)、reconnect (讓其回到原台)

出處:第 8 段「Sticky session:重連要回到同一台」

ip_hash

依 IP 雜湊分配

nginx upstream 的一種策略:用 client IP 算 hash 決定後端,同一個 IP 永遠落在同一台。

設定就是在 upstream 區塊裡寫一行 ip_hash;,取代上一段的 least_conn。它不使用 cookie、client 完全無感,因此最簡單;缺點是同一個出口 IP(公司、學校、行動網路 NAT)後面的所有人都會被綁到同一台,負載可能極不平均,而且使用者換網路就會被送到別台。IPv4 只取前三個 octet 計算。當你需要的是「同一個瀏覽器」而不是「同一個 IP」的黏性時,應改用 cookie 式做法。

相關術語: sticky session (一種)、least connection (取代)、cookie (不使用)

出處:第 8 段「Sticky session:重連要回到同一台」

cookie

瀏覽器自動帶回的小型鍵值

server 透過 Set-Cookie 存在瀏覽器裡、之後每個對同網域的請求都會自動附上的一小段資料。

在 sticky session 的語境裡,cookie 式黏性是 load balancer 第一次回應時發一個記錄後端識別(或其 hash)的 cookie,之後依這個值路由;可以設過期時間,過期後重新分配。這比 ip_hash 精確(綁的是瀏覽器不是 IP),但需要 client 帶回 cookie——瀏覽器的 WebSocket 交握會自動附上同網域 cookie,所以對瀏覽器 client 沒問題,非瀏覽器 client 要自己處理。作者示範的那個帶過期時間與 hash 值的 cookie 就是這類。

相關術語: sticky session (另一種實作載體)、ip_hash (對照組)

出處:第 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)。
留給下一段 有了 sticky session,每個 client 都穩穩黏在自己那台 server 上。但這反過來製造了最後一個問題:如果 API 1 上的某個 client 發了一則要給「所有人」的訊息,而其他人黏在 API 2 上——API 1 根本碰不到他們的連線,這則訊息要怎麼送過去?

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 做一樣的事。

白板上「API 1 → RabbitMQ → API 2 → 各自的 client」的廣播架構圖,說明為什麼需要 broker 在中間。
11:45 · 白板上「API 1 → RabbitMQ → API 2 → 各自的 client」的廣播架構圖,說明為什麼需要 broker 在中間。
server 端 AMQP 連線 → createChannel → consume 後把 message content 轉發給 WebSocket client 的程式碼,是 broker 落地的具體實作。
12:32 · server 端 AMQP 連線 → createChannel → consume 後把 message content 轉發給 WebSocket client 的程式碼,是 broker 落地的具體實作。
承上 承接上一段留下的「API 1 收到一則要給所有人的訊息,但其他人黏在 API 2 上,API 1 碰不到他們的連線,怎麼送過去」:作者的答案是在 server 之間放一個 message broker,讓 API 之間互相轉告。

推理因為上一段的 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 做的事一樣。

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 讓多台之間仍能像一台一樣廣播——每個解法都在補上一個解法製造出來的新洞。

broadcasting

廣播(一對所有)

一則訊息要送給所有連線中的 client,而不只是某一個。

在單台 server 時廣播很簡單:對 wss.clients 全部 send 一遍。問題出在水平擴展之後:每台 server 只握有自己那一部分 client 的連線,作者說「this client is not connected to this API, it's maybe connected to this one」,所以「所有 client」變成分散在多台上的集合,需要一個機制讓每台都收到這則訊息、各自送給自己的人。聊天室、即時通知、協作編輯都是典型的廣播需求。

相關術語: message broker (跨台時需要)、sticky session (因它而分散)

出處:第 9 段「Broadcasting:用 RabbitMQ 跨 server 廣播」

message broker

訊息代理(訊息中介)

站在多個服務之間、負責收下訊息並轉交給訂閱者的獨立元件,讓服務之間不必直接互相連線。

作者舉 RabbitMQ 與 Redis(Redis 的 Pub/Sub 功能)為例。broker 把「每台都要知道其他每一台」的 N×N 關係變成「每台只認得 broker」的 N×1,新增或移除 server 都不用改其他台的設定。代價是多一跳延遲與多一個要維運的元件(作者說「near real time」就是指這一跳)。要注意 broker 的投遞語意:同一個 queue 多個 consumer 是「分工」(每則只給一個),要「每台都收到」得用 fanout / pub-sub 模式,各台各自一個 queue 或 subscription。

相關術語: RabbitMQ (一種)、broadcasting (用來實現)、horizontal scaling (解決其副作用)

出處:第 9 段「Broadcasting:用 RabbitMQ 跨 server 廣播」

RabbitMQ

一款開源訊息代理軟體

實作 AMQP 協定的 message broker,透過 exchange 與 queue 的組合支援分工、路由、廣播等多種投遞模式。

作者說「RabbitMQ 可以、Redis 也可以,但我個人認為 RabbitMQ 最適合」。它的核心概念:publisher 把訊息送到 exchange,exchange 依類型(direct / fanout / topic)把訊息複製到綁定的 queue,consumer 從 queue 取。廣播對應 fanout exchange + 每個 consumer 一個 queue。跟 Redis Pub/Sub 比,RabbitMQ 可以把訊息持久化、有確認機制(ack)、consumer 暫時不在時訊息還在 queue 裡;Redis Pub/Sub 更輕但「當下沒在聽就沒了」。

相關術語: message broker (一種)、AMQP (使用協定)

出處:第 9 段「Broadcasting:用 RabbitMQ 跨 server 廣播」

AMQP

進階訊息佇列協定

RabbitMQ 使用的開放式訊息協定,定義了 connection、channel、exchange、queue 等概念與線上格式。

程式碼裡的 amqp.connect("amqp://localhost") 就是用 Node.js 的 amqplib 套件建立一條 AMQP 連線;一條連線上可以開多個 channel(輕量的虛擬連線,程式裡用它做 assertQueue / consume)。作者在口語裡說「establish mqp connection」指的就是它。AMQP 是協定名,RabbitMQ 是實作它的軟體,就像 WebSocket(見第 1 段)是協定、ws 套件是實作。

相關術語: RabbitMQ (被其實作)、WebSocket (同為協定)

出處:第 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。
留給下一段 總結收束

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

📄 全部影片 · 主題區: 網路與 API 基礎