HUAN 讙 · 開發者
ADR-0001:WebSocket 只送通知,REST 才是事實來源
狀態
已採用
背景
裝置需要知道內容什麼時候變了。可行的做法有幾種:
- 每隔幾秒完整拉一次狀態。
- 用 WebSocket 把完整的狀態推給裝置。
- 用 WebSocket 推「有東西變了」,裝置再自己去拉。
決策
採用第三種。WebSocket 只傳這種訊息:
{ "type": "desired_state_changed", "version": 42 }
裝置收到後打 GET /api/v1/device/state 取得完整內容。REST API 是唯一的事實來源。
同時保留每 5 分鐘一次的保底同步,不完全依賴 WebSocket。
理由
輪詢太浪費。 一百台裝置每 10 秒拉一次,就是每秒十次全量查詢,而其中九成九的回應完全相同。
推完整狀態會有一致性問題。 推播是「射後不理」的:訊息在傳輸中遺失、裝置剛好在重連、或者兩次推播的順序顛倒,裝置就會停在一個錯誤的狀態,而且沒有人知道。要修這個問題就得加上確認、序號與重送——等於在 WebSocket 之上重新實作一次 TCP。
通知加拉取沒有這個問題。 通知遺失最壞的結果是延遲,因為保底同步一定會補上。順序顛倒也沒關係,因為裝置拉到的永遠是當下最新的完整狀態。裝置重連後只要拉一次就同步了,不需要重播任何訊息。
保底同步不是備援設計上的懶惰,而是承認長連線本來就會斷、會被中間設備靜默丟棄、會遇到 NAT 逾時。
後果
- 一次變更會產生一次推播加一次 REST 請求,比純推播多一趟往返。對於「幾分鐘才變一次」的數位看板,這完全不是問題。
- WebSocket 完全掛掉時,系統會退化成 5 分鐘輪詢一次,功能不受影響,只是變慢。
- 訊息很小,因此 WebSocket 連線本身的成本很低。
替代方案
Server-Sent Events:單向、比 WebSocket 簡單。但裝置的 heartbeat 需要往上傳,只用 SSE 就得再開一條 HTTP 通道。WebSocket 一條連線兩個方向,反而更單純。
長輪詢:可行,但每次逾時都要重建連線,在幾百台裝置的規模下會產生大量無謂的連線。