選 SOAP?XML-RPC?REST?gRPC?答案應由通訊樣式驅動。本章把常見技術擺到第 3 章那張樣式座標圖上,並先建立挑選原則。

Figure 4.5:微服務通訊樣式與對應實作技術

挑選通訊技術的五個原則#

1. 讓向後相容變容易#

新增欄位等簡單操作不應讓舊客戶端出錯,並且最好能在部署到正式環境前驗證相容性。

2. 讓介面顯式(Explicit Interface)#

  • 消費者要清楚這個服務暴露什麼
  • 維護者要清楚什麼不能改
  • 強烈建議使用 schema,加上適度文件

3. 保持 API 技術中立#

避免綁死實作技術——今年 .NET 不代表五年後不換 Go 或 Rust。

4. 讓服務「對消費者」好用#

提供 client library 可以降低使用門檻,但要警惕:library 經常成為耦合來源。

5. 隱藏內部實作細節#

  • 暴露內部結構 = 增加耦合
  • 任何強迫你曝光內部表示的技術都該避免

遠端程序呼叫(RPC)#

RPC 把遠端呼叫包裝成跟本地呼叫一樣的形式。常見:SOAP、gRPC、Java RMI、Thrift。

  • 多數 RPC 需要顯式 schema(gRPC:Protocol Buffers;SOAP:WSDL)
  • 透過 schema 可自動產生 client/server stub
  • AVRO RPC 比較特別:會把 schema 與 payload 一起送出,client 可動態解讀

挑戰#

  • 技術耦合:Java RMI 把 client 與 server 綁在 JVM 上;gRPC、SOAP、Thrift 互通性較好
  • 「本地呼叫不是遠端呼叫」
    • 抽象藏太深會讓開發者忽略網路成本
    • 「網路是可靠的」是分散式運算八大謬誤的第一條——網路會慢、會壞、會丟封包
  • 脆弱性(Brittleness)
    • Java RMI 中改 createCustomer 的簽章 → 所有 client stub 都得重新產生
    • 即使你只是刪掉 Customer 內沒人用的 age 欄位 → consumer 反序列化照樣壞
    • 二進位序列化的物件實質上是「只能擴增」的型別

用 RPC 時,請避免把網路完全藏起來;確保 server 介面演進不會強迫 client lock-step 升級。

適用情境#

  • gRPC 是現代 RPC 首選:建構於 HTTP/2 之上,效能優異、生態完整(如 Protolock 工具支援 schema 比對)
  • 適合 client/server 都由你控制的場景
  • 若需支援大量異質 client,REST 可能更適合

REST#

REST 圍繞「資源(Resource)」概念:服務知道某個事物(如 Customer),請求時產生資源的「表示(Representation)」。內部如何儲存、外部以什麼格式呈現完全解耦。

想深入了解 REST 不同程度,可參考 Richardson Maturity Model。

REST 與 HTTP#

  • HTTP 動詞(GET/POST/PUT 等)天然映射到 REST 對資源的操作
  • 整個 HTTP 生態都能直接複用:caching proxy(Varnish)、load balancer、監控、安全機制
  • HTTP 也可以拿來實作 RPC(SOAP)但浪費了 HTTP 大半能力
  • gRPC 則充分利用 HTTP/2

HATEOAS(超媒體作為應用狀態引擎)#

概念:客戶端透過資源中的連結(hypermedia control)找到下一步要操作的位置,而不是寫死 URI。

類比:人類使用 Amazon 網站時不在乎購物車的 URL 變動,只要看得見「購物車」這個圖示就會去點。

優點:客戶端與伺服端解耦,URI 改變不會破壞既有客戶端。
現實:HATEOAS 在實務上採用率極低,作者本人也未見它真正帶來顯著效益。

挑戰#

  • 早期 REST 沒有 client code 自動產生工具;OpenAPI 規範改善了部分問題,但複雜度仍高
  • payload 比二進位協定大;HTTP 本身有額外開銷
  • HTTP/3 採用 QUIC 協定,在內網先享受改善
  • HATEOAS 風格容易導致呼叫鏈變長、延遲增加

適用情境#

  • 需暴露給多種客戶端的同步請求/回應介面
  • 希望充分利用 HTTP 快取
  • 對外周邊(Perimeter)API 的優選

GraphQL#

GraphQL 讓客戶端用一條查詢取回所需資料,避免多次往返與過度抓取。

