在微服務架構下,服務提供者和服務消費者運行在不同物理機上的不同進程內,它們之間的呼叫相比於本地方法呼叫,需要通過遠程方法呼叫(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 協議。

連接過程

  1. 三次握手:建立連接
  2. 資料傳輸:發起 HTTP 呼叫
  3. 四次揮手:斷開連接

適用場景

  • 跨業務平台之間的服務協議
  • 對外開放的服務接口
  • 省去溝通服務協議的成本

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 讀寫由內核完成,完成後通知客戶端。

優點

  • 客戶端無需等待,不存在阻塞問題

缺點

  • 編程難度最大,程式不易於理解

適用場景:連接數比較多而且請求消耗比較重的業務場景(如相冊伺服器)

處理模型對比表
特性BIONIOAIO
阻塞方式同步阻塞同步非阻塞異步非阻塞
線程模型一連接一線程多路復用回調通知
編程難度簡單中等複雜
適用連接數少量大量大量
適用請求類型任意輕量級重量級

通信協議#

協議的組成#

通信協議定義了一個「契約」,使服務消費者和提供者能夠達成共識。協議通常包括兩個部分:

部分內容
訊息頭協議的公共字段以及用戶擴展字段
訊息體傳輸資料的具體內容

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 協議頭結構

字段大小說明
Magic2 bytes魔數,固定值
Request/Response Flag1 bit請求/響應標識
2-Way1 bit是否需要返回值
Event1 bit是否為事件訊息
Serialization5 bits序列化方式標識
Status1 byte響應狀態碼
Request ID8 bytes請求唯一標識
Data Length4 bytes訊息體長度

無論是開放的還是私有的協議,服務消費者按照契約編碼資料,服務提供者按照契約解碼資料,然後處理請求並返回結果。

序列化與反序列化#

為什麼需要序列化#

網路傳輸的耗時取決於:

  1. 網路帶寬的大小
  2. 資料傳輸量

對資料進行序列化(編碼)的主要目的是減小資料傳輸量

序列化方式的分類#

文本類協議

  • 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 序列化等多種格式。