🎯 什麼情境該想到我
當你「想讓一個系統像呼叫本地方法一樣,同步地請另一個系統做事並立刻拿回結果」的時候(RPC、遠端服務呼叫、Web API)。
⚙️ 怎麼用(步驟 / 公式)
意圖:讓每個應用把自己的部分功能開放成可被遠端呼叫的介面,其他應用透過同步呼叫來使用其功能與資料。
做法 / 結構要點:
- 應用各自封裝資料,只透過介面(方法/服務)對外提供行為,隱藏內部實作。
- 呼叫端同步等待被呼叫端回傳結果,語意上接近呼叫本地函式。
- 相較共享資料庫,耦合的是「行為介面」而非資料模型,各系統可獨立維護資料。
★ 今天的 RPC:gRPC/REST/GraphQL
| EIP 書裡 | 今天 |
|---|---|
| CORBA/DCOM/RMI/SOAP | ★ gRPC(HTTP/2 + protobuf)、REST/JSON、GraphQL、Connect |
| WSDL | ★ .proto(有 codegen、有向後相容規則)、OpenAPI |
| 「像呼叫本地函式一樣」 | ★ 這個理想已被放棄——現代 RPC 明確要求你處理 ctx、timeout、retry、error code |
// ★ Go 的 gRPC 呼叫:所有「遠端」的現實都在簽章上
ctx, cancel := context.WithTimeout(ctx, 2*time.Second) // ★ 必須有超時
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: id})
if err != nil {
if status.Code(err) == codes.DeadlineExceeded { … } // ★ 錯誤要分類:能不能重試?
return fmt.Errorf("get user %s: %w", id, err)
}★★ 最重要的一段:RPC 最大的謊言是「它看起來像本地呼叫」
分散式運算的八個謬誤(Deutsch/Gosling,1994;至今仍然全中):
① 網路是可靠的 ② 延遲是零 ③ 頻寬是無限的 ④ 網路是安全的
⑤ 拓撲不會改變 ⑥ 只有一個管理者 ⑦ 傳輸成本是零 ⑧ 網路是同質的
具體後果:
// ★★ ① 部分失敗(partial failure)—— 本地呼叫沒有這個狀態
resp, err := client.Charge(ctx, req)
if err != nil {
// ★ 扣款到底成功了沒?你不知道。
// 可能:請求沒送到 / 送到了但回應丟了 / 對方處理中還沒回
// → ★ 所以 RPC 也需要冪等(帶 idempotency key),和訊息一樣
}
// ★★ ② 可用性是相乘的
// 三個同步依賴,每個 99.9% → 0.999³ = 99.7%
// → 一年多出約 16 小時的不可用。★ 而且這是你「什麼都沒做錯」的情況下★ 這兩點是「同步 RPC vs 非同步訊息」真正的分水嶺:
訊息把「對方現在活著嗎」這個問題變成「對方遲早會處理」。
★ 同步 RPC 必須配的四件事(少一件就是事故等著發生)
// ① 超時 —— ★ 沒有超時的呼叫會讓你的連線池被慢速依賴吃光
// ② 重試 + 指數退避 + jitter,且只重試「安全的」錯誤
// ★ 只對 DeadlineExceeded / Unavailable 重試;InvalidArgument 重試一萬次也一樣
// ③ ★ 斷路器 —— 對方掛了就快速失敗,不要繼續堆積請求
// ④ ★ 重試預算(retry budget)—— 三層各重試 3 次 = 最底層收到 27 倍流量★ 什麼時候 RPC 是對的(不要一律改成訊息)
| 選 RPC | 選訊息 |
|---|---|
| ★ 呼叫端需要答案才能繼續(查餘額、驗證權限) | 呼叫端不需要立即結果 |
| ★ 請求-回應語義天然(讀取、查詢) | 通知、廣播 |
| 只有兩個服務、呼叫量小 | ★ 一對多、需要削峰 |
| ★ 需要立即的錯誤回饋給使用者 | 可以稍後補償 |
| 延遲預算很緊(< 100ms) | ★ 延遲容忍高 |
★ 判準:呼叫端在不在乎「現在」的結果?
在乎 → RPC;不在乎 → 訊息。
⚠️ 「為了解耦」而把所有讀取都改成訊息,會得到一個沒人能推理的系統——
查詢本來就該是同步的。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
同步等待造成時間耦合:被呼叫端變慢或當機,呼叫端也被拖住。
-
受網路可靠性影響,遠端呼叫可能失敗、逾時、延遲,錯誤處理比本地呼叫複雜。
-
一連串跨系統同步呼叫容易堆疊延遲並形成緊密相依;需要削峰或高可用時不理想。
-
★★ 「像本地呼叫一樣」是 RPC 最大的謊言。部分失敗(不知道成功沒)是本地呼叫不存在的狀態。
-
★ RPC 也需要冪等。帶 idempotency key,和訊息一樣——因為回應可能丟失而請求已生效。
-
★★ 可用性是相乘的。三個 99.9% 的同步依賴 = 0.999³ ≈ 99.7%:一年不可用 ≈ (1−0.999³)×8760 ≈ 26.3 小時,單一服務是 0.001×8760 ≈ 8.8 小時,等於一年多出約 17.5 小時。
-
★ 沒有超時的呼叫會吃光連線池,把對方的慢變成自己的掛。
-
★ 只重試安全的錯誤(
Unavailable、DeadlineExceeded),不要重試InvalidArgument。 -
★ 重試會放大:三層各 3 次 = 27 倍流量。要有重試預算與斷路器。
-
不要為了解耦把查詢也改成訊息。 需要答案才能繼續的呼叫,本來就該是同步的。
-
gRPC 的
.proto有相容性規則(欄號不可重用、不可改型別),違反了是靜默的資料損壞(見 工具-資料編碼與演進)。
🔗 相關工具
- 訊息傳遞 Messaging(同為整合風格,改用非同步解除時間耦合)
- 共享資料庫 Shared Database(同為整合風格,耦合資料而非行為介面)
- 檔案傳輸 File Transfer(同為整合風格,最鬆耦合但即時性最差)
- 企業整合模式
- 檔案傳輸、共享資料庫、訊息傳遞 —— 另外三種整合風格
- 請求-回覆 —— 用訊息做出 RPC 的語義
- 工具-服務容錯設計 —— 超時、重試、斷路器、艙壁
- 工具-限流器設計 —— 保護被呼叫端
- 工具-資料編碼與演進 —— protobuf 的相容性規則
- 遠端外觀 —— 粗粒度的遠端介面