GRPC是google開源的一個(gè)高性能誓篱、跨語(yǔ)言的RPC框架朋贬,基于HTTP2協(xié)議,基于protobuf 3.x窜骄,基于Netty 4.x +锦募。GRPC與thrift、avro-rpc等其實(shí)在總體原理上并沒有太大的區(qū)別邻遏,簡(jiǎn)而言之GRPC并沒有太多突破性的創(chuàng)新糠亩。(如下描述,均基于JAVA語(yǔ)言的實(shí)現(xiàn))
? ? 對(duì)于開發(fā)者而言:
? ? 1)需要使用protobuf定義接口准验,即.proto文件
? ? 2)然后使用compile工具生成特定語(yǔ)言的執(zhí)行代碼赎线,比如JAVA、C/C++糊饱、Python等垂寥。類似于thrift,為了解決跨語(yǔ)言問(wèn)題。
? ? 3)啟動(dòng)一個(gè)Server端矫废,server端通過(guò)偵聽指定的port盏缤,來(lái)等待Client鏈接請(qǐng)求,通常使用Netty來(lái)構(gòu)建蓖扑,GRPC內(nèi)置了Netty的支持唉铜。
? ? 4)啟動(dòng)一個(gè)或者多個(gè)Client端,Client也是基于Netty律杠,Client通過(guò)與Server建立TCP長(zhǎng)鏈接潭流,并發(fā)送請(qǐng)求;Request與Response均被封裝成HTTP2的stream Frame柜去,通過(guò)Netty Channel進(jìn)行交互灰嫉。
? ? 對(duì)于GRPC的“鼓吹”,本文不多表述嗓奢,截止到今日讼撒,GRPC仍然處于開發(fā)階段,尚沒有release版本股耽,而且特性也很多需要補(bǔ)充根盒;GRPC基于protobuf 3.x,但是protobuf 3.x也沒有release版本物蝙;雖然HTTP2協(xié)議已成定局炎滞,但尚未被主流web容器包括代理服務(wù)器支持,這意味著GRPC在HTTP負(fù)載均衡方面尚有欠缺诬乞;最終册赛,在短期內(nèi)我們還不能在production環(huán)境中實(shí)施,可以做技術(shù)儲(chǔ)備震嫉。不過(guò)GRPC的缺點(diǎn)森瘪,在將來(lái)將會(huì)成為它的優(yōu)點(diǎn),我們需要時(shí)間等待它的成熟责掏。
? ? 1)GRPC尚未提供連接池
? ? 2)尚未提供“服務(wù)發(fā)現(xiàn)”柜砾、“負(fù)載均衡”機(jī)制
? ? 3)因?yàn)榛贖TTP2,絕大部多數(shù)HTTP Server换衬、Nginx都尚不支持,即Nginx不能將GRPC請(qǐng)求作為HTTP請(qǐng)求來(lái)負(fù)載均衡证芭,而是作為普通的TCP請(qǐng)求瞳浦。(nginx將會(huì)在1.9版本支持)
? ? 4)GRPC尚不成熟,易用性還不是很理想废士;就本人而言叫潦,我還是希望GRPC能夠像hessian一樣:無(wú)IDL文件,無(wú)需代碼生成官硝,接口通過(guò)HTTP表達(dá)矗蕊。
? ? 5)Spring容器尚未提供整合短蜕。
? ? 在實(shí)際應(yīng)用中,GRPC尚未完全提供連接池傻咖、服務(wù)自動(dòng)發(fā)現(xiàn)朋魔、進(jìn)程內(nèi)負(fù)載均衡等高級(jí)特性,需要開發(fā)人員額外的封裝卿操;最大的問(wèn)題警检,就是GRPC生成的接口,調(diào)用方式實(shí)在是不太便捷(JAVA)害淤,最起碼與thrift相比還有差距扇雕,希望未來(lái)能夠有所改進(jìn)。
一窥摄、實(shí)例
?1镶奉、proto文件
GRPC并沒有創(chuàng)造新的序列化協(xié)議,而是使用已有的protobuf崭放;基于protobuf來(lái)聲明數(shù)據(jù)模型和RPC接口服務(wù)腮鞍,當(dāng)然protobuf是一個(gè)非常優(yōu)秀的協(xié)議框架。關(guān)于protobuf 3.x的相關(guān)文檔莹菱,請(qǐng)參見【protobuf 3】
? ? 接下來(lái)移国,我們?cè)O(shè)計(jì)一個(gè)sayHello接口,我們將數(shù)據(jù)模型和RPC接口分別保存在兩個(gè)文件中道伟。
?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);
}
? ? proto文件中需要注意加上“syntax”迹缀,表示使用protobuf 3的語(yǔ)法。
?2蜜徽、生成JAVA代碼
? ? 生成代碼祝懂,我們最好借助于maven插件,可以在pom文件中增加如下信息:
? ? <pluginRepositories><!-- 插件庫(kù) -->
? ? ? ? <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”指令即可拘鞋,此后我們會(huì)在項(xiàng)目的target目錄下看到生成的classes文件砚蓬,當(dāng)然最終我們還是需要將service打成jar包發(fā)布的。maven仍然可以幫助我們做這些工作盆色,由.proto生成classes是在compile階段灰蛙,那么jar階段仍然是可以將classes打成jar,只需要借助maven-jar-plugin插件即可隔躲。
?3摩梧、開發(fā)Server端服務(wù)(簡(jiǎn)例)
//server端實(shí)現(xiàn)類,擴(kuò)展原有接口
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();//阻塞直到退出
? ? }
}
? ? 稍后啟動(dòng)TestServer即可宣旱。
?4仅父、開發(fā)Client端(簡(jiǎn)例)
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();
? ? }
}
? ? 然后我們運(yùn)行即可,代碼非常簡(jiǎn)單,當(dāng)然無(wú)論是Client還是Server端笙纤,我們還有其他額外的參數(shù)可以配置耗溜,我們稍后詳細(xì)介紹。
二省容、原理解析
? ? GRPC的Client與Server抖拴,均通過(guò)Netty Channel作為數(shù)據(jù)通信,序列化蓉冈、反序列化則使用Protobuf城舞,每個(gè)請(qǐng)求都將被封裝成HTTP2的Stream,在整個(gè)生命周期中寞酿,客戶端Channel應(yīng)該保持長(zhǎng)連接家夺,而不是每次調(diào)用重新創(chuàng)建Channel畏吓、響應(yīng)結(jié)束后關(guān)閉Channel(即短連接倒彰、交互式的RPC),目的就是達(dá)到鏈接的復(fù)用里初,進(jìn)而提高交互效率惨好。
?1煌茴、Server端
? ? 我們通常使用NettyServerBuilder,即IO處理模型基于Netty日川,將來(lái)可能會(huì)支持其他的IO模型蔓腐。Netty Server的IO模型簡(jiǎn)析:
? ? 1)創(chuàng)建ServerBootstrap,設(shè)定BossGroup與workerGroup線程池
? ? 2)注冊(cè)childHandler龄句,用來(lái)處理客戶端鏈接中的請(qǐng)求成幀
? ? 3)bind到指定的port回论,即內(nèi)部初始化ServerSocketChannel等,開始偵聽和接受客戶端鏈接分歇。
? ? 4)BossGroup中的線程用于accept客戶端鏈接傀蓉,并轉(zhuǎn)發(fā)(輪訓(xùn))給workerGroup中的線程。
? ? 5)workerGroup中的特定線程用于初始化客戶端鏈接职抡,初始化pipeline和handler葬燎,并將其注冊(cè)到worker線程的selector上(每個(gè)worker線程持有一個(gè)selector,不共享)
? ? 6)selector上發(fā)生讀寫事件后缚甩,獲取事件所屬的鏈接句柄谱净,然后執(zhí)行handler(inbound),同時(shí)進(jìn)行拆封package蹄胰,handler執(zhí)行完畢后岳遥,數(shù)據(jù)寫入通過(guò),由outbound handler處理(封包)通過(guò)鏈接發(fā)出裕寨。 ? ?注意每個(gè)worker線程上的數(shù)據(jù)請(qǐng)求是隊(duì)列化的。
? ? 參見源碼:SingleThreadEventLoop、NioEventLoop宾袜。(請(qǐng)求隊(duì)列化)
? ? GRPC而言捻艳,只是對(duì)Netty Server的簡(jiǎn)單封裝,底層使用了PlaintextHandler庆猫、Http2ConnectionHandler的相關(guān)封裝等认轨。具體Framer、Stream方式請(qǐng)參考Http2相關(guān)文檔月培。
1)bossEventLoopGroup:如果沒指定嘁字,默認(rèn)為一個(gè)static共享的對(duì)象,即JVM內(nèi)所有的NettyServer都使用同一個(gè)Group杉畜,默認(rèn)線程池大小為1纪蜒。
2)workerEventLoopGroup:如果沒指定,默認(rèn)為一個(gè)static共享的對(duì)象此叠,線程池大小為coreSize * 2纯续。這兩個(gè)對(duì)象采用默認(rèn)值并不會(huì)帶來(lái)問(wèn)題;通常情況下灭袁,即使你的application中有多個(gè)GRPC Server猬错,默認(rèn)值也一樣能夠帶來(lái)收益。不合適的線程池大小茸歧,有可能會(huì)是性能受限倦炒。
3)channelType:默認(rèn)為NioServerSocketChannel,通常我們采用默認(rèn)值软瞎;當(dāng)然你也可以開發(fā)自己的類逢唤。如果此值為NioServerSocketChannel,則開啟keepalive铜涉,同時(shí)設(shè)定SO_BACKLOG為128智玻;BACKLOG就是系統(tǒng)底層已經(jīng)建立引入鏈接但是尚未被accept的Socket隊(duì)列的大小,在鏈接密集型(特別是短連接)時(shí)芙代,如果隊(duì)列超過(guò)此值吊奢,新的創(chuàng)建鏈接請(qǐng)求將會(huì)被拒絕(有可能你在壓力測(cè)試時(shí),會(huì)遇到這樣的問(wèn)題)纹烹,keepalive和BACKLOG特性目前無(wú)法直接修改页滚。
[root@sh149 ~]# 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查看和修改相關(guān)值
##tcp_keepalive_time:最后一個(gè)實(shí)際數(shù)據(jù)包發(fā)送完畢后,首個(gè)keepalive探測(cè)包發(fā)送的時(shí)間铺呵。
##如果首個(gè)keepalive包探測(cè)成功裹驰,那么鏈接會(huì)被標(biāo)記為keepalive(首先TCP開啟了keepalive)
##此后此參數(shù)將不再生效,而是使用下述的2個(gè)參數(shù)繼續(xù)探測(cè)
##tcp_keepalive_intvl:此后片挂,無(wú)論通道上是否發(fā)生數(shù)據(jù)交換幻林,keepalive探測(cè)包發(fā)送的時(shí)間間隔
##tcp_keepalive_probes:在斷定鏈接失效之前贞盯,嘗試發(fā)送探測(cè)包的次數(shù);
##如果都失敗沪饺,則斷定鏈接已關(guān)閉躏敢。
對(duì)于Server端,我們需要關(guān)注上述keepalive的一些設(shè)置整葡;如果Netty Client在空閑一段時(shí)間后件余,Server端會(huì)主動(dòng)關(guān)閉鏈接,有可能Client仍然保持鏈接的句柄遭居,將會(huì)導(dǎo)致RPC調(diào)用時(shí)發(fā)生異常啼器。這也會(huì)導(dǎo)致GRPC客戶端調(diào)用時(shí)偶爾發(fā)生錯(cuò)誤的原因之一。
? ? 4)followControlWindow:流量控制的窗口大小俱萍,單位:字節(jié)端壳,默認(rèn)值為1M,HTTP2中的“Flow Control”特性鼠次;連接上更哄,已經(jīng)發(fā)送尚未ACK的數(shù)據(jù)幀大小,比如window大小為100K腥寇,且winow已滿成翩,每次向Client發(fā)送消息時(shí),如果客戶端反饋ACK(攜帶此次ACK數(shù)據(jù)的大猩庖邸)麻敌,window將會(huì)減掉此大小掂摔;每次向window中添加亟待發(fā)送的數(shù)據(jù)時(shí)术羔,window增加;如果window中的數(shù)據(jù)已達(dá)到限定值乙漓,它將不能繼續(xù)添加數(shù)據(jù)级历,只能等待Client端ACK。
? ? 5)maxConcurrentCallPerConnection:每個(gè)connection允許的最大并發(fā)請(qǐng)求數(shù)叭披,默認(rèn)值為Integer.MAX_VALUE;如果此連接上已經(jīng)接受但尚未響應(yīng)的streams個(gè)數(shù)達(dá)到此值寥殖,新的請(qǐng)求將會(huì)被拒絕。為了避免TCP通道的過(guò)度擁堵涩蜘,我們可以適度調(diào)整此值嚼贡,以便Server端平穩(wěn)處理,畢竟buffer太多的streams會(huì)對(duì)server的內(nèi)存造成巨大壓力同诫。
? ? 6)maxMessageSize:每次調(diào)用允許發(fā)送的最大數(shù)據(jù)量粤策,默認(rèn)為100M。
? ? 7)maxHeaderListSize:每次調(diào)用允許發(fā)送的header的最大條數(shù)误窖,GRPC中默認(rèn)為8192叮盘。
? ? 對(duì)于其他的比如SSL/TSL等秩贰,可以參考其他文檔。
? ? GRPC Server端熊户,還有一個(gè)最終要的方法:addService萍膛】苑【如下文service代理模式】
? ? 在此之前嚷堡,我們需要介紹一下bindService方法,每個(gè)GRPC生成的service代碼中都有此方法艇棕,它以硬編碼的方式遍歷此service的方法列表蝌戒,將每個(gè)方法的調(diào)用過(guò)程都與“被代理實(shí)例”綁定,這個(gè)模式有點(diǎn)類似于靜態(tài)代理沼琉,比如調(diào)用sayHello方法時(shí)北苟,其實(shí)內(nèi)部直接調(diào)用“被代理實(shí)例”的sayHello方法(參見MethodHandler.invoke方法,每個(gè)方法都有一個(gè)唯一的index打瘪,通過(guò)硬編碼方式執(zhí)行)友鼻;bindService方法的最終目的是創(chuàng)建一個(gè)ServerServiceDefinition對(duì)象,這個(gè)對(duì)象內(nèi)部位置一個(gè)map闺骚,key為此Service的方法的全名(fullname彩扔,{package}.{service}.{method}),value就是此方法的GRPC封裝類(ServerMethodDefinition)。
? ? 源碼分析:
private static final int METHODID_SAY_HELLO = 0;
private static class MethodHandlers<Req, Resp> implements
? ? ? ... {
? ? private final TestRpcService serviceImpl;//實(shí)際被代理實(shí)例
? ? 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: //通過(guò)方法的index來(lái)判定具體需要代理那個(gè)方法
? ? ? ? ? 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方法可以添加多個(gè)Service僻爽,即一個(gè)Netty Server可以為多個(gè)service服務(wù)虫碉,這并不違背設(shè)計(jì)模式和架構(gòu)模式。addService方法將會(huì)把service保存在內(nèi)部的一個(gè)map中胸梆,key為serviceName(即{package}.{service}),value就是上述bindService生成的對(duì)象敦捧。
? ? 那么究竟Server端是如何解析RPC過(guò)程的?Client在調(diào)用時(shí)會(huì)將調(diào)用的service名稱 + method信息保存在一個(gè)GRPC“保留”的header中碰镜,那么Server端即可通過(guò)獲取這個(gè)特定的header信息兢卵,就可以得知此stream需要請(qǐng)求的service、以及其method绪颖,那么接下來(lái)只需要從上述提到的map中找到service秽荤,然后找到此method,直接代理調(diào)用即可菠发。執(zhí)行結(jié)果在Encoder之后發(fā)送給Client王滤。(參見:NettyServerHandler)
?因?yàn)槭莔ap存儲(chǔ),所以我們需要在定義.proto文件時(shí)滓鸠,盡可能的指定package信息雁乡,以避免因?yàn)閟ervice過(guò)多導(dǎo)致名稱可能重復(fù)的問(wèn)題。
?2糜俗、Client端
我們使用ManagedChannelBuilder來(lái)創(chuàng)建客戶端channel踱稍,ManagedChannelBuilder使用了provider機(jī)制曲饱,具體是創(chuàng)建了哪種channel有provider決定,可以參看META-INF下同類名的文件中的注冊(cè)信息珠月。當(dāng)前Channel有2種:NettyChannelBuilder與OkHttpChannelBuilder扩淀。本人的當(dāng)前版本中為NettyChannelBuilder;我們可以直接使用NettyChannelBuilder來(lái)構(gòu)建channel啤挎。如下描述則針對(duì)NettyChannelBuilder:
? ? 配置參數(shù)與NettyServerBuilder基本類似驻谆,再次不再贅言。默認(rèn)情況下庆聘,Client端默認(rèn)的eventLoopGroup線程池也是static的胜臊,全局共享的,默認(rèn)線程個(gè)數(shù)為coreSize * 2伙判。合理的線程池個(gè)數(shù)可以提高客戶端的吞吐能力象对。
?ManagedChannel是客戶端最核心的類,它表示邏輯上的一個(gè)channel宴抚;底層持有一個(gè)物理的transport(TCP通道勒魔,參見NettyClientTransport),并負(fù)責(zé)維護(hù)此transport的活性菇曲;即在RPC調(diào)用的任何時(shí)機(jī)冠绢,如果檢測(cè)到底層transport處于關(guān)閉狀態(tài)(terminated),將會(huì)嘗試重建transport羊娃。(參見TransportSet.obtainActiveTransport())
? ? 通常情況下唐全,我們不需要在RPC調(diào)用結(jié)束后就關(guān)閉Channel,Channel可以被一直重用蕊玷,直到Client不再需要請(qǐng)求位置或者Channel無(wú)法真的異常中斷而無(wú)法繼續(xù)使用邮利。當(dāng)然,為了提高Client端application的整體并發(fā)能力垃帅,我們可以使用連接池模式延届,即創(chuàng)建多個(gè)ManagedChannel,然后使用輪訓(xùn)贸诚、隨機(jī)等算法方庭,在每次RPC請(qǐng)求時(shí)選擇一個(gè)Channel即可。(備注酱固,連接池特性械念,目前GRPC尚未提供,需要額外的開發(fā))
? ? 每個(gè)Service客戶端运悲,都生成了2種stub:BlockingStub和FutureStub龄减;這兩個(gè)Stub內(nèi)部調(diào)用過(guò)程幾乎一樣,唯一不同的是BlockingStub的方法直接返回Response Model班眯,而FutureStub返回一個(gè)Future對(duì)象希停。BlockingStub內(nèi)部也是基于Future機(jī)制烁巫,只是封裝了阻塞等待的過(guò)程:
try {
//也是基于Future
? ? ? ListenableFuture<RespT> responseFuture = futureUnaryCall(call, param);
? ? ? //阻塞過(guò)程
? ? ? 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();
? ? ? throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t);
}
?創(chuàng)建一個(gè)Stub的成本是非常低的,我們可以在每次請(qǐng)求時(shí)都通過(guò)channel創(chuàng)建新的stub宠能,這并不會(huì)帶來(lái)任何問(wèn)題(只不過(guò)是創(chuàng)建了大量對(duì)象)亚隙;其實(shí)更好的方式是,我們應(yīng)該使用一個(gè)Stub發(fā)送多次請(qǐng)求违崇,即Stub也是可以重用的阿弃;直到Stub上的狀態(tài)異常而無(wú)法使用。最常見的異常亦歉,就是“io.grpc.StatusRuntimeException: DEADLINE_EXCEEDED”恤浪,即表示DEADLINE時(shí)間過(guò)期,我們可以為每個(gè)Stub配置deadline時(shí)間肴楷,那么如果此stub被使用的時(shí)長(zhǎng)超過(guò)此值(不是空閑的時(shí)間),將不能再發(fā)送請(qǐng)求荠呐,此時(shí)我們應(yīng)該創(chuàng)建新的Stub赛蔫。很多人想盡辦法來(lái)使用“withDeadlineAfter”方法來(lái)實(shí)現(xiàn)一些奇怪的事情,此參數(shù)的主要目的就是表明:此stub只能被使用X時(shí)長(zhǎng)泥张,此后將不能再進(jìn)行請(qǐng)求呵恢,應(yīng)該被釋放。所以媚创,它并不能實(shí)現(xiàn)類似于“keepAlive”的語(yǔ)義渗钉,即使我們需要keepAlive,也應(yīng)該在Channel級(jí)別钞钙,而不是在一個(gè)Stub上鳄橘。
? ? 如果你使用了連接池,那么其實(shí)連接池不應(yīng)該關(guān)注DEADLINE的錯(cuò)誤芒炼,只要Channel本身沒有terminated即可瘫怜;就把這個(gè)問(wèn)題交給調(diào)用者處理。如果你也對(duì)Stub使用了對(duì)象池本刽,那么你就可能需要關(guān)注這個(gè)情況了鲸湃,你不應(yīng)該向調(diào)用者返回一個(gè)“DEADLINE”的stub,或者如果調(diào)用者發(fā)現(xiàn)了DEADLINE子寓,你的對(duì)象池應(yīng)該能夠移除它暗挑。
? ? 1)實(shí)例化ManagedChannel,此channel可以被任意多個(gè)Stub實(shí)例引用斜友;如上文說(shuō)述炸裆,我們可以通過(guò)創(chuàng)建Channel池,來(lái)提高application整體的吞吐能力蝙寨。此Channel實(shí)例晒衩,不應(yīng)該被shutdown嗤瞎,直到Client端停止服務(wù);在任何時(shí)候听系,特別是創(chuàng)建Stub時(shí)贝奇,我們應(yīng)該判定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(...)
? ? 因?yàn)镃hannel是可以多路復(fù)用靠胜,所以我們用Pool機(jī)制(比如commons-pool)也可以實(shí)現(xiàn)連接池掉瞳,只是這種池并非完全符合GRPC/HTTP2的設(shè)計(jì)語(yǔ)義,因?yàn)镚RPC允許一個(gè)Channel上連續(xù)發(fā)送對(duì)個(gè)Requests(然后一次性接收多個(gè)Responses)浪漠,而不是“交互式”的Request-Response模式陕习,當(dāng)然這么使用并不會(huì)有任何問(wèn)題。
? ? 2)對(duì)于批量調(diào)用的場(chǎng)景址愿,我們可以使用FutureStub该镣,對(duì)于普通的業(yè)務(wù)類型RPC,我們應(yīng)該使用BlockingStub响谓。
? ? 3)每個(gè)RPC方法的調(diào)用损合,比如sayHello,調(diào)用開始后娘纷,將會(huì)為每個(gè)調(diào)用請(qǐng)求創(chuàng)建一個(gè)ClientCall實(shí)例嫁审,其內(nèi)部封裝了調(diào)用的方法、配置選項(xiàng)(headers)等赖晶。此后將會(huì)創(chuàng)建Stream對(duì)象律适,每個(gè)Stream都持有唯一的streamId,它是Transport用于分揀Response的憑證遏插。最終調(diào)用的所有參數(shù)都會(huì)被封裝在Stream中捂贿。
? ? 4)檢測(cè)DEADLINE,是否已經(jīng)過(guò)期涩堤,如果過(guò)期眷蜓,將使用FailingClientStream對(duì)象來(lái)模擬整個(gè)RPC過(guò)程,當(dāng)然請(qǐng)求不會(huì)通過(guò)通道發(fā)出胎围,直接經(jīng)過(guò)異常流處理過(guò)程吁系。
? ? 5)然后獲取transport,如果此時(shí)檢測(cè)到transport已經(jīng)中斷白魂,則重建transport汽纤。(自動(dòng)重練機(jī)制,ClientCallImpl.start()方法)
6)發(fā)送請(qǐng)求參數(shù)福荸,即我們Request實(shí)例蕴坪。一次RPC調(diào)用,數(shù)據(jù)是分多次發(fā)送,但是ClientCall在創(chuàng)建時(shí)已經(jīng)綁定到了指定的線程上背传,所以數(shù)據(jù)發(fā)送總是通過(guò)一個(gè)線程進(jìn)行(不會(huì)亂序)呆瞻。
? ? 7)將ClientCall實(shí)例置為halfClose,即半關(guān)閉径玖,并不是將底層Channel或者Transport半關(guān)閉痴脾,只是邏輯上限定此ClientCall實(shí)例上將不能繼續(xù)發(fā)送任何stream信息,而是等待Response梳星。
? ? 8)Netty底層IO將會(huì)對(duì)reponse數(shù)據(jù)流進(jìn)行解包(Http2ConnectionDecoder),并根據(jù)streamId分揀Response赞赖,同時(shí)喚醒響應(yīng)的ClientCalls阻塞。(參見ClientCalls冤灾,GrpcFuture)
? ? 9)如果是BlockingStub前域,則請(qǐng)求返回,如果響應(yīng)中包含應(yīng)用異常韵吨,則封裝后拋出匿垄;如果是網(wǎng)絡(luò)異常,則可能觸發(fā)Channel重建学赛、Stream重置等年堆。
? ? 到此為止,已經(jīng)把GRPC的基本原理描述完畢
文章來(lái)源:http://shift-alt-ctrl.iteye.com/blog/2292862