問題
我做了一個實驗,要比較「商家平板拉新訂單」的三種做法:長輪詢、短輪詢、平台用 webhook 打平板。 第一版跑出來的結果,有三處是實驗設計造成的,不是協定本身的差別——不仔細看就會寫進文章當結論。
| # | 第一版看到的 | 看起來像 | 真正原因 |
|---|---|---|---|
| 1 | 短輪詢只收到 4/5 張單 | 「短輪詢會漏單」 | 時間窗只比最後一張付款晚 1.4 秒,短輪詢間隔 2 秒——實驗先結束了,它還沒來得及問 |
| 2 | 10 秒 5 張單,兩邊請求數都是 5 | 「長輪詢沒有省請求」 | 時間表太忙,一直有新單,長輪詢每次都馬上回。長輪詢的優勢在閒置時才出現,而真實的商家大部分時間是閒的 |
| 3 | webhook 打平板失敗 | 「平板沒公開網址所以連不到」 | 用的是 1 號 port,fetch 本身就禁止連(bad port),請求根本沒送出去——量到的是 fetch 的限制,不是網路狀況 |
第 3 例的依據在 WHATWG Fetch 規格的「Port blocking」一節:HTTP(S) 請求的 port 若在 bad port 表上就直接擋下,
而 1 號(tcpmux)就在那張表裡,同表還有 21、22、25 這類常見服務的 port
(Fetch Standard — bad port)。
我那次拿到的錯誤是 TypeError: bad port,跟「連不上」完全是兩回事。
第 3 例和 比錯基準 裡的「管道錯」是同一族:檢查有跑、有輸出,量的卻是別的東西。
解法
-
時間窗:最後一個事件之後至少留一個「最慢那一方的週期」;寫成程式檢查,太短直接丟錯
const last = Math.max(...schedule.paidAtMs); if (schedule.windowMs < last + schedule.shortIntervalMs + 300) throw new Error('時間窗太短'); -
工作負載要像真實情況:加入閒置期(前 9 秒 5 張單、後 21 秒沒有)。修正後:長輪詢 5 次請求、短輪詢 15 次
-
「連不到」要真的連不到:先開一個伺服器拿系統分配的 port、再關掉,得到真正的
ECONNREFUSED;並在測試裡 assert 失敗原因,而不只是「失敗了」
為什麼這樣解
- 對照實驗回答的是「A 和 B 差在哪」。只要條件對其中一方不利(時間不夠、負載不典型、量錯東西),差別就變成實驗設計的產物
- 三個錯都會讓結果「看起來合理」:短輪詢漏單、長輪詢沒省多少、平板連不到——每一個都符合某種直覺,所以特別容易放行
- 把公平前提寫成程式檢查,而不是寫在說明裡:下次改時間表的人(包括我)不會再踩
怎麼預防再次發生
寫任何對照實驗前,先列出並寫成檢查:
- 兩邊做的是同一件事嗎? 我的每個對照實驗都有「公平前提」測試:組頁面的實驗檢查兩邊組出的畫面相同、查價的實驗檢查兩邊回傳結果相同、平板收單的實驗檢查兩邊都收齊訂單
- 條件有沒有偏袒一方? 時間窗、負載形狀、輪詢間隔
- 失敗的原因是我以為的那個嗎? 不只 assert「失敗」,要 assert 失敗原因(
ECONNREFUSED、DEADLINE_EXCEEDED) - 跟機器有關的數字(延遲)只看相對關係,而且至少跑兩次確認方向一致
相關
- 檢查跑了、有輸出、結論還是錯的:單一檢查量錯基準的六種形態;本篇的第 3 例同族
- gRPC 錯誤細節放中文,整個錯誤變 UNKNOWN:另一個「要 assert 錯誤碼本身,而不只是有錯」的例子
- WebSocket 即時通訊協定:輪詢、長輪詢與 WebSocket 的基本差異