rpc框架原理
RPC 框架的目標就是讓遠程服務調(diào)用更加簡單庸毫、透明,RPC 框架負責屏蔽底層的傳輸方式(TCP 或者 UDP)偷霉、序列化方式(XML/Json/ 二進制)和通信細節(jié)。服務調(diào)用者可以像調(diào)用本地接口一樣調(diào)用遠程的服務提供者,而不需要關心底層通信細節(jié)和調(diào)用過程习勤。
grpc框架
grpc
grpc是google開源的一個高性能、跨語言的rpc框架焙格,基于http2協(xié)議图毕,基于protobuf3.x, 基于netty4.x+, grpc與thrift眷唉、avro-rpc等其實在原理上沒有太大區(qū)別予颤,grpc并沒有太多突破性的創(chuàng)新。
對于開發(fā)者而言開發(fā)grpc程序:
- 需要使用protobuf定義接口冬阳,即.proto文件
- 使用compile工具生成特定語言的執(zhí)行代碼蛤虐,比如java c/c++、python等摩泪,類似thrift笆焰,為了解決跨語言問題
- 啟動一個server端,server端通過偵聽指定的port见坑,來等待client連接請求嚷掠,通常使用netty來構(gòu)建捏检,grpc內(nèi)置了netty的支持
- 啟動一個或多個client端,client也是基于netty不皆,Client通過與server建立TCP長連接贯城,并發(fā)送請求,Request與Response均被封裝成HTTP2的stream Frame霹娄,通過Netty Channel進行交互能犯。
實例說明
- proto文件
grpc并沒有創(chuàng)造新的序列化協(xié)議,而是使用已有的protobuf犬耻,基于protobuf來聲明數(shù)據(jù)模型和rpc接口服務踩晶。 接下來,我們設計一個sayHello接口枕磁,我們將數(shù)據(jù)模型和RPC接口分別保存在兩個文件中渡蜻。
1)TestModel.proto
syntax = "proto3";
package com.test.grpc;
option java_package = "com.test.grpc.service.model";
message TestRequest{
string name = 1;
int32 id = 2;
}
message TestResponse{
string message = 1;
}
2)TestService.proto
syntax = "proto3";
package com.test.grpc;
option java_package = "com.test.grpc.service";
import "TestModel.proto";
service TestRpcService{
rpc sayHello(TestRequest) returns (TestResponse);
}
2、生成JAVA代碼
生成代碼计济,我們最好借助于maven插件茸苇,可以在pom文件中增加如下信息:
<pluginRepositories><!-- 插件庫 -->
<pluginRepository>
<id>protoc-plugin</id>
<url>https://dl.bintray.com/sergei-ivanov/maven/</url>
</pluginRepository>
</pluginRepositories>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.4.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>com.google.protobuf.tools</groupId>
<artifactId>maven-protoc-plugin</artifactId>
<version>0.4.4</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.0.0-beta-2:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
然后只需要執(zhí)行“mvn compile”指令即可,此后我們會在項目的target目錄下看到生成的classes文件
3沦寂、開發(fā)Server端服務
//server端實現(xiàn)類,擴展原有接口
public class TestServiceImpl implements TestRpcServiceGrpc.TestRpcService {
@Override
public void sayHello(TestModel.TestRequest request, StreamObserver<TestModel.TestResponse> responseObserver) {
String result = request.getName() + request.getId();
TestModel.TestResponse response = TestModel.TestResponse.newBuilder().setMessage(result).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
public class TestServer {
public static void main(String[] args) throws Exception{
ServerImpl server = NettyServerBuilder.forPort(50010).addService(TestRpcServiceGrpc.bindService(new TestServiceImpl())).build();
server.start();
server.awaitTermination();//阻塞直到退出
}
}
4传藏、開發(fā)Client端
public class TestClient {
private final TestRpcServiceGrpc.TestRpcServiceBlockingStub client;
public TestClient(String host,int port) {
ManagedChannel channel = NettyChannelBuilder.forAddress(host, port).usePlaintext(true).build();
client = TestRpcServiceGrpc.newBlockingStub(channel).withDeadlineAfter(60000, TimeUnit.MILLISECONDS);
}
public String sayHello(String name,Integer id) {
TestModel.TestRequest request = TestModel.TestRequest.newBuilder().setId(id).setName(name).build();
TestModel.TestResponse response = client.sayHello(request);
return response.getMessage();
}
}
原理解析
GRPC的Client與Server腻暮,均通過Netty Channel作為數(shù)據(jù)通信,序列化漩氨、反序列化則使用Protobuf西壮,每個請求都將被封裝成HTTP2的Stream遗增,在整個生命周期中叫惊,客戶端Channel應該保持長連接,而不是每次調(diào)用重新創(chuàng)建Channel做修、響應結(jié)束后關閉Channel(即短連接霍狰、交互式的RPC),目的就是達到鏈接的復用饰及,進而提高交互效率蔗坯。
1、Server端
gRPC 服務端創(chuàng)建采用 Build 模式燎含,對底層服務綁定宾濒、transportServer 和 NettyServer 的創(chuàng)建和實例化做了封裝和屏蔽,讓服務調(diào)用者不用關心 RPC 調(diào)用細節(jié)屏箍,整體上分為三個過程:
創(chuàng)建 Netty HTTP/2 服務端绘梦;
將需要調(diào)用的服務端接口實現(xiàn)類注冊到內(nèi)部的 Registry 中橘忱,RPC 調(diào)用時,可以根據(jù) RPC 請求消息中的服務定義信息查詢到服務接口實現(xiàn)類卸奉;
創(chuàng)建 gRPC Server钝诚,它是 gRPC 服務端的抽象,聚合了各種 Listener榄棵,用于 RPC 消息的統(tǒng)一調(diào)度和處理凝颇。
我們通常使用NettyServerBuilder,即IO處理模型基于Netty疹鳄,將來可能會支持其他的IO模型拧略。Netty Server的IO模型簡析:
1)創(chuàng)建ServerBootstrap,設定BossGroup與workerGroup線程池
2)注冊childHandler瘪弓,用來處理客戶端鏈接中的請求成幀
3)bind到指定的port辑鲤,即內(nèi)部初始化ServerSocketChannel等月褥,開始偵聽和接受客戶端鏈接。
4)BossGroup中的線程用于accept客戶端鏈接导狡,并轉(zhuǎn)發(fā)(輪訓)給workerGroup中的線程。
5)workerGroup中的特定線程用于初始化客戶端鏈接走贪,初始化pipeline和handler坠狡,并將其注冊到worker線程的selector上(每個worker線程持有一個selector逃沿,不共享)
6)selector上發(fā)生讀寫事件后凯亮,獲取事件所屬的鏈接句柄假消,然后執(zhí)行handler(inbound),同時進行拆封package亿傅,handler執(zhí)行完畢后葵擎,數(shù)據(jù)寫入通過酬滤,由outbound handler處理(封包)通過鏈接發(fā)出盯串。 注意每個worker線程上的數(shù)據(jù)請求是隊列化的体捏。
GRPC而言几缭,只是對Netty Server的簡單封裝年栓,底層使用了PlaintextHandler某抓、Http2ConnectionHandler的相關封裝等否副。
1)bossEventLoopGroup:如果沒指定备禀,默認為一個static共享的對象痹届,即JVM內(nèi)所有的NettyServer都使用同一個Group蚕捉,默認線程池大小為1。
2)workerEventLoopGroup:如果沒指定为严,默認為一個static共享的對象第股,線程池大小為coreSize * 2夕吻。這兩個對象采用默認值并不會帶來問題涉馅;
3)channelType:默認為NioServerSocketChannel黄虱,通常我們采用默認值晤揣;當然你也可以開發(fā)自己的類朱灿。如果此值為NioServerSocketChannel滞诺,則開啟keepalive习霹,同時設定SO_BACKLOG為128淋叶;BACKLOG就是系統(tǒng)底層已經(jīng)建立引入鏈接但是尚未被accept的Socket隊列的大小煞檩,在鏈接密集型(特別是短連接)時斟湃,如果隊列超過此值注暗,新的創(chuàng)建鏈接請求將會被拒絕
sysctl -a|grep tcp_keepalive
net.ipv4.tcp_keepalive_time = 60 ##單位:秒
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75 ##單位:秒
可以在/etc/sysctl.conf查看和修改相關值
tcp_keepalive_time:最后一個實際數(shù)據(jù)包發(fā)送完畢后捆昏,首個keepalive探測包發(fā)送的時間。
如果首個keepalive包探測成功寇仓,那么鏈接會被標記為keepalive(首先TCP開啟了keepalive)
此后此參數(shù)將不再生效焚刺,而是使用下述的2個參數(shù)繼續(xù)探測
tcp_keepalive_intvl:此后乳愉,無論通道上是否發(fā)生數(shù)據(jù)交換蔓姚,keepalive探測包發(fā)送的時間間隔
tcp_keepalive_probes:在斷定鏈接失效之前坡脐,嘗試發(fā)送探測包的次數(shù)备闲;
如果都失敗恬砂,則斷定鏈接已關閉。
4)followControlWindow:流量控制的窗口大小梧奢,單位:字節(jié)亲轨,默認值為1M希柿,HTTP2中的“Flow Control”特性曾撤;連接上,已經(jīng)發(fā)送尚未ACK的數(shù)據(jù)幀大小巫湘,比如window大小為100K装悲,且winow已滿,每次向Client發(fā)送消息時尚氛,如果客戶端反饋ACK(攜帶此次ACK數(shù)據(jù)的大芯髡铩),window將會減掉此大性乃弧属瓣;每次向window中添加亟待發(fā)送的數(shù)據(jù)時,window增加讯柔;如果window中的數(shù)據(jù)已達到限定值抡蛙,它將不能繼續(xù)添加數(shù)據(jù),只能等待Client端ACK捣炬。
5)maxConcurrentCallPerConnection:每個connection允許的最大并發(fā)請求數(shù),默認值為Integer.MAX_VALUE;如果此連接上已經(jīng)接受但尚未響應的streams個數(shù)達到此值,新的請求將會被拒絕。為了避免TCP通道的過度擁堵,我們可以適度調(diào)整此值化撕,以便Server端平穩(wěn)處理热芹,畢竟buffer太多的streams會對server的內(nèi)存造成巨大壓力
6)maxMessageSize:每次調(diào)用允許發(fā)送的最大數(shù)據(jù)量,默認為100M。
7)maxHeaderListSize:每次調(diào)用允許發(fā)送的header的最大條數(shù)捌刮,GRPC中默認為8192俄认。
gRPC 的請求消息由 Netty HTTP/2 協(xié)議棧接入钾埂,通過 gRPC 注冊的 Http2FrameListener,將解碼成功之后的 HTTP Header 和 HTTP Body 發(fā)送到 gRPC 的 NettyServerHandler 中妓湘,實現(xiàn)基于 HTTP/2 的 RPC 請求消息接入鬼佣。
GRPC Server端翁狐,還有一個最終要的方法:addService
在此之前蛇耀,我們需要介紹一下bindService方法崎脉,每個GRPC生成的service代碼中都有此方法,它以硬編碼的方式遍歷此service的方法列表灶体,將每個方法的調(diào)用過程都與“被代理實例”綁定蝎抽,這個模式有點類似于靜態(tài)代理破花,比如調(diào)用sayHello方法時捂寿,其實內(nèi)部直接調(diào)用“被代理實例”的sayHello方法(參見MethodHandler.invoke方法杠愧,每個方法都有一個唯一的index待榔,通過硬編碼方式執(zhí)行)bindService方法的最終目的是創(chuàng)建一個ServerServiceDefinition對象,這個對象內(nèi)部位置一個map流济,key為此Service的方法的全名(fullname,{package}.{service}.{method}),value就是此方法的GRPC封裝類(ServerMethodDefinition)腌闯。
/** Definition of a service to be exposed via a Server. */
public final class ServerServiceDefinition {
/** Convenience that constructs a {@link ServiceDescriptor} simultaneously. */
public static Builder builder(String serviceName) {
return new Builder(serviceName);
}
public static Builder builder(ServiceDescriptor serviceDescriptor) {
return new Builder(serviceDescriptor);
}
private final ServiceDescriptor serviceDescriptor;
private final Map<String, ServerMethodDefinition<?, ?>> methods;
源碼分析:
public final class TestRpcServiceGrpc {
private static final int METHODID_SAY_HELLO = 0;
private static class MethodHandlers<Req, Resp> implements
... {
private final TestRpcService serviceImpl;//實際被代理實例
private final int methodId;
public MethodHandlers(TestRpcService serviceImpl, int methodId) {
this.serviceImpl = serviceImpl;
this.methodId = methodId;
}
@java.lang.SuppressWarnings("unchecked")
public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserver) {
switch (methodId) {
case METHODID_SAY_HELLO: //通過方法的index來判定具體需要代理那個方法
serviceImpl.sayHello((com.test.grpc.service.model.TestModel.TestRequest) request,
(io.grpc.stub.StreamObserver<com.test.grpc.service.model.TestModel.TestResponse>) responseObserver);
break;
default:
throw new AssertionError();
}
}
....
}
public static io.grpc.ServerServiceDefinition bindService(
final TestRpcService serviceImpl) {
return io.grpc.ServerServiceDefinition.builder(SERVICE_NAME)
.addMethod(
METHOD_SAY_HELLO,
asyncUnaryCall(
new MethodHandlers<
com.test.grpc.service.model.TestModel.TestRequest,
com.test.grpc.service.model.TestModel.TestResponse>(
serviceImpl, METHODID_SAY_HELLO)))
.build();
}
}
addService方法將會把service保存在內(nèi)部的一個map中绳瘟,key為serviceName(即{package}.{service}),value就是上述bindService生成的對象
那么究竟Server端是如何解析RPC過程的?Client在調(diào)用時會將調(diào)用的service名稱 + method信息保存在一個GRPC“保留”的header中姿骏,那么Server端即可通過獲取這個特定的header信息糖声,就可以得知此stream需要請求的service、以及其method分瘦,那么接下來只需要從上述提到的map中找到service蘸泻,然后找到此method,直接代理調(diào)用即可嘲玫。執(zhí)行結(jié)果在Encoder之后發(fā)送給Client悦施。
因為是map存儲,所以我們需要在定義.proto文件時去团,盡可能的指定package信息抡诞,以避免因為service過多導致名稱可能重復的問題。
Client端
我們使用ManagedChannelBuilder來創(chuàng)建客戶端channel,ManagedChannel是客戶端最核心的類土陪,它表示邏輯上的一個channel昼汗;底層持有一個物理的transport(TCP通道,參見NettyClientTransport)鬼雀,并負責維護此transport的活性顷窒;即在RPC調(diào)用的任何時機,如果檢測到底層transport處于關閉狀態(tài)(terminated)源哩,將會嘗試重建transport
通常情況下鞋吉,我們不需要在RPC調(diào)用結(jié)束后就關閉Channel出刷,Channel可以被一直重用,直到Client不再需要請求位置或者Channel無法真的異常中斷而無法繼續(xù)使用坯辩。當然馁龟,為了提高Client端application的整體并發(fā)能力,我們可以使用連接池模式漆魔,即創(chuàng)建多個ManagedChannel坷檩,然后使用輪訓、隨機等算法改抡,在每次RPC請求時選擇一個Channel即可矢炼。
每個Service客戶端,都生成了2種stub:BlockingStub和FutureStub阿纤;這兩個Stub內(nèi)部調(diào)用過程幾乎一樣句灌,唯一不同的是BlockingStub的方法直接返回Response Model,而FutureStub返回一個Future對象欠拾。BlockingStub內(nèi)部也是基于Future機制胰锌,只是封裝了阻塞等待的過程:
/**
* Executes a unary call and blocks on the response.
*
* @return the single response message.
*/
public static <ReqT, RespT> RespT blockingUnaryCall(
Channel channel, MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, ReqT param) {
ThreadlessExecutor executor = new ThreadlessExecutor();
ClientCall<ReqT, RespT> call = channel.newCall(method, callOptions.withExecutor(executor));
try {
//也是基于Future
ListenableFuture<RespT> responseFuture = futureUnaryCall(call, param);
//阻塞過程
while (!responseFuture.isDone()) {
try {
executor.waitAndDrain();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw Status.CANCELLED.withCause(e).asRuntimeException();
}
}
return getUnchecked(responseFuture);
} catch (Throwable t) {
call.cancel(null, t);
throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t);
}
}
創(chuàng)建一個Stub的成本是非常低的,我們可以在每次請求時都通過channel創(chuàng)建新的stub藐窄,這并不會帶來任何問題(只不過是創(chuàng)建了大量對象)资昧;其實更好的方式是,我們應該使用一個Stub發(fā)送多次請求荆忍,即Stub也是可以重用的格带;直到Stub上的狀態(tài)異常而無法使用。
最常見的異常刹枉,就是“io.grpc.StatusRuntimeException: DEADLINE_EXCEEDED”叽唱,即表示DEADLINE時間過期,我們可以為每個Stub配置deadline時間微宝,那么如果此stub被使用的時長超過此值(不是空閑的時間)棺亭,將不能再發(fā)送請求,此時我們應該創(chuàng)建新的Stub芥吟。
很多人想盡辦法來使用“withDeadlineAfter”方法來實現(xiàn)一些奇怪的事情侦铜,此參數(shù)的主要目的就是表明:此stub只能被使用X時長,此后將不能再進行請求钟鸵,應該被釋放钉稍。所以,它并不能實現(xiàn)類似于“keepAlive”的語義棺耍,即使我們需要keepAlive贡未,也應該在Channel級別,而不是在一個Stub上。
如果你使用了連接池俊卤,那么其實連接池不應該關注DEADLINE的錯誤嫩挤,只要Channel本身沒有terminated即可;就把這個問題交給調(diào)用者處理消恍。如果你也對Stub使用了對象池岂昭,那么你就可能需要關注這個情況了,你不應該向調(diào)用者返回一個“DEADLINE”的stub狠怨,或者如果調(diào)用者發(fā)現(xiàn)了DEADLINE约啊,你的對象池應該能夠移除它。
- 實例化ManagedChannel佣赖,此channel可以被任意多個Stub實例引用恰矩;如上文說述,我們可以通過創(chuàng)建Channel池憎蛤,來提高application整體的吞吐能力外傅。此Channel實例,不應該被shutdown俩檬,直到Client端停止服務萎胰;在任何時候,特別是創(chuàng)建Stub時豆胸,我們應該判定Channel的狀態(tài)奥洼。
synchronized (this) {
if (channel.isShutdown() || channel.isTerminated()) {
channel = ManagedChannelBuilder.forAddress(poolConfig.host, poolConfig.port).usePlaintext(true).build();
}
//new Stub
}
//或者
ManagedChannel channel = (ManagedChannel)client.getChannel();
if(channel.isShutdown() || channel.isTerminated()) {
client = createBlockStub();
}
client.sayHello(...)
因為Channel是可以多路復用,所以我們用Pool機制(比如commons-pool)也可以實現(xiàn)連接池晚胡,只是這種池并非完全符合GRPC/HTTP2的設計語義,因為GRPC允許一個Channel上連續(xù)發(fā)送多個Requests(然后一次性接收多個Responses)嚼沿,而不是“交互式”的Request-Response模式估盘,當然這么使用并不會有任何問題。
每個RPC方法的調(diào)用骡尽,比如sayHello遣妥,調(diào)用開始后,將會為每個調(diào)用請求創(chuàng)建一個ClientCall實例攀细,其內(nèi)部封裝了調(diào)用的方法箫踩、配置選項(headers)等。此后將會創(chuàng)建Stream對象谭贪,每個Stream都持有唯一的streamId境钟,它是Transport用于分揀Response的憑證。最終調(diào)用的所有參數(shù)都會被封裝在Stream中俭识。
檢測DEADLINE慨削,是否已經(jīng)過期,如果過期,將使用FailingClientStream對象來模擬整個RPC過程缚态,當然請求不會通過通道發(fā)出磁椒,直接經(jīng)過異常流處理過程。
然后獲取transport玫芦,如果此時檢測到transport已經(jīng)中斷浆熔,則重建transport。(自動重連機制桥帆,ClientCallImpl.start()方法)
發(fā)送請求參數(shù)医增,即我們Request實例。一次RPC調(diào)用环葵,數(shù)據(jù)是分多次發(fā)送调窍,但是ClientCall在創(chuàng)建時已經(jīng)綁定到了指定的線程上,所以數(shù)據(jù)發(fā)送總是通過一個線程進行(不會亂序)
將ClientCall實例置為halfClose张遭,即半關閉邓萨,并不是將底層Channel或者Transport半關閉,只是邏輯上限定此ClientCall實例上將不能繼續(xù)發(fā)送任何stream信息菊卷,而是等待Response
Netty底層IO將會對reponse數(shù)據(jù)流進行解包(Http2ConnectionDecoder),并根據(jù)streamId分揀Response缔恳,同時喚醒響應的ClientCalls阻塞
如果是BlockingStub,則請求返回洁闰,如果響應中包含應用異常歉甚,則封裝后拋出;如果是網(wǎng)絡異常扑眉,則可能觸發(fā)Channel重建纸泄、Stream重置等。
實戰(zhàn): 構(gòu)建一個簡單的名稱解析服務
可參考本人githubgrpc hello world demo