Dubbo-go Server端開啓服務過程

導讀

導讀:隨着微服務架構的流行,許多高性能 rpc 框架應運而生,由阿里開源的 dubbo 框架 go 語言版本的 dubbo-go 也成爲了衆多開發者不錯的選擇。本文將介紹 dubbo-go 框架的基本使用方法,以及從 export 調用鏈的角度進行 server 端源碼導讀,但願能引導讀者進一步認識這款框架。下週將發表本文的姊妹篇:《從 client 端源碼導讀 dubbo-go 框架》。

序言

近日閱讀了部分dubbo-go源碼
https://github.com/dubbogo/dubbo-gohtml

當拿到一款框架以後,一種不錯的源碼閱讀方式大體以下:從運行最基礎的helloworld demo 源碼開始,再查看配置文件,開啓各類依賴服務(好比zk、consul),開啓服務端,再到經過client調用服務端,打印完整請求日誌和回包。調用成功以後,再根據框架的設計模型,從配置文件解析開始,自頂向下遞閱讀整個框架的調用棧。
對於C/S模式的rpc請求來講,整個調用棧被拆成了client和server兩部分,因此能夠分別從server端的配置文件解析閱讀到server端的監聽啓動,從client端的配置文件解析閱讀到一次invoker Call 調用。這樣一次完整請求就明晰了起來。java

1. 運行官網提供的helloworld-demo

官方demonode

1.1 dubbo-go 2.7版本 QuickStart

1. 開啓一個go-server服務
  • 將倉庫clone 到本地
$ git clone https://github.com/dubbogo/dubbo-samples.git
  • 進入dubbo目錄
$ cd dubbo-samples/golang/helloworld/dubbo

進入目錄後可看到四個文件夾,分別支持go和java的client以及server,咱們嘗試運行一個go的server
進入app子文件夾內,能夠看到裏面保存了go文件。git

$ cd go-server/app
  • sample文件結構:

可在go-server裏面看到三個文件夾:app、assembly、profiles
其中app文件夾下保存go源碼,assembly文件夾下保存可選的針對特定環境的build腳本,profiles下保存配置文件。對於dubbo-go框架,配置文件很是重要,沒有文件將致使服務沒法啓動。github

  • 設置指向配置文件的環境變量

因爲dubbo-go框架依賴配置文件啓動,讓框架定位到配置文件的方式就是經過環境變量來找。對於server端須要兩個必須配置的環境變量:CONF_PROVIDER_FILE_PATH、APP_LOG_CONF_FILE,分別應該指向服務端配置文件、日誌配置文件。
在sample裏面,咱們可使用dev環境,即profiles/dev/log.yml profiles/dev/server.yml 兩個文件。
在app/下,經過命令行中指定好這兩個文件:golang

$ export CONF_PROVIDER_FILE_PATH="../profiles/dev/server.yml"
$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
  • 設置go代理並運行服務
$ go run .

若是提示timeout,則須要設置goproxy代理apache

$ export GOPROXY="http://goproxy.io"

再運行go run 便可開啓服務設計模式

2. 運行zookeeper

安裝zookeeper,並運行zkServer, 默認爲2181端口緩存

3. 運行go-client調用server服務
  • 進入go-client的源碼目錄
$ cd go-client/app
  • 同理,在/app下配置環境變量
$ export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"
$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"

配置go代理bash

$ export GOPROXY="http://goproxy.io"
  • 運行程序
$ go run .

便可在日誌中找到打印出的請求結果

response result: &{A001 Alex Stocks 18 2020-10-28 14:52:49.131 +0800 CST}

一樣,在運行的server中,也能夠在日誌中找到打印出的請求:

