選型文最常見的寫法是列優點:GraphQL 彈性、gRPC 快、SSE 輕。問題是優點要放在某個位置才成立,換一個位置可能就變成缺點;而且「快」「輕」通常沒說是跟誰比、在什麼條件下比。

所以這七組實驗都用同一個做法:先把主角放在它該在的位置(一張訂單的一生裡的那條連線),再換成最接近的替代方案跑一次。每組都先確認公平——兩邊拿到的結果相同——再看差在哪。

只比優點不夠,要知道限制啊。而且實驗本身也會騙人:tablet-inbox 第一版有三處結果是實驗設計造成的,不是協定的差別。之後每組實驗都把「公平前提」寫成測試,兩邊結果不一樣就不比。

一、店家頁:GraphQL 對 REST 三支 API

一個畫面要店名、菜單、兩則評價。兩邊畫出的畫面相同(測試檢查),GraphQL 打 1 次請求、回應 347 bytes;REST 打 3 次、回應 1101 bytes,還多帶了 14 種畫面用不到的欄位。

但 GraphQL 不是免費的:它每次都要把查詢內容送上去(這次 231 bytes),REST 的 GET 沒有 body。延遲這組沒有量——本機 loopback 量不出「多打兩次往返」在真實網路上的成本。

二、內部呼叫:gRPC 對 JSON over HTTP

這組的結果跟一般印象不一樣。

訊息大小符合預期:protobuf 的回應只有 JSON 的 41%(89 對 217 bytes)。但延遲在本機各呼叫 1000 次,中位數 gRPC 0.182 ms、JSON 0.173 ms——JSON 還略低,差 0.009 ms,這個差距小到不能說誰比較快。

這不代表 gRPC 慢。本機 loopback、依序呼叫,量不到真實網路上的傳輸成本,也沒有併發。它代表的是:「gRPC 比較快」這句話需要條件,至少在這個條件下量不出來。我後來選 gRPC 做 api 到 catalog 這段,理由是強制的介面定義、訊息比較小、以及一定要設的 deadline。grpc.io 自己寫「By default, gRPC does not set a deadline」,不設的話對方卡住你就一直等。實驗場設了 1 秒:停掉 catalog 容器時,api 等滿這 1 秒才回 503——deadline 設多長,出事時顧客就多等多久;測試裡換成一個根本沒有服務的位址時,則是在 deadline 之前就失敗。

以前的文章寫過「gRPC 快 30%」「快 2–5 倍」,這次回頭改寫了:「小 50%、快 30%」是第 22 篇序列化格式本身的比較,「快 2–5 倍」找不到來源。整個呼叫快不快,要看條件。

三、訂單進度:SSE 對天真版 WebSocket 對短輪詢

21 秒內有 6 次狀態變化,其中「已付款」和「已接單」只差 0.3 秒。三種做法沒斷線時都看得到全部 6 個狀態。差別在斷線和輪詢間隔:

  • SSE 斷線一次:重連時瀏覽器自動帶上 Last-Event-ID,伺服器補送 1 則,6 個狀態一個不漏
  • 天真版 WebSocket 斷線一次:漏掉 ready。WebSocket 沒有內建補收,要自己設計(聊天室那段我用「最後收到的編號」補)
  • 每 2 秒短輪詢:11 次請求(5 次是 304 沒變),漏掉 paid——兩次變化的間隔比輪詢間隔短,輪詢只看得到「問的那一刻」

四、沒有公開網址的平板:長輪詢對短輪詢對 Webhook

做法請求數收到平均晚多久
長輪詢55 / 56 ms
短輪詢(每 2 秒)155 / 51113 ms
Webhook 打平板—打不到(ECONNREFUSED)—

30 秒內付款 5 張。Webhook 不是比較慢,是根本打不到。長輪詢的代價是請求要掛著,而中間的 proxy 會切閒置太久的連線(AWS ALB、nginx 的預設都是 60 秒),所以每次最多掛 25 秒。

五、派單服務掛掉:AMQP 對同步呼叫

連續付款 5 張,派單服務整段時間都掛著,之後才恢復。

同步呼叫版先呼叫派單、再寄收據:派單連不上,5 張付款確認全部失敗,收據一張都沒寄;派單恢復後也沒有任何機制記得哪些單沒派。AMQP 版:付款確認 5 張都成功、收據照寄 5 張、派單事件在 queue 裡等著,恢復後 5 張全部補派。

這組有一個簡化:假設 broker 本身是好的。broker 也掛的時候要靠 outbox(先把事件跟訂單一起寫進資料庫再發),這個實驗沒做。

六、外送員 GPS:MQTT 對 REST

5 位外送員各回報 10 秒、共 50 則,三種做法都收齊。每則實際流量:MQTT QoS 0 是 130 bytes、QoS 1 是 136 bytes、REST 是 468 bytes——MQTT QoS 0 是 REST 的 28%。QoS 1 多出來的,是 broker 回給外送員的確認。

這組有一個意外:REST 的連線數也是 5,不是 50。Node 的 fetch 預設 keep-alive,同一個外送員的請求會重用連線,「REST 每次都要重新連線」在這裡不成立。

真正讓人選 MQTT 的兩件事——電量、行動網路斷線重連的成本——在本機量不出來。

七、事件寫完才上線的讀者:Kafka 對 AMQP

25 則訂單事件都寫完之後,營運報表才上線,之後財務也上線。

  • Kafka:報表和財務各開一個新的讀者群組、從頭讀,都拿到 25 則,報表算得對
  • AMQP,讀者上線時才建 queue:一則都拿不到——發布時沒有 queue 綁著,訊息被丟掉,而 broker 照樣回 confirm
  • AMQP,事先替報表建好 queue(對 AMQP 最有利的條件):報表拿到 25 則;但財務是之後才想到的讀者,拿到 0 則;報表重啟想重算,也是 0 則——讀完、ack 就刪了

量不到的東西

這七組都在一台機器上跑,延遲是 loopback 上單次量測的相對關係,不能拿去推論真實網路。電量、行動網路、大量併發、broker 叢集,都沒有量。

反過來說,行為(收到幾則、漏掉哪個、打不打得到、拿不拿得到歷史)是測試檢查的,重跑結果相同——這些比延遲數字更值得引用。

七組做下來,幾乎都跟我想得滿不一樣的。

延伸閱讀