Dubbo 在跨語言和協議穿透性方向上的探索:支持 HTTP/2 gRPC 和 Protobuf
本文總體上可分為基礎產品簡介、Dubbo 對 gRPC (HTTP/2) 和 Protobuf 的支持及示例演示三部分,在簡介部分介紹了 Dubbo、HTTP/2、gRPC、Protobuf 的基本概念和特點;第二部分介紹了 Dubbo 為何要支持 gRPC (HTTP/2) 和 Protobuf,以及這種支持為 gRPC 和 Dubbo 開發帶來的好處與不同;第三部分通過兩個實例分別演示了 Dubbo gRPC 和 Dubbo Protobuf 的使用方式。
基本介紹
Dubbo 協議
從協議層面展開,以下是當前 2.7 版本支持的 Dubbo 協議
眾所周知,Dubbo 協議是直接定義在 TCP 傳輸層協議之上,由于 TCP 高可靠全雙工的特點,為 Dubbo 協議的定義提供了最大的靈活性,但同時也正是因為這樣的靈活性,RPC 協議普遍都是定制化的私有協議,Dubbo 同樣也面臨這個問題。在這里我們著重講一下 Dubbo 在協議通用性方面值得改進的地方,關于協議詳細解析請參見官網博客
Dubbo 協議體 Body 中有一個可擴展的 attachments 部分,這給 RPC 方法之外額外傳遞附加屬性提供了可能,是一個很好的設計。但是類似的 Header 部分,卻缺少類似的可擴展 attachments,這點可參考 HTTP 定義的 Ascii Header 設計,將 Body Attachments 和 Header Attachments 做職責劃分。
Body 協議體中的一些 RPC 請求定位符如 Service Name、Method Name、Version 等,可以提到 Header 中,和具體的序列化協議解耦,以更好的被網絡基礎設施識別或用于流量管控。
擴展性不夠好,欠缺協議升級方面的設計,如 Header 頭中沒有預留的狀態標識位,或者像 HTTP 有專為協議升級或協商設計的特殊 packet。
在 Java 版本的代碼實現上,不夠精簡和通用。如在鏈路傳輸中,存在一些語言綁定的內容;消息體中存在冗余內容,如 Service Name 在 Body 和 Attachments 中都存在。
HTTP/1
相比于直接構建與 TPC 傳輸層的私有 RPC 協議,構建于 HTTP 之上的遠程調用解決方案會有更好的通用性,如WebServices 或 REST 架構,使用 HTTP + JSON 可以說是一個事實標準的解決方案。
之所有選擇構建在 HTTP 之上,我認為有兩個最大的優勢:
HTTP 的語義和可擴展性能很好的滿足 RPC 調用需求。
通用性,HTTP 協議幾乎被網絡上的所有設備所支持,具有很好的協議穿透性。
具體來說,HTTP/1 的優勢和限制是:
典型的 Request – Response 模型,一個鏈路上一次只能有一個等待的 Request 請求
HTTP/1 支持 Keep-Alive 鏈接,避免了鏈接重復創建開銷
Human Readable Headers,使用更通用、更易于人類閱讀的頭部傳輸格式
無直接 Server Push 支持,需要使用 Polling Long-Polling 等變通模式
HTTP/2
HTTP/2 保留了 HTTP/1 的所有語義,在保持兼容的同時,在通信模型和傳輸效率上做了很大的改進。
支持單條鏈路上的 Multiplexing,相比于 Request - Response 獨占鏈路,基于 Frame 實現更高效利用鏈路
Request - Stream 語義,原生支持 Server Push 和 Stream 數據傳輸
Flow Control,單條 Stream 粒度的和整個鏈路粒度的流量控制
頭部壓縮 HPACK
Binary Frame
原生 TLS 支持
gRPC
上面提到了在 HTTP 及 TCP 協議之上構建 RPC 協議各自的優缺點,相比于 Dubbo 構建于 TPC 傳輸層之上,Google 選擇將 gRPC 直接定義在 HTTP/2 協議之上,關于 gRPC 的 基本介紹和 設計愿景請參考以上兩篇文章,我這里僅摘取 設計愿景 中幾個能反映 gRPC 設計目的特性來做簡單說明。
Coverage & Simplicity,協議設計和框架實現要足夠通用和簡單,能運行在任何設備之上,甚至一些資源首先的如 IoT、Mobile 等設備。
Interoperability & Reach,要構建在更通用的協議之上,協議本身要能被網絡上幾乎所有的基礎設施所支持。
General Purpose & Performant,要在場景和性能間做好平衡,首先協議本身要是適用于各種場景的,同時也要盡量有高的性能。
Payload Agnostic,協議上傳輸的負載要保持語言和平臺中立。
Streaming,要支持 Request - Response、Request - Stream、Bi-Steam 等通信模型。
Flow Control,協議自身具備流量感知和限制的能力。
Metadata Exchange,在 RPC 服務定義之外,提供額外附加數據傳輸的能力。
總的來說,在這樣的設計理念指導下,gRPC 最終被設計為一個跨語言、跨平臺的、通用的、高性能的、基于 HTTP/2 的 RPC 協議和框架。
Protobuf
Protocol buffers (Protobuf) 是 Google 推出的一個跨平臺、語言中立的結構化數據描述和序列化的產品,它定義了一套結構化數據定義的協議,同時也提供了相應的 Compiler 工具,用來將語言中立的描述轉化為相應語言的具體描述。
它的一些特性包括:
跨語言 跨平臺,語言中立的數據描述格式,默認提供了生成多種語言的 Compiler 工具。
安全性,由于反序列化的范圍和輸出內容格式都是 Compiler 在編譯時預生成的,因此繞過了類似 Java Deserialization Vulnarability 的問題。
二進制 高性能
強類型
字段變更向后兼容
message?Person?{ ??????required?string?name?=?1; ??????required?int32?id?=?2; ??????optional?string?email?=?3; ??????enum?PhoneType?{ ????????MOBILE?=?0; ????????HOME?=?1; ????????WORK?=?2; ??????} ??????message?PhoneNumber?{ ????????required?string?number?=?1; ????????optional?PhoneType?type?=?2?[default?=?HOME]; ??????} ??????repeated?PhoneNumber?phone?=?4; ????}
除了結構化數據描述之外,Protobuf 還支持定義 RPC 服務,它允許我們定義一個 .proto 的服務描述文件,進而利用 Protobuf Compiler 工具生成特定語言和 RPC 框架的接口和 stub。后續將要具體講到的 gRPC + Protobuf、Dubbo-gRPC + Protobuf 以及 Dubbo + Protobuf 都是通過定制 Compiler 類實現的。
service?SearchService?{ ????rpc?Search?(SearchRequest)?returns?(SearchResponse); }
Dubbo 所做的支持
跨語言的服務開發涉及到多個方面,從服務定義、RPC 協議到序列化協議都要做到語言中立,同時還針對每種語言有對應的 SDK 實現。雖然得益于社區的貢獻,現在 Dubbo 在多語言 SDK 實現上逐步有了起色,已經提供了包括 Java, Go, PHP, C#, Python, NodeJs, C 等版本的客戶端或全量實現版本,但在以上提到的跨語言友好型方面,以上三點還是有很多可改進之處。
協議,上面我們已經分析過 Dubbo 協議既有的缺點,如果能在 HTTP/2 之上構建應用層協議,則無疑能避免這些弊端,同時最大可能的提高協議的穿透性,避免網關等協議轉換組件的存在,更有利于鏈路上的流量管控。考慮到 gRPC 是構建在 HTTP/2 之上,并且已經是云原生領域推薦的通信協議,Dubbo 在第一階段選擇了直接支持 gRPC 協議作為當前的 HTTP/2 解決方案。我們也知道 gRPC 框架自身的弊端在于易用性不足以及服務治理能力欠缺(這也是目前絕大多數廠商不會直接裸用 gRPC 框架的原因),通過將其集成進 Dubbo 框架,用戶可以方便的使用 Dubbo 編程模型 + Dubbo 服務治理 + gRPC 協議通信的組合。
服務定義,當前 Dubbo 的服務定義和具體的編程語言綁定,沒有提供一種語言中立的服務描述格式,比如 Java 就是定義 Interface 接口,到了其他語言又得重新以另外的格式定義一遍。因此 Dubbo 通過支持 Protobuf 實現了語言中立的服務定義。
序列化,Dubbo 當前支持的序列化包括 Json、Hessian2、Kryo、FST、Java 等,而這其中支持跨語言的只有 Json、Hessian2,通用的 Json 有固有的性能問題,而 Hessian2 無論在效率還是多語言 SDK 方面都有所欠缺。為此,Dubbo 通過支持 Protobuf 序列化來提供更高效、易用的跨語言序列化方案。
示例
示例 1,使用 Dubbo 開發 gRPC 服務
gRPC 是 Google 開源的構建在 HTTP/2 之上的一個 PRC 通信協議。Dubbo 依賴其靈活的協議擴展機制,增加了對 gRPC (HTTP/2) 協議的支持。
目前的支持限定在 Dubbo Java 語言版本,后續 Go 語言或其他語言版本將會以類似方式提供支持。下面,通過一個簡單的示例來演示如何在 Dubbo 中使用 gRPC 協議通信。
1. 定義服務 IDL
首先,通過標準的 Protobuf 協議定義服務如下:
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?greeting ??????rpc?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; ????}
在此,我們定義了一個只有一個方法 sayHello 的 Greeter 服務,同時定義了方法的入參和出參,
2. Protobuf Compiler 生成 Stub
定義 Maven Protobuf Compiler 插件工具。這里我們擴展了 Protobuf 的 Compiler 工具,以用來生成 Dubbo 特有的 RPC stub,此當前以 Maven 插件的形式發布。
??org.xolstice.maven.plugins ??protobuf-maven-plugin ??0.5.1 ?? ????com.google.protobuf:protoc:3.7.1:exe:${os.detected.classifier}???? ???? ????dubbo-grpc-java ????org.apache.dubbo:protoc-gen-dubbo-java:1.19.0-SNAPSHOT:exe:${os.detected.classifier} ????build/generated/source/proto/main/java ????false ????grpc ?? ?? ???? ?????? ????????compile ????????compile-custom ?????? ???? ??
其中,
pluginArtifact 指定了 Dubbo 定制版本的 Java Protobuf Compiler 插件,通過這個插件來在編譯過程中生成 Dubbo 定制版本的 gRPC stub。
org.apache.dubbo:protoc-gen-dubbo-java:1.19.0-SNAPSHOT:exe:${os.detected.classifier}
由于 protoc-gen-dubbo-java 支持 gRPC 和 Dubbo 兩種協議,可生成的 stub 類型,默認值是 gRPC,關于 dubbo 協議的使用可參見 使用 Protobuf 開發 Dubbo 服務。
grpc
2. 生成 Java Bean 和 Dubbo-gRPC stub
#?運行以下?maven?命令 $?mvn?clean?compile
生成的 Stub 和消息類 如下:
重點關注 GreeterGrpc ,包含了所有 gRPC 標準的 stub 類/方法,同時增加了 Dubbo 特定的接口,之后 Provider 端的服務暴露和 Consumer 端的服務調用都將依賴這個接口。
/** *?Code?generated?for?Dubbo */ public?interface?IGreeter?{ ??default?public?io.grpc.examples.helloworld.HelloReply?????sayHello(io.grpc.examples.helloworld.HelloRequest?request)?{ ????throw?new?UnsupportedOperationException("No?need?to?override?this?method,?extend?XxxImplBase?and?override?all?methods?it?allows."); ??} ??default?public?com.google.common.util.concurrent.ListenableFuture?sayHelloAsync( ????io.grpc.examples.helloworld.HelloRequest?request)?{ ????throw?new?UnsupportedOperationException("No?need?to?override?this?method,?extend?XxxImplBase?and?override?all?methods?it?allows."); ??} ??public?void?sayHello(io.grpc.examples.helloworld.HelloRequest?request, ???????????????????????io.grpc.stub.StreamObserver?responseObserver); }
3. 業務邏輯開發
繼承 GreeterGrpc.GreeterImplBase (來自第 2 步),編寫業務邏輯,這點和原生 gRPC 是一致的。
package?org.apache.dubbo.samples.basic.impl; import?io.grpc.examples.helloworld.GreeterGrpc; import?io.grpc.examples.helloworld.HelloReply; import?io.grpc.examples.helloworld.HelloRequest; import?io.grpc.stub.StreamObserver; public?class?GrpcGreeterImpl?extends?GreeterGrpc.GreeterImplBase?{ ??@Override ??public?void?sayHello(HelloRequest?request,?StreamObserver?responseObserver)?????????{ ????System.out.println("Received?request?from?client."); ????System.out.println("Executing?thread?is?"?+?Thread.currentThread().getName()); ????HelloReply?reply?=?HelloReply.newBuilder() ??????.setMessage("Hello?"?+?????request.getName()).build(); ????responseObserver.onNext(reply); ????responseObserver.onCompleted(); ??} }
Provider 端暴露 Dubbo 服務
以 Spring XML 為例
引用 Dubbo 服務
示例1附:高級用法
一、異步調用
再來看一遍 protoc-gen-dubbo-java 生成的接口:
/** *?Code?generated?for?Dubbo */ public?interface?IGreeter?{ ??default?public?HelloReply?sayHello(HelloRequest?request)?{ ????//?...... ??} ??default?public?ListenableFuture?sayHelloAsync(HelloRequest?request)?{ ????//?...... ??} ??public?void?sayHello(HelloRequest?request,?StreamObserver?responseObserver); }
這里為 sayHello 方法生成了三種類型的重載方法,分別用于同步調用、異步調用和流式調用,如果消費端要進行異步調用,直接調用 sayHelloAsync() 即可:
public?static?void?main(String[]?args)?throws?IOException?{ ??//?... ??GreeterGrpc.IGreeter?greeter?=?(GreeterGrpc.IGreeter)?context.getBean("greeter"); ??ListenableFuture?future?=??? ????greeter.sayHAsyncello(HelloRequest.newBuilder().setName("world!").build()); ??//?... }
二、高級配置
由于當前實現方式是直接集成了 gRPC-java SDK,因此很多配置還沒有和 Dubbo 側對齊,或者還沒有以 Dubbo 的配置形式開放,因此,為了提供最大的靈活性,我們直接把 gRPC-java 的配置接口暴露了出來。
絕大多數場景下,你可能并不會用到以下擴展,因為它們更多的是對 gRPC 協議的攔截或者 HTTP/2 層面的配置。同時使用這些擴展點可能需要對 HTTP/2 或 gRPC 有基本的了解。
擴展點
目前支持的擴展點如下:
org.apache.dubbo.rpc.protocol.grpc.interceptors.ClientInterceptor
org.apache.dubbo.rpc.protocol.grpc.interceptors.GrpcConfigurator
org.apache.dubbo.rpc.protocol.grpc.interceptors.ServerInterceptor
org.apache.dubbo.rpc.protocol.grpc.interceptors.ServerTransportFilter
GrpcConfigurator 是最通用的擴展點,我們以此為例來說明一下,其基本定義如下:
public?interface?GrpcConfigurator?{ ??//?用來定制?gRPC?NettyServerBuilder ??default?NettyServerBuilder?configureServerBuilder(NettyServerBuilder?builder,?URL?url)?{ ????return?builder; ??} ??//?用來定制?gRPC?NettyChannelBuilder ??default?NettyChannelBuilder?configureChannelBuilder(NettyChannelBuilder?builder,?URL?url)?{ ????return?builder; ??} ??//?用來定制?gRPC?CallOptions,?定義某個服務在每次請求間傳遞數據 ??default?CallOptions?configureCallOptions(CallOptions?options,?URL?url)?{ ????return?options; ??} }
以下是一個示例擴展實現:
public?class?MyGrpcConfigurator?implements?GrpcConfigurator?{ ??private?final?ExecutorService?executor?=?Executors ????.newFixedThreadPool(200,?new?NamedThreadFactory("Customized-grpc",?true)); ??@Override ??public?NettyServerBuilder?configureServerBuilder(NettyServerBuilder?builder,?URL?url)?{ ????return?builder.executor(executor); ??} ??@Override ??public?NettyChannelBuilder?configureChannelBuilder(NettyChannelBuilder?builder,?URL?url) ??{ ????return?builder.flowControlWindow(10); ??} ??@Override ??public?CallOptions?configureCallOptions(CallOptions?options,?URL?url)?{ ????return?options.withOption(CallOptions.Key.create("key"),?"value"); ??} }
配置為 Dubbo SPI,`resources/META-INF/services 增加配置文件
default=org.apache.dubbo.samples.basic.comtomize.MyGrpcConfigurator
指定 Provider 端線程池 默認用的是 Dubbo 的線程池,有 fixed (默認)、cached、direct 等類型。以下演示了切換為業務自定義線程池。
private?final?ExecutorService?executor?=?Executors ?????????????.newFixedThreadPool(200,?new?NamedThreadFactory("Customized-grpc",?true)); public?NettyServerBuilder?configureServerBuilder(NettyServerBuilder?builder,?URL?url)? { ??return?builder.executor(executor); }
指定 Consumer 端限流值 設置 Consumer 限流值為 10
@Override public?NettyChannelBuilder?configureChannelBuilder(NettyChannelBuilder?builder,?URL?url) { ??return?builder.flowControlWindow(10); }
傳遞附加參數 DemoService 服務調用傳遞 key
@Override public?CallOptions?configureCallOptions(CallOptions?options,?URL?url)?{ ??if?(url.getServiceInterface().equals("xxx.DemoService"))?{ ????return?options.withOption(CallOptions.Key.create("key"),?"value"); ??}?else?{ ????return?options; ??} }
三、雙向流式通信代碼中還提供了一個支持雙向流式通信的示例,同時提供了攔截流式調用的 Interceptor 擴展示例實現。
*?MyClientStreamInterceptor,工作在?client?端,攔截發出的請求流和接收的響應流 *?MyServerStreamInterceptor,工作在?server?端,攔截收到的請求流和發出的響應流
四、TLS 配置
配置方式和 Dubbo 提供的通用的 TLS 支持一致,具體請參見 Dubbo 官方文檔
示例 2, 使用 Protobuf 開發 Dubbo 服務
下面,我們以一個具體的示例來看一下基于 Protobuf 的 Dubbo 服務開發流程。
1. 定義服務
通過標準 Protobuf 定義服務
syntax?=?"proto3"; ????option?java_multiple_files?=?true; ????option?java_package?=?"org.apache.dubbo.demo"; ????option?java_outer_classname?=?"DemoServiceProto"; ????option?objc_class_prefix?=?"DEMOSRV"; ????package?demoservice; ????//?The?demo?service?definition. ????service?DemoService?{ ??????rpc?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; ????}
這里定義了一個 DemoService 服務,服務只包含一個 sayHello 方法,同時定義了方法的入參和出參。
2. Compiler 編譯服務
引入 Protobuf Compiler Maven 插件,同時指定 protoc-gen-dubbo-java RPC 擴展
??org.xolstice.maven.plugins ??protobuf-maven-plugin ??0.5.1 ?? ????com.google.protobuf:protoc:3.7.1:exe:${os.detected.classifier}???? ???? ????dubbo-grpc-java ????org.apache.dubbo:protoc-gen-dubbo-java:1.19.0-SNAPSHOT:exe:${os.detected.classifier} ????build/generated/source/proto/main/java ????false ????dubbo ?? ?? ???? ?????? ????????compile ????????compile-custom ?????? ???? ??
注意,這里與 Dubbo 對 gRPC 支持部分的區別在于: dubbo
生成 RPC stub
#?運行以下?maven?命令 $mvn?clean?compile
生成的 Java 類如下:
DemoServiceDubbo 為 Dubbo 定制的 stub
public?final?class?DemoServiceDubbo?{ ??private?static?final?AtomicBoolean?registered?=?new?AtomicBoolean(); ??private?static?Class ?init()?{ ????Class ?clazz?=?null; ????try?{ ??????clazz?=?Class.forName(DemoServiceDubbo.class.getName()); ??????if?(registered.compareAndSet(false,?true))?{ ????????org.apache.dubbo.common.serialize.protobuf.support.ProtobufUtils.marshaller( ??????????org.apache.dubbo.demo.HelloRequest.getDefaultInstance()); ????????org.apache.dubbo.common.serialize.protobuf.support.ProtobufUtils.marshaller( ??????????org.apache.dubbo.demo.HelloReply.getDefaultInstance()); ??????} ????}?catch?(ClassNotFoundException?e)?{ ??????//?ignore? ????} ????return?clazz; ??} ??private?DemoServiceDubbo()?{} ??public?static?final?String?SERVICE_NAME?=?"demoservice.DemoService"; ??/** ??????????*?Code?generated?for?Dubbo ??????????*/ ??public?interface?IDemoService?{ ????static?Class ?clazz?=?init(); ????org.apache.dubbo.demo.HelloReply?sayHello(org.apache.dubbo.demo.HelloRequest?request); ????java.util.concurrent.CompletableFuture?sayHelloAsync( ??????org.apache.dubbo.demo.HelloRequest?request); ??} }
最值得注意的是 IDemoService 接口,它會作為 Dubbo 服務定義基礎接口。
3. 開發業務邏輯
從這一步開始,所有開發流程就和直接定義 Java 接口一樣了。實現接口定義業務邏輯。
public?class?DemoServiceImpl?implements?DemoServiceDubbo.IDemoService?{ ??private?static?final?Logger?logger?=?LoggerFactory.getLogger(DemoServiceImpl.class); ??@Override ??public?HelloReply?sayHello(HelloRequest?request)?{ ????logger.info("Hello?"?+?request.getName()?+?",?request?from?consumer:?"?+?RpcContext.getContext().getRemoteAddress()); ????return?HelloReply.newBuilder() ??????.setMessage("Hello?"?+?request.getName()?+?",?response?from?provider:?" ??????????????????+?RpcContext.getContext().getLocalAddress()) ??????.build(); ??} ??@Override ??public?CompletableFuture?sayHelloAsync(HelloRequest?request)?{ ????return?CompletableFuture.completedFuture(sayHello(request)); ??} }
4. 配置 Provider
暴露 Dubbo 服務
5. 配置 Consumer
引用 Dubbo 服務
展望
RPC 協議是實現微服務體系互通的核心組件,通常采用不同的微服務通信框架則意味著綁定某一個特定的協議,如 Spring Cloud 基于 HTTP、gRPC 提供 gRPC over HTTP/2、Thrift Hessian 等都是自定義私有協議。
Dubbo 自身同樣提供了私有的 Dubbo 協議,這樣你也能基于 Dubbo 協議構建微服務。但除了單一協議之外,和以上所有框架不同的,Dubbo 最大的優勢在于它能同時支持多協議的暴露和消費,再配合 Dubbo 多注冊訂閱的模型,可以讓 Dubbo 成為橋接多種不同協議的微服務體系的開發框架,輕松的實現不同微服務體系的互調互通或技術棧遷移。
評論
查看更多