req:[]interface {}{"A001"}
rsp:main.User{Id:"A001", Name:"Alex Stocks", Age:18, Time:time.Time{...}

恭喜!一次基於dubbo-go的rpc調用成功。

4. 常見問題
    1. 當日志開始部分出現profiderInit和ConsumerInit均失敗的日誌。檢查環境變量中配置路徑是否正確,配置文件是否正確。
    1. 當日志中出現register失敗的狀況,通常爲向註冊中心註冊失敗,檢查註冊中心是否開啓,檢查配置文件中關於register的端口是否正確。
    1. sample的默認開啓端口爲20000,確保啓動前無佔用

1.2 配置環境變量

export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"

1.3 服務端源碼

1. 目錄結構

dubbo-go框架的example提供的目錄以下:

  • app/ 文件夾下存放源碼,能夠本身編寫環境變量配置腳本buliddev.sh
  • assembly/ 文件夾下存放不一樣平臺的構建腳本
  • profiles/ 文件夾下存放不一樣環境的配置文件
  • target/ 文件夾下存放可執行文件
2. 關鍵源碼

源碼放置在app/文件夾下,主要包含server.go 和user.go 兩個文件,顧名思義,server.go用於使用框架開啓服務以及註冊傳輸協議,user.go則定義了rpc-service結構體,以及傳輸協議的結構。
user.go

func init() {
    config.SetProviderService(new(UserProvider))
    // ------for hessian2------
    hessian.RegisterPOJO(&User{})
}
type User struct {
    Id   string
    Name string
    Age  int32
    Time time.Time
}
type UserProvider struct {
}
func (u *UserProvider) GetUser(ctx context.Context, req []interface{}) (*User, error) {

能夠看到,user.go中存在init函數,是服務端代碼中最早被執行的部分。User爲用戶自定義的傳輸結構體,UserProvider爲用戶自定義的rpc_service。
包含一個rpc函數,GetUser。
固然,用戶能夠自定義其餘的rpc功能函數。
在init函數中,調用config的SetProviderService函數,將當前rpc_service註冊在框架config上。
能夠查看dubbo官方文檔提供的設計圖

service層下面就是config層,用戶服務會逐層向下註冊,最終實現服務端的暴露。
rpc-service註冊完畢以後,調用hessian接口註冊傳輸結構體User。
至此init函數執行完畢
server.go

// they are necessary:
//      export CONF_PROVIDER_FILE_PATH="xxx"
//      export APP_LOG_CONF_FILE="xxx"
func main() {
    hessian.RegisterPOJO(&User{})
    config.Load()
    initSignal()
}
func initSignal() {
    signals := make(chan os.Signal, 1)
    ...

以後執行main函數
main函數中只進行了兩個操做,首先使用hessian註冊組件將User結構體註冊(與以前略有重複),從而能夠在接下來使用getty打解包。
以後調用config.Load函數,該函數位於框架config/config_loader.go內,這個函數是整個框架服務的啓動點,下面會詳細講這個函數內重要的配置處理過程。執行完Load()函數以後,配置文件會讀入框架,以後根據配置文件的內容,將註冊的service實現到配置結構裏,再調用Export暴露給特定的registry,進而開啓特定的service進行對應端口的tcp監聽,成功啓動而且暴露服務。
最終開啓信號監聽initSignal()優雅地結束一個服務的啓動過程。

1.4 客戶端源碼

客戶端包含client.go和user.go兩個文件,其中user.go與服務端徹底一致,再也不贅述。
client.go

// they are necessary:
//      export CONF_CONSUMER_FILE_PATH="xxx"
//      export APP_LOG_CONF_FILE="xxx"
func main() {
    hessian.RegisterPOJO(&User{})
    config.Load()
    time.Sleep(3e9)
    println("\n\n\nstart to test dubbo")
    user := &User{}
    err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
    if err != nil {
  panic(err)
    }
    println("response result: %v\n", user)
    initSignal()
}

main函數和服務端也相似,首先將傳輸結構註冊到hessian上,再調用config.Load()函數。在下文會介紹,客戶端和服務端會根據配置類型執行config.Load()中特定的函數loadConsumerConfig()和loadProviderConfig(),從而達到「開啓服務」、「調用服務」的目的。
加載完配置以後,仍是經過實現服務,增長函數proxy,申請registry,reloadInvoker指向服務端ip等操做,重寫了客戶端實例userProvider的對應函數,這時再經過調用GetUser函數,能夠直接經過invoker,調用到已經開啓的服務端,實現rpc過程。
下面會從server端和client端兩個角度,詳細講解服務啓動、registry註冊、調用過程:

1.5 自定義配置文件(非環境變量)方法

1.5.1 服務端自定義配置文件
  1. var providerConfigStr = xxxxx// 配置文件內容,能夠參考logclient

    • 在這裏你能夠定義配置文件的獲取方式,好比配置中心,本地文件讀取
  2. config.Load()以前設置配置,例如:
func main() {
    hessian.RegisterPOJO(&User{})
    providerConfig := config.ProviderConfig{}
    yaml.Unmarshal([]byte(providerConfigStr), &providerConfig)
    config.SetProviderConfig(providerConfig)
    defaultServerConfig := dubbo.GetDefaultServerConfig()
    dubbo.SetServerConfig(defaultServerConfig)
    logger.SetLoggerLevel("warn") // info,warn
    config.Load()
    select {
    }
}
1.5.2 客戶端自定義配置文件
  1. var consumerConfigStr = xxxxx// 配置文件內容,能夠參考logclient

    • 在這裏你能夠定義配置文件的獲取方式,好比配置中心,本地文件讀取
  2. config.Load()以前設置配置,例如:
func main() {
     p := config.ConsumerConfig{}
     yaml.Unmarshal([]byte(consumerConfigStr), &p)
     config.SetConsumerConfig(p)
     defaultClientConfig := dubbo.GetDefaultClientConfig()
     dubbo.SetClientConf(defaultClientConfig)
     logger.SetLoggerLevel("warn") // info,warn
     config.Load()

     user := &User{}
     err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
     if err != nil {
         log.Print(err)
         return
     }
  log.Print(user)
}

2. server端:

服務暴露過程涉及到屢次原始rpcService的封裝、暴露,網上其餘文章的圖感受太過籠統,我簡要的繪製了一個用戶定義服務的數據流圖

2.1 加載配置

2.1.1 框架初始化

在加載配置以前,框架提供了不少已定義好的協議、工廠等組件,都會在對應模塊init函數內註冊到extension模塊上,以供接下來配置文件中進行選用。
其中重要的有:

  1. 默認函數代理工廠
    common/proxy/proxy_factory/default.go
func init() {
    extension.SetProxyFactory("default", NewDefaultProxyFactory)
}

他的做用是將原始rpc-service進行封裝,造成proxy_invoker,更易於實現遠程call調用,詳情可見其invoke函數。

  1. 註冊中心註冊協議
    registry/protocol/protocol.go
func init() {
    extension.SetProtocol("registry", GetProtocol)
}

他負責在將invoker暴露給對應註冊中心,好比zk註冊中心。

  1. zookeeper註冊協議
    registry/zookeeper/zookeeper.go
func init() {
    extension.SetRegistry("zookeeper", newZkRegistry)
}

他合併了base_resiger,負責在服務暴露過程當中,將服務註冊在zookeeper註冊器上,從而爲調用者提供調用方法。

  1. dubbo傳輸協議
    protocol/dubbo/dubbo.go
func init() {
    extension.SetProtocol(DUBBO, GetProtocol)
}

他負責監聽對應端口,將具體的服務暴露,並啓動對應的事件handler,將遠程調用的event事件傳遞到invoker內部,調用本地invoker並得到執行結果返回。

  1. filter包裝調用鏈協議
    protocol/protocolwrapper/protocol_filter_wrapper.go
func init() {
    extension.SetProtocol(FILTER, GetProtocol)
}

他負責在服務暴露過程當中,將代理invoker打包,經過配置好的filter造成調用鏈,並交付給dubbo協議進行暴露。
上述提早註冊好的框架已實現的組件,在整個服務暴露調用鏈中都會用到,會根據配置取其所需。

2.1.2 配置文件

服務端須要的重要配置有三個字段:services、protocols、registries
profiles/dev/server.yaml:

registries :
  "demoZk":
    protocol: "zookeeper"
    timeout    : "3s"
    address: "127.0.0.1:2181"
services:
  "UserProvider":
    # 能夠指定多個registry,使用逗號隔開;不指定默認向全部註冊中心註冊
    registry: "demoZk"
    protocol : "dubbo"
    # 至關於dubbo.xml中的interface
    interface : "com.ikurento.user.UserProvider"
    loadbalance: "random"
    warmup: "100"
    cluster: "failover"
    methods:
    - name: "GetUser"
      retries: 1
      loadbalance: "random"
protocols:
  "dubbo":
    name: "dubbo"
    port: 20000

其中service指定了要暴露的rpc-service名("UserProvider),暴露的協議名("dubbo"),註冊的協議名("demoZk"),暴露的服務所處的interface,負載均衡策略,集羣失敗策略,調用的方法等等。
其中中間服務的協議名須要和registries下的mapkey對應,暴露的協議名須要和protocols下的mapkey對應
能夠看到上述例子中,使用了dubbo做爲暴露協議,使用了zookeeper做爲中間註冊協議,而且給定了端口。若是zk須要設置用戶名和密碼,也能夠在配置中寫好。

2.1.3 配置文件的讀入和檢查

config/config_loader.go:: Load()
在上述example的main函數中,有config.Load()函數的直接調用,該函數執行細節以下:

// Load Dubbo Init
func Load() {
    // init router
    initRouter()
    // init the global event dispatcher
    extension.SetAndInitGlobalDispatcher(GetBaseConfig().EventDispatcherType)
    // start the metadata report if config set
    if err := startMetadataReport(GetApplicationConfig().MetadataType, GetBaseConfig().MetadataReportConfig); err != nil {
  logger.Errorf("Provider starts metadata report error, and the error is {%#v}", err)
  return
    }
    // reference config
    loadConsumerConfig()
    // service config
    loadProviderConfig()
    // init the shutdown callback
    GracefulShutdownInit()
}

在本文中,咱們重點關心loadConsumerConfig()和loadProviderConfig()兩個函數
對於provider端,能夠看到loadProviderConfig()函數代碼以下:

前半部分是配置的讀入和檢查,進入for循環後,是單個service的暴露起始點。
前面提到,在配置文件中已經寫好了要暴露的service的種種信息,好比服務名、interface名、method名等等。在圖中for循環內,會將全部service的服務依次實現。
for循環的第一行,根據key調用GetProviderService函數,拿到註冊的rpcService實例,這裏對應上述提到的init函數中用戶手動註冊的本身實現的rpc-service實例:

這個對象也就成爲了for循環中的rpcService變量,將這個對象註冊經過Implement函數寫到sys(ServiceConfig類型)上,設置好sys的key和協議組,最終調用了sys的Export方法。
此處對應流程圖的部分:

至此,框架配置結構體已經拿到了全部service有關的配置,以及用戶定義好的rpc-service實例,他觸發了Export方法,旨在將本身的實例暴露出去。這是Export調用鏈的起始點。

2.2 原始service封裝入proxy_invoker

config/service_config.go :: Export()
接下來進入ServiceConfig.Export()函數:
這個函數進行了一些細碎的操做,好比爲不一樣的協議分配隨機端口,若是指定了多箇中心註冊協議,則會將服務經過多箇中心註冊協議的registryProtocol暴露出去,咱們只關心對於一個註冊協議是如何操做的。還有一些操做好比生成調用url和註冊url,用於爲暴露作準備。

2.2.1 首先經過配置生成對應registryUrl和serviceUrl

registryUrl是用來向中心註冊組件發起註冊請求的,對於zookeeper的話,會傳入其ip和端口號,以及附加的用戶名密碼等信息
這個regUrl目前只存有註冊(zk)相關信息,後續會補寫入ServiceIvk,即服務調用相關信息,裏面包含了方法名,參數等...

2.2.2 對於一個註冊協議,將傳入的rpc-service實例註冊在common.ServiceMap

這個Register函數將服務實例註冊了兩次,一次是以Interface爲key寫入接口服務組內,一次是以interface和proto爲key寫入特定的一個惟一的服務。
後續會從common.Map裏面取出來這個實例。

2.2.3 獲取默認代理工廠,將實例封裝入代理invoker
// 拿到一個proxyInvoker,這個invoker的url是傳入的regUrl,這個地方將上面註冊的service實例封裝成了invoker
// 這個GetProxyFactory返回的默認是common/proxy/proxy_factory/default.go
// 這個默認工廠調用GetInvoker得到默認的proxyInvoker,保存了當前註冊url
invoker := extension.GetProxyFactory(providerConfig.ProxyFactory).GetInvoker(*regUrl)
// 暴露出來 生成exporter,開啓tcp監聽
// 這裏就該跳到registry/protocol/protocol.go registryProtocol 調用的Export,將當前proxyInvoker導出
exporter = c.cacheProtocol.Export(invoker)

這一步的GetProxyFactory("default")方法獲取默認代理工廠,經過傳入上述構造的regUrl,將url封裝入代理invoker。
能夠進入common/proxy/proxy_factory/default.go::ProxyInvoker.Invoke()函數裏,看到對於common.Map取用爲svc的部分,以及關於svc對應Method的實際調用Call的函數以下:

到這裏,上面GetInvoker(*regUrl)返回的invoker即爲proxy_invoker,他封裝好了用戶定義的rpc_service,並將具體的調用邏輯封裝入了Invoke函數內。

爲何使用Proxy_invoker來調用?
我認爲,經過這個proxy_invoke調用用戶的功能函數,調用方式將更加抽象化,能夠在代碼中看到,經過ins和outs來定義入參和出參,將整個調用邏輯抽象化爲invocation結構體,而將具體的函數名的選擇,參數向下傳遞,reflect反射過程封裝在invoke函數內,這樣的設計更有利於以後遠程調用。我認爲這是dubbo Invoke調用鏈的設計思想。

至此,實現了圖中對應的部分:

2.3 registry協議在zkRegistry上暴露上面的proxy_invoker

上面,咱們執行到了exporter = c.cacheProtocol.Export(invoker)
這裏的cacheProtocol爲一層緩存設計,對應到原始的demo上,這裏是默認實現好的registryProtocol
registry/protocol/protocol.go:: Export()
這個函數內構造了多個EventListener,很是有java的設計感。
咱們只關心服務暴露的過程,先忽略這些監聽器。

2.3.1 獲取註冊url和服務url

2.3.2 獲取註冊中心實例zkRegistry

一層緩存操做,若是cache沒有須要從common裏面從新拿zkRegistry

2.3.3 zkRegistry調用Registry方法,在zookeeper上註冊dubboPath

上述拿到了具體的zkRegistry實例,該實例的定義在
registry/zookeeper/registry.go

該結構體組合了registry.BaseRegistry結構,base結構定義了註冊器基礎的功能函數,好比Registry、Subscribe等,但在這些默認定義的函數內部,仍是會調用facade層(zkRegistry層)的具體實現函數,這一設計模型能在保證已有功能函數不須要重複定義的同時,引入外層函數的實現,相似於結構體繼承卻又複用了代碼。
這一設計模式我認爲值得學習。
咱們查看上述registry/protocol/protocol.go:: Export()函數,直接調用了:

// 1. 經過zk註冊器,調用Register()函數,將已有@root@rawurl註冊到zk上
    err := reg.Register(*registeredProviderUrl)

將已有RegistryUrl註冊到了zkRegistry上。
這一步調用了baseRegistry的Register函數,進而調用zkRegister的DoRegister函數,進而調用:

在這個函數裏,將對應root創造一個新的節點

而且寫入具體node信息,node爲url通過encode的結果,包含了服務端的調用方式。
這部分的代碼較爲複雜,具體能夠看baseRegistry的 processURL()函數
至此,將服務端調用url註冊到了zookeeper上,而客戶端若是想獲取到這個url,只須要傳入特定的dubboPath,向zk請求便可。目前client是能夠獲取到訪問方式了,但服務端的特定服務尚未啓動,尚未開啓特定協議端口的監聽,這也是registry/protocol/protocol.go:: Export()函數接下來要作的事情:

2.3.4 proxy_invoker 封裝入wrapped_invoker,獲得filter調用鏈
// invoker封裝入warppedInvoker
wrappedInvoker := newWrappedInvoker(invoker, providerUrl)
// 通過爲invoker增長filter調用鏈,再使用dubbo協議Export,開啓service而且返回了Exporter 。
// export_1
cachedExporter = extension.GetProtocol(protocolwrapper.FILTER).Export(wrappedInvoker)

新建一個WrappedInvoker,用於以後鏈式調用
拿到提早實現並註冊好的ProtocolFilterWrapper,調用Export方法,進一步暴露。
protocol/protocolwrapped/protocol_filter_wrapper.go:Export()

protocol/protocolwrapped/protocol_filter_wrapper.go:buildInvokerChain

可見,根據配置的內容,經過鏈式調用的構造,層層將proxy_invoker包裹在調用鏈的最底部,最終返回一個調用鏈invoker。
對應圖中部分:

至此,咱們已經拿到filter調用鏈,期待將這個chain暴露到特定端口,用於相應請求事件

2.3.5 經過dubbo協議暴露wrapped_invoker

protocol/protocolwrapped/protocol_filter_wrapper.go:Export()

// 經過dubbo協議Export  dubbo_protocol調用的 export_2
    return pfw.protocol.Export(invoker)

回到上述Export函數的最後一行,調用了dubboProtocol的Export方法,將上述chain真正暴露。
該Export方法的具體實如今
protocol/dubbo/dubbo_protocol.go: Export()

這一函數作了兩個事情:構造觸發器、啓動服務

  1. 將傳入的Invoker調用chain進一步封裝,封裝成一個exporter,再將這個export放入map保存。注意!這裏昂exporter放入了SetExporterMap中,在下面服務啓動的時候,會以註冊事件監聽器的形式將這個exporter取出!
  2. 調用dubboProtocol的openServer方法,開啓一個針對特定端口的監聽

如上圖所示,一個Session被傳入,開啓對應端口的事件監聽。
至此構造出了exporter,完成圖中部分:

2.4 註冊觸發動做

上述只是啓動了服務,但尚未看到觸發事件的細節,點進上面的s.newSession能夠看到,dubbo協議爲一個getty的session默認使用了以下配置

其中很重要的一個配置是EventListener,傳入的是dubboServer的默認rpcHandler
protocol/dubbo/listener.go:OnMessage()
rpcHandler有一個實現好的OnMessage函數,根據getty的API,當client調用該端口時,會觸發OnMessage

// OnMessage notified when RPC server session got any message in connection
func (h *RpcServerHandler) OnMessage(session getty.Session, pkg interface{}) {

這一函數實現了在getty session接收到rpc 調用後的一系列處理:

  • 傳入包的解析

  • 根據請求包構造請求url

  • 拿到對應請求key,找到要被調用的exporter

  • 拿到對應的Invoker

  • 構造invocation

  • 調用

  • 返回

整個被調過程一鼓作氣。實現了從getty.Session的調用事件,到通過層層封裝的invoker的調用。
至此,一次rpc調用得以正確返回。

3. 小結

  • 關於Invoker的層層封裝
    能把一次調用抽象成一次invoke,能把一個協議抽象成針對invoke的封裝,能把針對一次invoke所作出的特定改變封裝到invoke函數內部,能夠下降模塊之間的耦合性。層層封裝邏輯更加清晰
  • 關於URL的抽象
    關於dubbo的統一化請求對象URL的極度抽象是我以前沒有見過的... 我認爲這樣封裝能保證請求參數列表的簡化和一致。但我認爲在開發的過程當中,濫用極度抽象的接口可能形成...debug的困難?以及不知道哪些字段是當前已經封裝好的,哪些字段是無用的。
  • 關於協議的理解
    以前理解的協議仍是太過具體化了,而關於dubbo-go對於dubboProtocol的協議,我認爲是基於getty的進一步封裝,他定義了客戶端和服務端,對於getty的session應該有哪些特定的操做,從而保證主調和被調的協議一致性,而這種保證也是一種協議的體現,是由dubbo協議來規範的。

若是你有任何疑問,歡迎釘釘掃碼加入交流羣【釘釘羣號 23331795】:

做者簡介

李志信 (GitHubID LaurenceLiZhixin),中山大學軟件工程專業在校學生,擅長使用 Java/Go 語言,專一於雲原生和微服務等技術方向。

相關文章
相關標籤/搜索