這個 Tag 的第六個主題是「服務通訊」,講白話些就是「我要怎麼呼叫另一個服務的功能?」我們在第三個主題講了「服務通訊的大致邏輯」,以及在第四個和第五個主題花了很大的篇幅講了「服務怎麼 連線 到服務」,但好像從來都沒有真正說「服務怎麼和服務 互動」、「RPC 是什麼」,以及具體要怎麼通訊、有哪些通訊方法,以及有哪些通訊手段。這裡就要開始介紹了!
這篇是之前 SITCON 2025 的草稿,不是後面寫的,也還沒有做過 peer reviewing!如果有任何問題的話,也歡迎到 Threads 或 X 上標我指正。
「通訊」是什麼?
在單體服務的世界,你要呼叫一個方法,比如「選課」,你通常是用函式或 HTTP API 呼叫完成的。舉例來說:
- 你的前端往你的 API 發出了
POST /api/v1/course/:course/selection
。我們會稱這個網址叫做端點 (endpoint)。 - 你的 API 有一個「選課」Controller,裡面有一個處理選課端點的「方法」(method),這裡叫做
SelectCourse
吧! - 接著你的
SelectCourse
方法會透過呼叫GetStudentInfo
方法來取得這個學生的資訊,以及透過GetCourseContraints
方法取得課程的限修資訊 - 接著
SelectCourse
方法會根據這些資訊決定允不允許學生選這堂課,然後將選課結果插入資料庫當中。
📚 小提醒:在 MVCS 架構下,我們通常會叫這種功能性質的 class 為 Service。不過為了防止跟微服務的 Service 混淆(雖然其實概念是一樣的),這裡就不會用 Service 這個名詞。
如果你能理解上面這張圖,那我們把它換成微服務,你就很清楚我們要解決什麼「通訊」問題了:
上面這張圖的青色線段,就是我們這次要探討的問題了:這些服務都不在一起,我們要怎麼呼叫對方的方法——也就是「青色」這個線條,究竟怎麼實作?
🫨 選課服務是怎麼連線到使用者服務的?你可以往回看看「服務探索」這個章節!從這裡開始,我們都假定我們已經知道這些服務的 IP 和連線方式了,要處理的只是應用層的溝通問題。
用 HTTP 設計自己的 RPC
首先,最直覺的方法,就是你給每個服務開一個 HTTP 的 API,用 HTTP 的呼叫邏輯和其他服務溝通。因為這些 HTTP 方法只是內部使用的,你不用像 REST API 一樣,需要認真思考每個端點的命名,只需要讓每個服務看得懂就好。因此,你這樣設計你的微服務溝通方法:
POST /getStudentInfo
Host: 使用者服務
Content-Type: application/json
--> {"studentID": "A123456789"}
<-- {"studentID": "A123456789", "gender": "male", ...}
其實上面這樣就是一個 RPC 了——根據 AWS 的定義,RPC 其實就是隨意奔放、主要「專注於功能或動作」的 API。實際上這種以 HTTP 為基礎的 RPC 很常見,甚至有很多 RPC 框架是根據上面的邏輯為基礎設計的。比如說,JSON-RPC 就是一個類似上面這種設計的 RPC 標準:
POST /rpc
Host: 使用者服務
Content-Type: application/json
--> {"jsonrpc": "2.0", "method": "getStudentInfo", "params": {"studentID": "A123456789"}, "id": 1}
<-- {"jsonrpc": "2.0", "result": {"studentID": "A123456789", "gender": "male", ...}, "id": 1}
🤔 你知道哪些 RPC 框架是以 JSON-RPC 為基礎的嗎?
為什麼需要 RPC 框架?
不過你會發現上面的 RPC 有幾個小問題:
- 我是怎麼把外部呼叫的
POST /getStudentInfo
,跟使用者服務裡面實際的GetStudentInfo
連結的?就目前沒有框架的設計,你得自己在 HTTP 框架寫一個「POST /getStudentInfo
」路由對應到GetStudentInfo
的規則。 - 我的「選課服務」是怎麼知道「使用者服務」可以用
POST /getStudentInfo
呼叫「取得使用者資訊」,然後可以傳入studentID
參數的?就目前沒有框架的設計,這單純是靠人腦保證的:我在開發「選課服務」的當下,知道「使用者服務」有一個這樣的方法和參數能呼叫,所以我就在「選課服務」呼叫getStudentInfo
方法。
你或許就發現跟「服務探索」很相似的問題了:假如對方變動了——舉例來說,我把 POST /getStudentInfo
改名成 POST /getStudentInformation
了呢?這樣子「選課服務」當然就沒有辦法呼叫「使用者服務」取得使用者資訊的方法了。
說實話上面這兩件事情是廢話:你在單體服務呼叫不存在的方法,肯定也是會出錯的。但不同的是,你可以在程式編寫的期間就很明顯地知道「沒有這個方法」,以及會很清楚「這個方法可以傳入哪些參數」。
所以,我們希望引入框架來解決上面兩個問題:
- 我們可不可以請框架自動把內部的方法和外部呼叫的 RPC 方法對應起來?
- 這個框架可不可以產出一些程式碼,讓我可以方便的呼叫框架產出的 RPC 方法?
- 同時,這些程式碼可不可以讓開發者方便知道(甚至是驗證)這個方法接受的參數?
引入 RPC 框架
RPC 框架的目的,就是讓你不用再關心底層的通訊方式(JSON、HTTP POST),透過產生程式碼(stubs),讓我們可以像單體服務一樣用高層次(high-level)的函式,呼叫遠端服務的方法。這裡就不詳細展開「RPC 框架有什麼類型」,我們就直接跳入重點:主流的 RPC 框架要怎麼用?
🤔 RPC 框架有什麼類型,以及他們分別的代表是什麼?
這裡就介紹一個常見而且頗為主流的 RPC 框架:gRPC。
首先你給每個服務用 gRPC 的 proto 語言寫出他的方法、他接受的參數以及回傳值:
// 不是完整的 proto 定義! service UserService { rpc GetStudentInformation(GetStudentInformationRequest) returns (GetStudentInformationResponse) } message GetStudentInformationRequest { string student_id = 1; } message GetStudentInformationResponse { string student_id = 1; string gender = 2; // .... }
用
protoc
(proto 的編譯器) 分別產出他的用戶端 (client) 和伺服器 (server)。protoc 的編譯器會產生伺服器端的介面宣告 (interface),你負責在產出的「使用者服務」伺服器實作 (implement)
GetStudentInformation
方法。然後,你在「選課服務」使用編譯器產出的用戶端,連線到「使用者服務」的 gRPC 伺服器,然後像一般的函式呼叫般呼叫
GetStudentInformation
——傳入GetStudentInformationRequest
,等待伺服器回傳GetStudentInformationResponse
。完成!
gRPC 有幾個特點:首先他使用 Protocol Buffers 來把結構編碼成二進位的格式,會比 JSON 緊湊,並且格式和類型也會更精確、嚴格得多。除此之外,它用上了許多 HTTP/2 的特性來傳輸編碼後的結果,來盡量降低遠端呼叫的延遲。不過它也有個小缺點:protoc
不是所有語言都支援的,遇到不支援的語言,可能就會出一些小狀況——不過主流的語言基本上 protoc
都有支援(或者是第三方實作)啦。
🤔 不支援 gRPC 協定的語言,我們作為伺服器端可以怎麼優雅地提供備用介面 (HTTP) 呢?
非同步訊息通訊
到這裡,我們其實就學會了服務之間怎麼互相通訊了。不過你會發現到我們目前講到的都是「同步通訊」,也就是我請求「使用者資訊」然後等待回應。但有些方法其實你完全不需要真的等待。比如說「選課」我們可不可以讓他變成放在背景執行,使用者按下去之後可以先做其他事情,而我們處理好之後再告知使用者呢?
🤔 你直覺上會怎麼實作「背景執行」的功能?
我們下一個主題正是要講到「非同步訊息通訊」1。而且有個好消息:這個「非同步訊息通訊」是單體服務也能實作的!
這篇沒有寫完,所以沒有發佈。 ↩︎