文章目錄
- gRPC知識歸檔
- gRPC原理
- 什么是gRPC
- gRPC的特性
- gRPC支持語言
- gRPC使用場景
- gRPC設計的動機和原則
- 數據封裝和數據傳輸問題
- 網絡傳輸中的內容封裝和數據體積問題
- JSON
- Protobuf(微服務之間的服務器調用,一般采用二進制序列化,比如protobuf)
- 網絡傳輸效率問題
- gRPC的4種模式
- 一元RPC模式
- 服務端流RPC模式
- 客戶端流RPC
- 雙向流RPC模式
- gRPC同步異步
- gRPC調用關系圖
- proto文件
- Client
- server
- 異步相關概念
- 異步Client
- 異步Server
- 關系圖
gRPC知識歸檔
gRPC原理
什么是gRPC
RPC是遠程調用協議(Remote Procedure Call Protocol),可以讓我們像調用本地對象一樣發起遠程調用。RPC憑借強大的治理功能,成為解決分布式系統問題的一大利器。
gRPC是一個現代的,高性能的,開源的和語言無關的通用RPC框架,HTTP2協議設計,序列化使用PB(Protocol Buffer)高性能序列化框架,基于HTTP2+PB保證高性能。
gRPC的Server和Client工作流程大致如下:
- tars兼容grpc
- brpc也兼容grpc
- grpc不兼容tars和brpc
gRPC的特性
- gRPC基于服務的思想:定義一個服務,描述這個服務的方法和出入參數,服務端有這個服務的具體實現,客戶端保有一個存根,提供與服務端相同的服務。
- gRPC默認采用protobuf作為IDL(Interface Description)接口描述語言,服務之間通信數據序列化和反序列化也是基于protocol buffer的,因為protocol buffer的特殊性,所以gRPC框架是跨語言的通信框架,可就是說用jave開發的gRPC服務,可以用GoLang,C++等語言調用。
- gRPC同時支持同步和異步調用,同步RPC調用會一直阻塞到服務端處完成返回數據,異步RPC是客戶端調用服務端時不等待服務端處理完成返回結果,而是服務端處理完成后主動回調客戶端高速客戶端處理完成。
- gRPC是基于HTTP2.0協議實現的,http2提供了很多新特性,并且在性能上相比http1提高了許多,所以gRPC的性能是非常好的。
- gRPC并沒有直接實現負載均衡和服務發現的功能,但是已經提供了自己的設計思路。已經為命名解析和負載均衡提供了接口。
- 基于http2的協議特性,gRPC允許自定義4類服務方法
- 一元RPC
- 服務端流式RPC
- 客戶端流式RPC
- 雙向流式RPC
gRPC支持語言
C++,Java(包括安卓),OC,Python,Ruby,Go,C#,Node.js。
gRPC使用場景
- 低延遲,高可擴展性的分布式系統
- 開發與云服務器的客戶端
- 設計一個準確,高效,與語言無關的新協議
- 分層設計,實現擴展,例如:身份驗證,負載平衡,日志記錄和監控。
gRPC設計的動機和原則
- 自由,開放:所有人,所有平臺都可以使用,開源,跨平臺,跨語言
- 協議可插拔:不同的服務需要使用不同的消息類型和編碼機制,例如,JSON,XML和Thirft,所以協議應允許可插拔機制,還有負載均衡,日志,監控等都支持可插拔機制。
- 阻塞和非阻塞:支持客戶端和服務器交換的消息序列的異步和同步處理。
- 取消和超時:一次RPC操作可能是持久并且昂貴的,應該允許客戶設置取消RPC通信和對這次通信加上超時時間。
- 拒絕:必須允許服務器通過在繼續處理請求的同時拒絕新請求的到來并且優雅地關閉
- 流處理:存儲系統依靠流和流控制來表達大型數據集,其他服務,如語音到文本或者股票行情,依賴流來表達與時間相關的消息序列。
- 流控制:計算能力和網絡容量在客戶端和服務器之間通常是不平衡的。流控制允許更好的緩沖區管理,以及過度活躍的對等提供DOS保護。
- 元數據交換:認證或跟蹤常見的跨領域問題依賴于不屬于服務器的接口數據交換。依賴他們的將這些特性演進到服務,暴露API來提供能力。
- 標準化狀態罵:客戶端通常以有限的方式相應API調用返回的錯誤。應約束狀態碼名稱空間,以使這些錯誤處理決策更加清晰。如果需要更加豐富的特定領域的狀態,則可以使用元數據交換機制來提供狀態。
- 互通性:報文協議必須遵循普通互聯網服務基礎框架
數據封裝和數據傳輸問題
網絡傳輸中的內容封裝和數據體積問題
早期的RPCJSON的方式,目前的RPC基本上都采用類似的Protobuf的二進制序列化方式。
其差別在于:json的設計是給人看的,protobuf的設計是給機器看的
JSON
優點:在body中對JSON內容編碼,極易跨語言,不需要約定特定的復雜編碼格式和Stub文件。在版本兼容性上很好,擴展容易。
缺點:JSON難以表達復雜的參數類型,如結構體等;數據冗余和低壓縮率使得傳輸特性能差。
Protobuf(微服務之間的服務器調用,一般采用二進制序列化,比如protobuf)
// XXXX.proto
// rpc服務的類 service關鍵字, Test服務類名
service Test {
// rpc 關鍵字,rpc的接口rpc HowRpcDefine (Request) returns (Response) ; // 定義一個RPC方法
}
// message 類,c++ class
message Request {
//類型 | 字段名字| 標號int64 user_id = 1;string name = 2;
}
message Response {repeated int64 ids = 1; // repeated 表示數組Value info = 2; // 可嵌套對象map<int, Value> values = 3; // 可輸出map映射
}
message Value {bool is_man = 1;int age = 2;
}
網絡傳輸效率問題
grpc采用HTTP2.0,相對于HTTP1.0在更快的傳輸
和更低的成本
上做了改進。有一下幾個基本點:
- HTTP2未改變HTTP的語義(如GET/POST等),只是在傳輸上優化
- 引入幀,流的概念,在TCP連接中,可以區分多個request和response
- 一個域名只會有一個TCP連接,借助幀,流可以實現多路復用,降低資源消耗
- 引入二進制編碼,降低header帶來的空間占用
- HTTP1.1核心問題在于:在同一個TCP連接中,沒辦法區分response是屬于哪個請求,一旦多個請求返回的文本內容混在一起,則沒法區分數據歸屬于哪個請求,所以請求只能一個個串行排隊發送。這直接導致了TCP資源的閑置。
- HTTP2為了解決這個問題,提出了 流 的概念,每一次請求對應一個流,有一個唯一ID,用來區分不同的請求。基于流的概念,進一步提出了 幀 ,一個請求的數據會被分成多個幀,方便進行數據分割傳輸,每個幀都唯一屬于某一個流ID,將幀按照流ID進行分組,即可分離出不同的請求。這樣同一個TCP連接中就可以同時并發多個請求,不同請求的幀數據可穿插在一起,根據流ID分組即可。HTTP2.0基于這種二進制協議的亂序模式 (Duplexing),直接解決了HTTP1.1的核心痛點,通過這種復用TCP連接的方式,不用再同時建多個連接,提升了TCP的利用效率。
gRPC的4種模式
一元RPC模式
一元RPC模式也被稱為簡單RPC模式
,當客戶端調用服務端的遠程方法時,客戶端發送請求到服務端并且獲取一個響應,與響應一起發送的還有狀態細節以及trailer元數據。
服務端流RPC模式
在服務器端流RPC模式中,服務器端在接受到客戶端額請求消息后,會發回一個響應的序列。這種多個響應所組成的序列被稱為流
。在將所有的服務器端響應發送完畢之后,服務器端會以trailer元數據的形式將其狀態發送給客戶端,從而標記流結束。
客戶端流RPC
在客戶端流 RPC 模式中,客戶端會發送多個請求給服務器端,而不再是單個請求。服務器端則會發送一個響應給客戶端。但是,服務器端不一定要等到從客戶端接收到所有消息后才發送響應。基于這樣的邏輯,我們可以在接收到流中的一條消息或幾條消息之后就發送響應,也可以在讀取完流中的所有消息之后再發送響應。
雙向流RPC模式
在雙向流RPC模式中,客戶端以消息流的形式發請求給服務端,服務端也以消息流的形式響應。調用必須有客戶端發起,但在此之后,通信完全基于gRPC客戶端和服務端的應用程序邏輯。
gRPC同步異步
gRPC調用關系圖
上圖列出了gRPC基礎概念及其關系圖。其中包括:Service(定義)、RPC、API、Client、Stub、Channel、Server、Service(實現)、ServiceBuilder等。
接下來,以官方提供的example/helloworld為例說明。
proto文件
syntax = "proto3";option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";package helloworld;// The greeting service definition.
service Greeter {// Sends a greetingrpc SayHello (HelloRequest) returns (HelloReply) {}
}// The request message containing the user's name.
message HelloRequest {string name = 1;
}// The response message containing the greetings
message HelloReply {string message = 1;
}
Client
class Greeterclient
是Client,是對Stub封裝;通過Stub可以真正調用RPC請求。
class GreeterClient {public:GreeterClient(std::shared_ptr<Channel> channel): stub_(Greeter::NewStub(channel)) {}// Assembles the client's payload, sends it and presents the response back// from the server.std::string SayHello(const std::string& user) {// Data we are sending to the server.HelloRequest request;request.set_name(user);// Container for the data we expect from the server.HelloReply reply;// Context for the client. It could be used to convey extra information to// the server and/or tweak certain RPC behaviors.ClientContext context;// The actual RPC.Status status = stub_->SayHello(&context, request, &reply);// Act upon its status.if (status.ok()) {return reply.message();} else {std::cout << status.error_code() << ": " << status.error_message()<< std::endl;return "RPC failed";}}private:std::unique_ptr<Greeter::Stub> stub_;
};
Channel提供一個特定gRPC server的主機和端口簡歷的連接。
Stub就是在Channel的基礎上創建而成。
target_str = "localhost:50051";
auto channel = grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials());
GreeterClient greeter(channel);
std::string user("world");
std::string reply = greeter.SayHello(user);
server
Server端需要實現對應的RPC,所有的RPC組成了Sevice
// Logic and data behind the server's behavior.
class GreeterServiceImpl final : public Greeter::Service {Status SayHello(ServerContext* context, const HelloRequest* request,HelloReply* reply) override {std::string prefix("Hello ");reply->set_message(prefix + request->name());return Status::OK;}
};
Server的創建需要一個Builder,添加上監聽的地址和端口,注冊上該端口上綁定的服務,最后構建出Server并且啟動。
ServerBuilder builder;// Listen on the given address without any authentication mechanism.builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());// Register "service" as the instance through which we'll communicate with// clients. In this case it corresponds to an *synchronous* service.builder.RegisterService(&service);// Finally assemble the server.std::unique_ptr<Server> server(builder.BuildAndStart());
RPC和API的區別:RPC(Remote Procedure Call)是一次遠程過程調用的整個動作,而API(Application Programming Interface)是不同語言實現RPC中的具體接口。一個RPC可能對應多種API,比如同步的,異步的,回調的。
異步相關概念
不管Client還是Server,異步gRPC都是利用CompletionQueue API進行異步操作。基本流程:
- 綁定一個CompletionQueue到一個RPC調用
- 利用唯一的void* Tag進行讀寫
- 調用CompletionQueue::Next()
等待操作完成,完成后通過唯一的Tag來判斷對應什么請求/返回進行后續操作
異步Client
greeter_async_client.cc
中是異步Client的Demo,其中只有一次請求,邏輯簡單
- 創建CompletionQueue
- 創建RPC(即
clientAsyncResponseReader<HelloReply>
),這里有兩種方式:stub_->PrepareAsyncSayHello()
+rpc->StartCall()
stub_->AsyncSayHello()
- 調用
rpc->Finish()
設置請求消息reply和唯一的tag關聯,將請求發送出去。 - 使用
cq.Next()
等待Completion Queue返回響應消息體,通過tag關聯對應請求。
異步Server
RequestSayHello()
這個函數沒有任何說明。只是說:“we request that the system start processing SayHello requset.”也沒有說和cq_->Next(&tag, &ok);
的關系。我來說一下流程:
- 創建一個CallData,初始構造列表中將狀態設置為CREATE
- 構造函數中,調用Process()成員函數,調用service_->RequestSayHello()
后,狀態變更為PROCESS:
- 傳入ServerContext ctx_
- 傳入HelloRequest request_
- 傳入ServerAsyncResponsewriter<HelloReply>
responder_
- 傳入ServerCompletionQueue* cq_
- 將對象自身的地址作為tag
傳入
- 該動作,能將事件加入事件循環,可以在CompletionQueue中等待
- 收到請求,cp->Next()
的阻塞結束并且返回,得到tag,即上次傳入的CallData對象
- 調用tag對應CallData對象Process()
,此時狀態Process
- 創建新的CallData對象以接受新請求
- 處理消息并且設置reply_
- 將狀態設置為FINISH
- 調用responder_.Finish()
將返回發送給客戶端
- 該動作,能將事件加入到循環,可以將CompletionQueue中等待
- 發送完畢,cp->Next()
的阻塞結束并且返回,得到tag。現實中,如果發送有異常應當有其他處理。
- 調用tag對應的CallData對象proceed
,此時狀態為FINISH,delete this
清理自己,一條消息處理完成。