dubbo-go 源碼:https://github.com/apache/dubbo-gojava
導讀:隨着微服務架構的流行,許多高性能 rpc 框架應運而生,由阿里開源的 dubbo 框架 go 語言版本的 dubbo-go 也成爲了衆多開發者不錯的選擇。本文將介紹 dubbo-go 框架的基本使用方法,以及從 export 調用鏈的角度進行 server 端源碼導讀,但願能引導讀者進一步認識這款框架。下週將發表本文的姊妹篇:《從 client 端源碼導讀 dubbo-go 框架》。
當拿到一款框架以後,一種不錯的源碼閱讀方式大體以下:從運行最基礎的 helloworld demo 源碼開始 —> 再查看配置文件 —> 開啓各類依賴服務(好比zk、consul) —> 開啓服務端 —> 再到經過 client 調用服務端 —> 打印完整請求日誌和回包。調用成功以後,再根據框架的設計模型,從配置文件解析開始,自頂向下遞閱讀整個框架的調用棧。node
對於 C/S 模式的 rpc 請求來講,整個調用棧被拆成了 client 和 server 兩部分,因此能夠分別從 server 端的配置文件解析閱讀到 server 端的監聽啓動,從 client 端的配置文件解析閱讀到一次 invoker Call 調用。這樣一次完整請求就明晰了起來。git
官方 demo 相關連接:https://github.com/dubbogo/dubbo-samples/tree/master/golang/helloworld/dubbogithub
$ git clone https://github.com/dubbogo/dubbo-samples.git
$ cd dubbo-samples/golang/helloworld/dubbo
進入目錄後可看到四個文件夾,分別支持 go 和 java 的 client 以及 server,咱們嘗試運行一個 go 的 server。進入 app 子文件夾內,能夠看到裏面保存了 go 文件。golang
$ cd go-server/app
能夠在 go-server 裏面看到三個文件夾:app、assembly、profiles。apache
其中 app 文件夾下保存 go 源碼,assembly 文件夾下保存可選的針對特定環境的 build 腳本,profiles 下保存配置文件。對於 dubbo-go 框架,配置文件很是重要,沒有文件將致使服務沒法啓動。設計模式
因爲 dubbo-go 框架依賴配置文件啓動,讓框架定位到配置文件的方式就是經過環境變量來找。對於 server 端須要兩個必須配置的環境變量:CONF_PROVIDER_FILE_PATH、APP_LOG_CONF_FILE,分別應該指向服務端配置文件、日誌配置文件。緩存
在 sample 裏面,咱們可使用 dev 環境,即 profiles/dev/log.yml 和 profiles/dev/server.yml 兩個文件。在 app/ 下,經過命令行中指定好這兩個文件:session
$ export CONF_PROVIDER_FILE_PATH="../profiles/dev/server.yml"$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"架構
$ go run .
若是提示 timeout,則須要設置 goproxy 代理。
$ export GOPROXY="http://goproxy.io"
再運行 go run 便可開啓服務。
安裝 zookeeper,並運行 zkServer, 默認爲 2181 端口。
$ cd go-client/app
$ export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
配置 go 代理:
$ 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 調用成功。
export APP_LOG_CONF_FILE="../profiles/dev/log.yml" export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"
dubbo-go 框架的 example 提供的目錄以下:
源碼放置在 app/ 文件夾下,主要包含 server.go 和 user.go 兩個文件,顧名思義,server.go 用於使用框架開啓服務以及註冊傳輸協議;user.go 則定義了 rpc-service 結構體,以及傳輸協議的結構。
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 函數執行完畢。
// 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() 優雅地結束一個服務的啓動過程。
客戶端包含 client.go 和 user.go 兩個文件,其中 user.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 註冊和調用過程。
xxxxx
// 配置文件內容,能夠參考 log 和 client。在這裏你能夠定義配置文件的獲取方式,好比配置中心,本地文件讀取。log 地址: https://github.com/dubbogo/dubbo-samples/blob/master/golang/helloworld/dubbo/go-client/profiles/release/log.yml
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 { } }
xxxxx
// 配置文件內容,能夠參考 log 和 clien。在這裏你能夠定義配置文件的獲取方式,好比配置中心,本地文件讀取。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) }
服務暴露過程涉及到屢次原始 rpcService 的封裝、暴露,網上其餘文章的圖感受太過籠統,在此,簡要地繪製了一個用戶定義服務的數據流圖:
在加載配置以前,框架提供了不少已定義好的協議、工廠等組件,都會在對應模塊 init 函數內註冊到 extension 模塊上,以供接下來配置文件中進行選用。
其中重要的有:
func init() { extension.SetProxyFactory("default", NewDefaultProxyFactory) }
它的做用是將原始 rpc-service 進行封裝,造成 proxy_invoker,更易於實現遠程 call 調用,詳情可見其 invoke 函數。
registry/protocol/protocol.go
func init() { extension.SetProtocol("registry", GetProtocol) }
它負責將 invoker 暴露給對應註冊中心,好比 zk 註冊中心。
func init() { extension.SetRegistry("zookeeper", newZkRegistry) }
它合併了 base_resiger,負責在服務暴露過程當中,將服務註冊在 zookeeper 註冊器上,從而爲調用者提供調用方法。
func init() { extension.SetProtocol(DUBBO, GetProtocol) }
它負責監聽對應端口,將具體的服務暴露,並啓動對應的事件 handler,將遠程調用的 event 事件傳遞到 invoker 內部,調用本地 invoker 並得到執行結果返回。
func init() { extension.SetProtocol(FILTER, GetProtocol) }
它負責在服務暴露過程當中,將代理 invoker 打包,經過配置好的 filter 造成調用鏈,並交付給 dubbo 協議進行暴露。
上述提早註冊好的框架已實現的組件,在整個服務暴露調用鏈中都會用到,會根據配置取其所需。
服務端須要的重要配置有三個字段:services、protocols、registries。
profiles/dev/server.yml:
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 須要設置用戶名和密碼,也能夠在配置中寫好。
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 調用鏈的起始點。
config/service_config.go :: Export()
接下來進入 ServiceConfig.Export() 函數.
這個函數進行了一些細碎的操做,好比爲不一樣的協議分配隨機端口,若是指定了多箇中心註冊協議,則會將服務經過多箇中心註冊協議的 registryProtocol 暴露出去,咱們只關心對於一個註冊協議是如何操做的。還有一些操做好比生成調用 url 和註冊 url,用於爲暴露作準備。
registryUrl 是用來向中心註冊組件發起註冊請求的,對於 zookeeper 的話,會傳入其 ip 和端口號,以及附加的用戶名密碼等信息。
這個 regUrl 目前只存有註冊(zk)相關信息,後續會補寫入 ServiceIvk,即服務調用相關信息,裏面包含了方法名,參數等...
這個 Register 函數將服務實例註冊了兩次,一次是以 Interface 爲 key 寫入接口服務組內,一次是以 interface 和 proto 爲 key 寫入特定的一個惟一的服務。
後續會從 common.Map 裏面取出來這個實例。
// 拿到一個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 調用鏈的設計思想。
至此,實現了圖中對應的部分:
上面,咱們執行到了 exporter = c.cacheProtocol.Export(invoker)。
這裏的 cacheProtocol 爲一層緩存設計,對應到原始的 demo 上,這裏是默認實現好的 registryProtocol。
registry/protocol/protocol.go:: Export()
這個函數內構造了多個 EventListener,很是有 java 的設計感。
咱們只關心服務暴露的過程,先忽略這些監聽器。
一層緩存操做,若是 cache 沒有須要從 common 裏面從新拿 zkRegistry。
上述拿到了具體的 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() 函數:http://t.tb.cn/6Xje4bijnsIDNaSmyPc4Ot。
至此,將服務端調用 url 註冊到了 zookeeper 上,而客戶端若是想獲取到這個 url,只須要傳入特定的 dubboPath,向 zk 請求便可。目前 client 是能夠獲取到訪問方式了,但服務端的特定服務尚未啓動,尚未開啓特定協議端口的監聽,這也是 registry/protocol/protocol.go:: Export() 函數接下來要作的事情。
// 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 暴露到特定端口,用於相應請求事件。
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()。
這一函數作了兩個事情:構造觸發器、啓動服務。
如上圖所示,一個 Session 被傳入,開啓對應端口的事件監聽。
至此構造出了 exporter,完成圖中部分:
上述只是啓動了服務,但尚未看到觸發事件的細節,點進上面的 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 調用後的一系列處理:
整個被調過程一鼓作氣。實現了從 getty.Session 的調用事件,到通過層層封裝的 invoker 的調用。
至此,一次 rpc 調用得以正確返回。
能把一次調用抽象成一次 invoke;能把一個協議抽象成針對 invoke 的封裝;能把針對一次 invoke 所作出的特定改變封裝到 invoke 函數內部,能夠下降模塊之間的耦合性。層層封裝邏輯更加清晰。
關於 dubbo 的統一化請求對象 URL 的極度抽象是以前沒有見過的... 我的認爲這樣封裝能保證請求參數列表的簡化和一致。但在開發的過程當中,濫用極度抽象的接口可能形成... debug 的困難?以及不知道哪些字段是當前已經封裝好的,哪些字段是無用的。
以前理解的協議仍是太過具體化了,而關於 dubbo-go 對於 dubboProtocol 的協議,我認爲是基於 getty 的進一步封裝,它定義了客戶端和服務端,對於 getty 的 session 應該有哪些特定的操做,從而保證主調和被調的協議一致性,而這種保證也是一種協議的體現,是由 dubbo 協議來規範的。
若是你有任何疑問,歡迎釘釘掃碼加入交流羣:釘釘羣號 23331795!
李志信 (GitHubID LaurenceLiZhixin),中山大學軟件工程專業在校學生,擅長使用 Java/Go 語言,專一於雲原生和微服務等技術方向。
「 阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,作最懂雲原生開發者的公衆號。」