無縫連線 dubbo-go 與 gRPC

joke59發表於2020-04-11

最近我們dubbogo社群裡面,呼聲很大的一個feature就是對grpc的支援。在某位大佬的不懈努力之下,終於弄出來了。

今天我就給大家分析一下大佬是怎麼連線dubbogogrpc

grpc

先來簡單介紹一下grpc。它是google推出來的一個RPC框架。grpc是通過IDL(Interface Definition Language)——介面定義語言——編譯成不同語言的客戶端來實現的。可以說是RPC理論的一個非常非常標準的實現。

因而grpc天然就支援多語言。這幾年,它幾乎成為了跨語言RPC框架的標準實現方式了,很多優秀的rpc框架,如Spring Clouddubbo,都支援grpc

server 端

go裡面,server端的用法是:

它的關鍵部分是:s := grpc.NewServer()pb.RegisterGreeterServer(s, &server{})兩個步驟。第一個步驟很容易,唯獨第二個步驟RegisterGreeterServer有點麻煩。為什麼呢?

因為pb.RegisterGreeterServer(s, &server{})這個方法是通過使用者定義的protobuf編譯出來的。

好在,這個編譯出來的方法,本質上是:

也就是說,如果我們在dubbogo裡面拿到這個_Greeter_serviceDesc,就可以實現這個server的註冊。因此,可以看到,在dubbogo裡面,要解決的一個關鍵問題就是如何拿到這個serviceDesc

client 端

client端的用法是:

這個東西要複雜一點:

  1. 建立連線:conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
  2. 建立clientc := pb.NewGreeterClient(conn)
  3. 呼叫方法:r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})

第一個問題其實挺好解決的,畢竟我們可以從使用者的配置裡面讀出address

第二個問題就是最難的地方了。如同RegisterGreeterServer是被編譯出來的那樣,這個NewGreeterClient也是被編譯出來的。

而第三個問題,乍一看是用反射就能解決,但是我們開啟SayHello就能看到:

結合greetClient的定義,很容易看到,我們的關鍵就在於err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)。換言之,我們只需要建立出來連線,並且拿到方法、引數就能通過類似的呼叫來模擬出c.SayHello

通過對grpc的簡單分析,我們大概知道要怎麼弄了。還剩下一個問題,就是我們的解決方案怎麼和dubbogo結合起來呢?

設計

我們先來看一下dubbogo的整體設計,思考一下,如果我們要做grpc的適配,應該是在哪個層次上做適配。

我們根據前面介紹的grpc的相關特性可以看出來,grpc已經解決了codectransport兩層的問題。

而從cluster往上,顯然grpc沒有涉及。於是,從這個圖裡面我們就可以看出來,要做這種適配,那麼protocol這一層是最合適的。即,我們可以如同dubbo protocol那般,擴充套件出來一個grpc protocol

這個grpc protocol大體上相當於一個介面卡,將底層的grpc的實現和我們自身的dubbogo連線在一起。

實現

dubbogo裡面,和grpc相關的主要是:

我們直接進去看看在grpc小節裡面提到的要點是如何實現的。

server 端

這樣看起來,還是很清晰的。如同dubbogo其它的protoco一樣,先拿到service,而後通過service來拿到serviceDesc,完成服務的註冊。

注意一下上圖我紅線標準的ds, ok := service.(DubboGrpcService)這一句。

為什麼我說這個地方有點奇怪呢?是因為理論上來說,我們這裡註冊的這個service實際上就是protobuf編譯之後生成的grpc服務端的那個service——很顯然,單純的編譯一個protobuf介面,它肯定不會實現DubboGrpcService介面:

那麼ds, ok := service.(DubboGrpcService)這一句,究竟怎麼才能讓它能夠執行成功呢?

我會在後面給大家揭曉這個謎底。

client 端

dubbogo設計了自身的Client,作為對grpc裡面client的一種模擬與封裝:

注意看,這個Client的定義與前面greetClient的定義及其相似。再看下面的NewClient方法,裡面也無非就是建立了連線conn,而後利用conn裡建立了一個Client例項。

注意的是,這裡面維護的invoker實際上是一個stub

當真正發起呼叫的時候:

紅色框框框住的就是關鍵步驟。利用反射從invoker——也就是stub——裡面拿到呼叫的方法,而後通過反射呼叫。

程式碼生成

前面提到過ds, ok := service.(DubboGrpcService)這一句,面臨的問題是如何讓protobuf編譯生成的程式碼能夠實現DubboGrpcService介面呢?

有些小夥伴可能也注意到,在我貼出來的一些程式碼裡面,反射操作會根據名字來獲取method例項,比如NewClint方法裡面的method := reflect.ValueOf(impl).MethodByName("GetDubboStub")這一句。這一句的impl,即指服務的實現,也是protobuf裡面編譯出來的,怎麼讓protobuf編譯出來的程式碼裡面含有這個GetDubboStub方法呢?

到這裡,答案已經呼之欲出了:修改protobuf編譯生成程式碼的邏輯!

慶幸的是,在protobuf裡面允許我們通過外掛的形式擴充套件我們自己的程式碼生成的邏輯。

所以我們只需要註冊一個我們自己的外掛:

然後這個外掛會把我們所需要的程式碼給嵌入進去。比如說嵌入GetDubboStub方法:

還有DubboGrpcService介面:

這個東西,屬於難者不會會者不難。就是如果你不知道可以通過plugin的形式來修改生成的程式碼,那就是真難;但是如果知道了,這個東西就很簡單了——無非就是水磨工夫罷了。

歡迎加入 dubbo-go 社群

目前 dubbo-go 已經到了一個比較穩定成熟的狀態。在接下來的版本里面,我們將集中精力在雲原生上。下一個版本,我們將首先實現應用維度的服務註冊,這是一個和現有註冊模型完全不同的新的註冊模型。也是我們朝著雲原生努力的一個關鍵版本。

dubbo-go 釘釘群 23331795 歡迎你的加入。

更多原創文章乾貨分享,請關注公眾號
  • 無縫連線 dubbo-go 與 gRPC
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章