在微服務架構下,服務提供者和服務消費者運行在不同物理機上的不同進程內,它們之間的呼叫相比於本地方法呼叫,需要通過遠程方法呼叫(RPC, Remote Procedure Call)來實作。本章節將深入探討服務通信的各個層面。
RPC 呼叫的基本原理#
從本地呼叫到遠程呼叫#
在單體應用時,一次服務呼叫發生在同一台機器上的同一個進程內部(本地方法呼叫)。進行服務化拆分之後,服務提供者和服務消費者運行在不同的進程,需要解決以下四個問題:
| 問題 | 說明 |
|---|---|
| 網路連接 | 客戶端和服務端如何建立網路連接? |
| 請求處理 | 服務端如何處理請求? |
| 通信協議 | 資料傳輸採用什麼協議? |
| 序列化 | 資料該如何序列化和反序列化? |
RPC 呼叫流程#
sequenceDiagram
participant C as 客戶端<br/>(Consumer)
participant S as 服務端<br/>(Provider)
C->>C: 1. 序列化請求資料
C->>S: 2. 通過網路傳輸
Note over S: 3. 反序列化請求資料<br/>處理業務邏輯<br/>序列化響應資料
S->>C: 4. 通過網路返回
C->>C: 5. 反序列化響應資料網路連接方式#
HTTP 通信#
HTTP 通信基於應用層 HTTP 協議,而 HTTP 協議又基於傳輸層 TCP 協議。
連接過程:
- 三次握手:建立連接
- 資料傳輸:發起 HTTP 呼叫
- 四次揮手:斷開連接
適用場景:
- 跨業務平台之間的服務協議
- 對外開放的服務接口
- 省去溝通服務協議的成本
Socket 通信#
Socket 通信基於 TCP/IP 協議的封裝,建立連接需要一對套接字。
通信過程分為四個步驟:
| 步驟 | 客戶端操作 | 服務端操作 |
|---|---|---|
| 1. 伺服器監聯 | - | bind() + listen() |
| 2. 客戶端請求 | connect() | - |
| 3. 連接確認 | - | accept() |
| 4. 資料傳輸 | send()/receive() | receive()/send() |
連接管理#
網路不總是可靠的,常見的處理手段:
1. 鏈路存活檢測
客戶端定時發送心跳檢測訊息(通常是 ping 請求)給服務端:
- 如果服務端連續 n 次心跳檢測沒有回覆
- 或者超過規定的時間沒有回覆
- 則認為鏈路已失效,需要重新建立連接
2. 斷連重試
多種情況會導致連接斷開(客戶端主動關閉、服務端宕機、網路故障等):
- 不能立刻完成重連
- 要等待固定的間隔後再發起重連
- 避免服務端連接回收不及時,客戶端瞬間重連請求太多
建議使用成熟的開源通信框架如 Netty、MINA 等,它們經過業界大規模應用驗證,是很可靠的方案。
服務端請求處理模型#
服務端如何處理客戶端請求,通常有三種方式:
同步阻塞方式(BIO)#
原理:客戶端每發一次請求,服務端就生成一個線程去處理。
優點:
- 編程簡單直觀,易於理解
缺點:
- 當客戶端請求很多時,需要創建大量線程
- 可能達到系統最大線程數瓶頸
適用場景:連接數比較小的業務場景
同步非阻塞方式(NIO)#
原理:通過 I/O 多路復用技術處理,把多個 I/O 的阻塞複用到同一個 select 的阻塞上。
優點:
- 單線程可以同時處理多個客戶端請求
- 開銷小,不用為每個請求創建線程
缺點:
- 編程比較複雜
適用場景:連接數比較多並且請求消耗比較輕的業務場景(如聊天伺服器)
異步非阻塞方式(AIO)#
原理:客戶端發起 I/O 操作後立即返回,真正的 I/O 讀寫由內核完成,完成後通知客戶端。
優點:
- 客戶端無需等待,不存在阻塞問題
缺點:
- 編程難度最大,程式不易於理解
適用場景:連接數比較多而且請求消耗比較重的業務場景(如相冊伺服器)
處理模型對比表
| 特性 | BIO | NIO | AIO |
|---|---|---|---|
| 阻塞方式 | 同步阻塞 | 同步非阻塞 | 異步非阻塞 |
| 線程模型 | 一連接一線程 | 多路復用 | 回調通知 |
| 編程難度 | 簡單 | 中等 | 複雜 |
| 適用連接數 | 少量 | 大量 | 大量 |
| 適用請求類型 | 任意 | 輕量級 | 重量級 |
通信協議#
協議的組成#
通信協議定義了一個「契約」,使服務消費者和提供者能夠達成共識。協議通常包括兩個部分:
| 部分 | 內容 |
|---|---|
| 訊息頭 | 協議的公共字段以及用戶擴展字段 |
| 訊息體 | 傳輸資料的具體內容 |
HTTP 協議#
HTTP 是一種開放的協議,各大網站的伺服器和瀏覽器之間的資料傳輸大都採用這種協議。
HTTP 響應報文示例:
HTTP/1.1 200 OK ← 狀態行
Server: Apache/2.4.1 ← 訊息頭開始
Content-Length: 1234
Content-Type: text/html
Date: Mon, 22 Jun 2020 09:30:00 GMT
← 空行分隔
<!DOCTYPE html> ← 訊息體開始
<html>
<body>...</body>
</html>私有協議#
除了開放的 HTTP 協議,還有定製的私有協議(如 Dubbo 協議)。
Dubbo 協議頭結構:
| 字段 | 大小 | 說明 |
|---|---|---|
| Magic | 2 bytes | 魔數,固定值 |
| Request/Response Flag | 1 bit | 請求/響應標識 |
| 2-Way | 1 bit | 是否需要返回值 |
| Event | 1 bit | 是否為事件訊息 |
| Serialization | 5 bits | 序列化方式標識 |
| Status | 1 byte | 響應狀態碼 |
| Request ID | 8 bytes | 請求唯一標識 |
| Data Length | 4 bytes | 訊息體長度 |
無論是開放的還是私有的協議,服務消費者按照契約編碼資料,服務提供者按照契約解碼資料,然後處理請求並返回結果。
序列化與反序列化#
為什麼需要序列化#
網路傳輸的耗時取決於:
- 網路帶寬的大小
- 資料傳輸量
對資料進行序列化(編碼)的主要目的是減小資料傳輸量。
序列化方式的分類#
文本類協議:
- XML
- JSON
二進制類協議:
- Protocol Buffers (PB)
- Thrift
- Hessian
選擇序列化方式的考量因素#
| 因素 | 說明 |
|---|---|
| 資料結構支援 | 支援的資料結構類型越豐富越好(如 Map、List 等) |
| 跨語言支援 | 是否支援跨語言呼叫(Java 序列化只支援 Java) |
| 壓縮比 | 序列化後的資料大小 |
| 序列化速度 | 序列化和反序列化的效能 |
| 可讀性 | 序列化後的資料是否可讀 |
主流序列化方式對比
| 方式 | 類型 | 壓縮比 | 速度 | 跨語言 | 可讀性 | 適用場景 |
|---|---|---|---|---|---|---|
| JSON | 文本 | 中 | 中 | 是 | 高 | 對外 API |
| XML | 文本 | 低 | 低 | 是 | 高 | 組態文件 |
| PB | 二進制 | 高 | 高 | 是 | 低 | 內部 RPC |
| Thrift | 二進制 | 高 | 高 | 是 | 低 | 跨語言 RPC |
| Hessian | 二進制 | 中 | 中 | 是 | 低 | Java RPC |
| Java | 二進制 | 中 | 中 | 否 | 低 | Java 內部 |
PB 序列化的壓縮比和速度都比 JSON 高很多,適合對效能和存儲空間要求高的系統;JSON 可讀性更好,更適合對外部提供服務。
服務描述方式#
RESTful API#
主要用於 HTTP 或 HTTPS 協議的接口定義,常用 Swagger 或 Wiki 進行管理。
特點:
- 學習成本低
- 適合跨部門、跨業務平台、對外開放的服務
示例:
@Path("/rest")
public interface RestfulService {
@GET
@Produces(MediaType.APPLICATION_JSON)
List<User> getUsers(@QueryParam("uid") int uid);
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
Response add(@FormParam("id") int id, @FormParam("name") String name);
}XML 組態#
多用於 RPC 協議的服務描述,通過 XML 組態文件定義接口名、參數以及返回值類型。
特點:
- 私有 RPC 框架常用
- 效能比 HTTP 協議高
- 對業務程式碼侵入性較高
- 組態變更時,服務提供者和消費者都要更新
適用場景:公司內部聯繫比較緊密的業務之間
對於 XML 組態方式的服務描述,如果應用到多個部門之間的接口格式約定,有變更時最好是新增接口,不要對原有的接口格式做變更。
IDL 文件#
IDL(Interface Description Language)是接口描述語言,通過中立的方式描述接口,使不同語言編寫的程式可以相互通信。
主流 IDL:
- Thrift:Facebook 開源
- gRPC/Protobuf:Google 開源
Protobuf 示例:
// 定義服務
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 定義請求訊息
message HelloRequest {
string name = 1;
}
// 定義響應訊息
message HelloReply {
string message = 1;
}特點:
- 主要用於跨語言平台的服務呼叫
- 通過 protoc 插件自動生成不同語言的客戶端和服務端程式碼
IDL 的局限性
如果接口返回值的字段比較多,並且經常變化時,IDL 文件方式不太合適:
- 可能會造成 IDL 文件過大難以維護
- 只要接口返回值有變更,都需要同步所有的服務消費者更新
案例:微博內容接口返回的字段有幾百個,且有些字段不固定,不適合用 Protobuf 描述。
服務描述方式選型指南#
| 場景 | 推薦方式 |
|---|---|
| 企業內部 + 單一語言(如 Java) | XML 組態 |
| 企業內部 + 多語言平台 | IDL 文件 |
| 對外開放服務 | RESTful API |
| 需要高效能 + 內部呼叫 | IDL 文件(Protobuf) |
RPC 框架的組成#
一個完整的 RPC 呼叫框架由三部分組成:
flowchart TB
subgraph RPC["RPC 框架"]
direction LR
T["通信框架<br/>(Transport)"]
P["通信協議<br/>(Protocol)"]
S["序列化<br/>(Serialization)"]
end
T --- T1["提供基礎通信能力<br/>Netty, MINA"]
P --- P1["描述通信契約<br/>HTTP, Dubbo"]
S --- S1["資料編/解碼<br/>JSON, PB"]
style T fill:#e3f2fd
style P fill:#fff3e0
style S fill:#e8f5e9一個通信框架可以適配多種通信協議,也可以採用多種序列化格式。例如 Dubbo 不僅支援 Dubbo 協議,還支援 RMI、HTTP 協議等,同時支援 JSON、Hessian、Java 序列化等多種格式。