範例:行動裝置要一頁顯示客戶資料 + 最近 5 筆訂單。傳統做法要打兩個服務、回傳多餘欄位;GraphQL 一次搞定。

挑戰#

  • 動態查詢可能在伺服端造成負載:類比於 SQL,但工具與限流機制相對欠成熟
  • 快取較複雜:缺少 HTTP response header 等慣用機制;CDN/反向代理快取很難套用
  • 寫入操作不及讀取流暢:常見混合做法——讀走 GraphQL、寫走 REST
  • 容易讓人把微服務當成「資料庫的薄殼」(類似 OData 思維),喪失行為與業務邏輯內聚

適用情境#

  • 系統邊界(perimeter),尤其是面向 GUI、行動裝置
  • 對外公開 API(GitHub 是早期採用者)
  • 不適合作為一般微服務之間的通訊
  • 替代方案:BFF(Backend For Frontend)模式

訊息代理(Message Brokers)#

中介軟體,常用於非同步通訊。常見:RabbitMQ、ActiveMQ、Kafka;雲服務有 AWS SQS、SNS、Kinesis 等。

佇列(Queue)vs 主題(Topic)#

形式特性適合
Queue點對點;多消費者組成 consumer group 競爭訊息請求/回應、負載分配(Competing Consumers)
Topic一對多;多個 consumer group 各收一份副本事件驅動廣播

Figure 4.1:Queue 透過 Competing Consumers 模式分配訊息

Figure 4.2:Topic 將同一訊息廣播給多個訂閱者

保證投遞(Guaranteed Delivery)#

訊息代理最大的賣點:下游不可達也沒關係,broker 會持續保留訊息直到送達。

  • 通常透過叢集確保訊息持久化
  • 代理沒設好,保證會破功:例如 RabbitMQ 叢集需要低延遲網路,否則狀態混亂、資料遺失
  • 「保證投遞」一詞各家定義不同,部署前務必細讀文件

其他特性#

  • 訊息排序:多數 broker 支援,但限制不一(Kafka 僅保證單一 partition 內順序)
  • 交易:寫入交易(Kafka 一筆交易寫多 topic)、讀取交易(JMS)
  • Exactly Once Delivery:爭議性功能;最佳做法是讓 consumer 本身具備冪等性——例如比對訊息 ID 後忽略重複

Kafka 特殊之處#

  • 為超大規模而生(5 萬 producer/consumer 在同一叢集並非神話)
  • 訊息可永久保存(可設定保留期),允許重新消費或新 consumer 補拉舊訊息
  • KSQL 提供類 SQL 串流處理,可作為動態 materialized view

序列化格式#

文字格式#

  • JSON 取代 XML 成為主流,因瀏覽器親和、相對輕量
  • AVRO 以 JSON 定義 schema,常用在訊息 payload,可隨 payload 帶 schema
  • 作者個人對 XML 的工具支援(XPath、CSS Selector)念念不忘——但承認自己是少數派

二進位格式#

  • Protocol Buffers(gRPC 使用)是目前最普及的二進位序列化
  • 其他:Simple Binary Encoding、Cap’n Proto、FlatBuffers
  • 建議:別盲信網路上的 benchmark;自己情境的 benchmark 才有意義
  • 多數系統的優化機會其實在「少傳資料」「少呼叫一次」,而非序列化格式微優化

是否該使用 Schema?#

作者立場:強烈建議使用顯式 schema。理由:

  • schema 讓服務暴露的東西明確化,省去大量文件
  • 可在 CI 階段比對版本,自動發現破壞性變更

結構性 vs 語意性破壞#

  • 結構性破壞(Structural Breakage):欄位刪除、必填欄位新增——schema 比對工具能抓
    • ProtoLock(Protocol Buffers)
    • json-schema-diff-validator(JSON-Schema)
    • openapi-diff(OpenAPI)
    • Confluent Schema Registry(支援 JSON-Schema、AVRO、Protocol Buffers)
  • 語意性破壞(Semantic Breakage):結構不變但行為變了(例如 calculate(a,b) 從加法改成乘法)——只能靠測試

沒有 schema 不代表沒有契約——只是把它變成隱性契約。客戶端的程式碼裡仍寫死了對資料結構的假設。

顯式 vs 隱式 schema:選擇「顯式」對團隊溝通與安全網都更有利。

處理變更:避免破壞性變更#

