選 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 反序列化照樣壞 - 二進位序列化的物件實質上是「只能擴增」的型別
- Java RMI 中改
用 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:「送出時保守,接收時寬容」
- Martin Fowler 提出。Email 服務只需要
- 選對技術
- 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)#
- 擴增:服務同時提供舊版與新版端點
- 給消費者時間遷移
- 收縮:移除舊端點與相關程式碼
路由方式: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 在大多數情況下值得擁有
- 盡量做向後相容變更;維持獨立部署性
- 不得已要做破壞性變更,優先選擇「同時暴露新舊端點」
- 跨服務共用程式碼要極度小心——真正的耦合常常從這裡偷渡進來