五項策略疊加使用:擴增式變更、寬容讀取者、選對技術、顯式介面、提早抓出意外破壞。

  • 擴增式變更(Expansion Changes)
    • 只往介面加東西,不刪舊東西
  • 寬容讀取者(Tolerant Reader)
    • Martin Fowler 提出。Email 服務只需要 firstname/lastname/email,就忽略其他欄位
    • 對應 Postel’s Law:「送出時保守,接收時寬容
  • 選對技術
    • Protocol Buffers 用 field number,新欄位不會打壞舊 client
    • AVRO 隨 payload 帶 schema
  • 顯式介面
    • REST 過去常無 schema,OpenAPI 與 JSON Schema 改善了這點
    • 非同步事件介面:AsyncAPI、CloudEvents(CNCF 支持)
  • 提早抓出意外破壞
    • schema diff 工具納入 CI;不相容直接讓 build 失敗
    • 沒有 schema 就靠消費者驅動契約測試(Consumer-Driven Contract Testing)

語意化版本(Semantic Versioning)MAJOR.MINOR.PATCH。MAJOR=破壞性、MINOR=新功能但相容、PATCH=修 bug。概念好,分散式系統實務上採用率不高。

處理變更:必須做破壞性變更時#

三條路:

策略說明風險
Lock-step Deployment服務與所有消費者一起升級部署違背獨立部署性
Coexist Incompatible Versions新舊版同時跑,路由分流雙倍維運成本、狀態同步難
Emulate The Old Interface同一服務同時暴露新舊端點作者偏好;用 expand-and-contract 模式

Figure 4.3:同時運行多個版本以支援舊端點

Figure 4.4:單一微服務模擬舊端點並暴露新端點

同時暴露新舊端點(Expand and Contract)#

  1. 擴增:服務同時提供舊版與新版端點
  2. 給消費者時間遷移
  3. 收縮:移除舊端點與相關程式碼

路由方式:URI 版本號(/v1/customer//v2/customer/)或 HTTP header;作者沒有強烈偏好

作者曾經同時養三個版本端點——「不推薦!」內部把 V1→V2、V2→V3 轉換串接,能讓淘汰邏輯邊界清晰。

社會契約(Social Contract)#

別只討論技術,還要約定流程:

  • 介面要改變時,誰來提?
  • 變更內容如何協商?
  • 升級工作由誰做?
  • 舊介面保留多久才下線?

追蹤使用情況#

  • 為每個端點記錄存取 log
  • 要求消費者帶 client identifier(user agent、API key)
  • 沒人在用才能安心下線

極端手段#

  • 某科技公司:給一年期限,期滿直接關閉舊端點,不追蹤誰受影響——「簡單但低效率
  • 對遲遲不升級的消費者,作者見過在舊介面加 sleep,慢慢拖長等候時間「逼」消費者升級——僅限其他手段都失敗時

DRY 與微服務世界中的程式碼重用#

DRY 原本說「不要重複系統行為與知識」,但在微服務世界裡,跨服務分享程式碼有風險。

透過共用函式庫#

  • 共用領域物件函式庫一旦變更,所有服務都得跟著動 → 耦合外溢
  • 內部函式庫(如 logging)沒問題;對外契約相關的不行
  • RealEstate.com.au 的做法:建新服務時複製模板而非依賴共用 library
  • 無法同步更新所有使用者:同一個函式庫總會有多版本同時在線;若需強迫所有人同時升級,改用「共用微服務」

Client Library#

  • 如果同一團隊維護 server 與 client lib,server 邏輯易洩漏到 client
  • AWS 的模式較好:core API + 多個 SDK(不同團隊維護);client 自己決定何時升級
  • Netflix 的 client library 包含服務發現、容錯——但長期下來造成新的耦合
  • 客戶端必須掌握升級時機

Service Mesh 與 API Gateway#

  • 兩者皆可作為微服務之間的 proxy,集中處理服務發現、logging 等共通行為
  • 前提:proxy 中的行為必須是完全通用、不帶任何特定服務邏輯

小結#

  • 先看通訊樣式,再選技術
  • 顯式 schema 在大多數情況下值得擁有
  • 盡量做向後相容變更;維持獨立部署性
  • 不得已要做破壞性變更,優先選擇「同時暴露新舊端點」
  • 跨服務共用程式碼要極度小心——真正的耦合常常從這裡偷渡進來