Dubbo-go Client端調用服務過程

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"導讀:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了上一篇文章"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/1eaa3531f5e37cc35e6eabcbchttps://blog.csdn.net/forevermoonlight/article/details/108962115","title":""},"content":[{"type":"text","text":"《Dubbo-go Server 端開啓服務過程》"}]},{"type":"text","text":"的鋪墊,可以類比客戶端啓動於服務端的啓動過程。其中最大的區別是服務端通過 zk 註冊服務,發佈自己的ivkURL並訂閱事件開啓監聽;而客戶應該是通過zk註冊組件,拿到需要調用的serviceURL,更新invoker並重寫用戶的RPCService,從而實現對遠程過程調用細節的封裝。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"1. 配置文件和客戶端源碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1 client配置文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"helloworld提供的demo:profiles/client.yaml"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"registries :\n \"demoZk\":\n protocol: \"zookeeper\"\n timeout : \"3s\"\n address: \"127.0.0.1:2181\"\n username: \"\"\n password: \"\"\nreferences:\n \"UserProvider\":\n # 可以指定多個registry,使用逗號隔開;不指定默認向所有註冊中心註冊\n registry: \"demoZk\"\n protocol : \"dubbo\"\n interface : \"com.ikurento.user.UserProvider\"\n cluster: \"failover\"\n methods :\n - name: \"GetUser\"\n retries: 3"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可看到配置文件與之前討論過的server端非常類似,其refrences部分字段就是對當前服務要主調的服務的配置,其中詳細說明了調用協議、註冊協議、接口id、調用方法、集羣策略等,這些配置都會在之後與註冊組件交互,重寫ivk、調用的過程中使用到。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.2 客戶端使用框架源碼"}]},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: user.go\nfunc init() {\n config.SetConsumerService(userProvider)\n hessian.RegisterPOJO(&User{})\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: main.go\nfunc main() {\n hessian.RegisterPOJO(&User{})\n config.Load()\n time.Sleep(3e9)\n println(\"\\n\\n\\nstart to test dubbo\")\n user := &User{}\n err := userProvider.GetUser(context.TODO(), []interface{}{\"A001\"}, user)\n if err != nil {\n panic(err)\n }\n println(\"response result: %v\\n\", user)\n initSignal()\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"官網提供的helloworld demo的源碼。可看到與服務端類似,在user.go內註冊了rpc-service,以及需要rpc傳輸的結構體user。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在main函數中,同樣調用了config.Load()函數,之後就可以直接通過實現好的rpc-service:userProvider 直接調用對應的功能函數,即可實現rpc調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以猜到,從hessian註冊結構、SetConsumerService,到調用函數.GetUser()期間,用戶定義的rpc-service也就是userProvider對應的函數被重寫,重寫後的GetUser函數已經包含了實現了遠程調用邏輯的invoker。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,就要通過閱讀源碼,看看dubbo-go是如何做到的。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"2. 實現遠程過程調用"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 加載配置文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file:config/config_loader.go :Load()\n\n// Load Dubbo Init\nfunc Load() {\n // init router\n initRouter()\n // init the global event dispatcher\n extension.SetAndInitGlobalDispatcher(GetBaseConfig().EventDispatcherType)\n // start the metadata report if config set\n if err := startMetadataReport(GetApplicationConfig().MetadataType, GetBaseConfig().MetadataReportConfig); err != nil {\n logger.Errorf(\"Provider starts metadata report error, and the error is {%#v}\", err)\n return\n }\n // reference config\n loadConsumerConfig()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在main函數中調用的config.Load()函數,進而調用了loadConsumerConfig,類似於之前講到的server端配置讀入函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在loadConsumerConfig函數中,進行了三步操作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: config/config_loader.go\n\nfunc loadConsumerConfig() {\n // 1 init other consumer config\n conConfigType := consumerConfig.ConfigType\n for key, value := range extension.GetDefaultConfigReader() {}\n checkApplicationName(consumerConfig.ApplicationConfig)\n configCenterRefreshConsumer()\n checkRegistries(consumerConfig.Registries, consumerConfig.Registry)\n \n // 2 refer-implement-reference\n for key, ref := range consumerConfig.References {\n if ref.Generic {\n genericService := NewGenericService(key)\n SetConsumerService(genericService)\n }\n rpcService := GetConsumerService(key)\n ref.id = key\n ref.Refer(rpcService)\n ref.Implement(rpcService)\n }\n\n // 3 wait for invoker is available, if wait over default 3s, then panic\n for {}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 檢查配置文件並將配置寫入內存"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. "},{"type":"text","marks":[{"type":"strong"}],"text":"在for循環內部"},{"type":"text","text":",依次引用(refer)並且實例化(implement)每個被調reference。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 等待三秒鐘所有invoker就緒"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中重要的就是for循環裏面的引用和實例化,兩步操作,會在接下來展開討論。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,配置已經被寫入了框架。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 獲取遠程Service URL,實現可供調用的invoker"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述的ref.Refer完成的就是這部分的操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a7/a710e0d54df9ba92d24228b021237fca.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖(一)"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.1 構造註冊url"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"和server端類似,存在註冊url和服務url,dubbo習慣將服務url作爲註冊url的sub。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: config/reference_config.go: Refer()\n\nfunc (c *ReferenceConfig) Refer(_ interface{}) {\n //(一)配置url參數(serviceUrl),將會作爲sub\n cfgURL := common.NewURLWithOptions(\n common.WithPath(c.id),\n common.WithProtocol(c.Protocol),\n common.WithParams(c.getUrlMap()),\n common.WithParamsValue(constant.BEAN_NAME_KEY, c.id),\n )\n ...\n // (二)註冊地址可以通過url格式給定,也可以通過配置格式給定\n // 這一步的意義就是配置->提取信息生成URL\n if c.Url != \"\" {// 用戶給定url信息,可以是點對點的地址,也可以是註冊中心的地址\n // 1. user specified URL, could be peer-to-peer address, or register center's address.\n urlStrings := gxstrings.RegSplit(c.Url, \"\\\\s*[;]+\\\\s*\")\n for _, urlStr := range urlStrings {\n serviceUrl, err := common.NewURL(urlStr)\n ...\n }\n } else {// 配置讀入註冊中心的信息\n // assemble SubURL from register center's configuration mode\n // 這是註冊url,protocol = registry,包含了zk的用戶名、密碼、ip等等\n c.urls = loadRegistries(c.Registry, consumerConfig.Registries, common.CONSUMER)\n ...\n // set url to regUrls\n for _, regUrl := range c.urls {\n regUrl.SubURL = cfgURL// regUrl的subURl存當前配置url\n }\n }\n //至此,無論通過什麼形式,已經拿到了全部的regURL\n // (三)獲取registryProtocol實例,調用其Refer方法,傳入新構建好的regURL\n if len(c.urls) == 1 {\n // 這一步訪問到registry/protocol/protocol.go registryProtocol.Refer\n // 這裏是registry\n c.invoker = extension.GetProtocol(c.urls[0].Protocol).Refer(*c.urls[0])\n } else {\n // 如果有多個註冊中心,即有多個invoker,則採取集羣策略\n invokers := make([]protocol.Invoker, 0, len(c.urls))\n ...\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個函數中,已經處理完從Register配置到RegisterURL的轉換,即圖(一)中部分:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b8/b893dd65aa320c2901a222660d6fa904.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,已經拿到的url將被傳遞給RegistryProtocol,進一步refer。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.2 registryProtocol獲取到zkRegistry實例,進一步Refer"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: registry/protocol/protocol.go: Refer\n\n// Refer provider service from registry center\n// 拿到的是配置文件registries的url,他能夠生成一個invoker = 指向目的addr,以供客戶端直接調用。\nfunc (proto *registryProtocol) Refer(url common.URL) protocol.Invoker {\n var registryUrl = url\n // 這裏拿到的是referenceConfig,serviceUrl裏面包含了Reference的所有信息,包含interfaceName、method等等\n var serviceUrl = registryUrl.SubURL\n if registryUrl.Protocol == constant.REGISTRY_PROTOCOL {// registryUrl.Proto = \"registry\"\n protocol := registryUrl.GetParam(constant.REGISTRY_KEY, \"\")\n registryUrl.Protocol = protocol//替換成了具體的值,比如\"zookeeper\"\n }\n // 接口對象\n var reg registry.Registry\n // (一)實例化接口對象,緩存策略\n if regI, loaded := proto.registries.Load(registryUrl.Key()); !loaded {\n // 緩存中不存在當前registry,新建一個reg\n reg = getRegistry(&registryUrl)\n // 緩存起來\n proto.registries.Store(registryUrl.Key(), reg)\n } else {\n reg = regI.(registry.Registry)\n }\n // 到這裏,獲取到了reg實例 zookeeper的registry\n //(二)根據Register的實例zkRegistry和傳入的regURL新建一個directory\n // 這一步存在複雜的異步邏輯,從註冊中心拿到了目的service的真實addr,獲取了invoker並放入directory,\n // 這一步將在下面詳細給出步驟\n // new registry directory for store service url from registry\n directory, err := extension.GetDefaultRegistryDirectory(&registryUrl, reg)\n if err != nil {\n logger.Errorf(\"consumer service %v create registry directory error, error message is %s, and will return nil invoker!\",\n serviceUrl.String(), err.Error())\n return nil\n }\n // (三)DoRegister 在zk上註冊當前client service\n err = reg.Register(*serviceUrl)\n if err != nil {\n logger.Errorf(\"consumer service %v register registry %v error, error message is %s\",\n serviceUrl.String(), registryUrl.String(), err.Error())\n }\n // (四)new cluster invoker,將directory寫入集羣,獲得具有集羣策略的invoker\n cluster := extension.GetCluster(serviceUrl.GetParam(constant.CLUSTER_KEY, constant.DEFAULT_CLUSTER))\n invoker := cluster.Join(directory)\n // invoker保存\n proto.invokers = append(proto.invokers, invoker)\n return invoker\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可詳細閱讀上述註釋,這個函數完成了從url到invoker的全部過程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(一)首先獲得Registry對象,默認是之前實例化的zkRegistry,和之前server獲取Registry的處理很類似。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(二)通過構造一個新的directory,異步拿到之前在zk上註冊的server端信息,生成invoker"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(三)在zk上註冊當前service"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(四)集羣策略,獲得最終invoker"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一步完成了圖(一)中所有餘下的絕大多數操作,接下來就需要詳細的查看directory的構造過程:"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.3 構造directory(包含較複雜的異步操作)"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/44/443a79e991df95bb099da308faa04c29.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖(二)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述的 "},{"type":"codeinline","content":[{"type":"text","text":"extension.GetDefaultRegistryDirectory(&registryUrl, reg)"}]},{"type":"text","text":"函數,本質上調用了已經註冊好的"},{"type":"codeinline","content":[{"type":"text","text":"NewRegistryDirectory"}]},{"type":"text","text":"函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: registry/directory/directory.go: NewRegistryDirectory()\n\n// NewRegistryDirectory will create a new RegistryDirectory\n// 這個函數作爲default註冊在extension上面\n// url爲註冊url,reg爲zookeeper registry\nfunc NewRegistryDirectory(url *common.URL, registry registry.Registry) (cluster.Directory, error) {\n if url.SubURL == nil {\n return nil, perrors.Errorf(\"url is invalid, suburl can not be nil\")\n }\n dir := &RegistryDirectory{\n BaseDirectory: directory.NewBaseDirectory(url),\n cacheInvokers: []protocol.Invoker{},\n cacheInvokersMap: &sync.Map{},\n serviceType: url.SubURL.Service(),\n registry: registry,\n }\n dir.consumerConfigurationListener = newConsumerConfigurationListener(dir)\n go dir.subscribe(url.SubURL)\n return dir, nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先構造了一個註冊directory,開啓協程調用其subscribe函數,傳入serviceURL。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個directory目前包含了對應的zkRegistry,以及傳入的URL,他cacheInvokers的部分是空的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進入dir.subscribe(url.SubURL)這個異步函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: registry/directory/directory.go: subscribe()\n\n// subscribe from registry\nfunc (dir *RegistryDirectory) subscribe(url *common.URL) {\n // 增加兩個監聽,\n dir.consumerConfigurationListener.addNotifyListener(dir)\n dir.referenceConfigurationListener = newReferenceConfigurationListener(dir, url)\n // subscribe調用\n dir.registry.Subscribe(url, dir)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重點來了,他調用了zkRegistry的Subscribe方法,與此同時將自己作爲ConfigListener傳入"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我認爲這種傳入listener的設計模式非常值得學習,而且很有java的味道。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對等待zk返回訂閱信息這樣的異步操作,需要傳入一個Listener,這個Listener需要實現Notify方法,進而在作爲參數傳入內部之後,可以被異步地調用Notify,將內部觸發的異步事件“傳遞出來”,再進一步處理加工。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"層層的Listener事件鏈,能將傳入的原始serviceURL通過zkConn發送給zk服務,獲取到服務端註冊好的url對應的二進制信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而Notify回調鏈,則將這串byte[]一步一步解析、加工;以事件的形式向外傳遞,最終落到directory上的時候,已經是成型的newInvokers了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體細節不再以源碼形式展示,可參照上圖查閱源碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此已經拿到了server端註冊好的真實invoker。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成了圖(一)中的部分:"}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/21/21ef1360c04a762b87c4729c675aa3ee.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.4 構造帶有集羣策略的clusterinvoker"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過上述操作,已經拿到了server端Invokers,放入了directory的cacheinvokers數組裏面緩存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後續的操作對應本文2.2.2的第四步,由directory生成帶有特性集羣策略的invoker"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// (四)new cluster invoker,將directory寫入集羣,獲得具有集羣策略的invoker\n cluster := extension.GetCluster(serviceUrl.GetParam(constant.CLUSTER_KEY, constant.DEFAULT_CLUSTER))\n invoker := cluster.Join(directory)\n123"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Join函數的實現就是如下函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: cluster/clusterimpl/failovercluster_invokers.go: newFailoverClusterInvoker()\n\nfunc newFailoverClusterInvoker(directory cluster.Directory) protocol.Invoker {\n return &failoverClusterInvoker{\n baseClusterInvoker: newBaseClusterInvoker(directory),\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"dubbo-go框架默認選擇failover策略,既然返回了一個invoker,我們查看一下failoverClusterInvoker的Invoker方法,看他是如何將集羣策略封裝到Invoker函數內部的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: cluster/cluster_impl/failover_cluster_invokers.go: Invoker()\n\n// Invoker 函數\nfunc (invoker *failoverClusterInvoker) Invoke(ctx context.Context, invocation protocol.Invocation) protocol.Result {\n ...\n //調用List方法拿到directory緩存的所有invokers\n invokers := invoker.directory.List(invocation)\n if err := invoker.checkInvokers(invokers, invocation); err != nil {// 檢查是否可以實現調用\n return &protocol.RPCResult{Err: err}\n }\n // 獲取來自用戶方向傳入的\n methodName := invocation.MethodName()\n retries := getRetries(invokers, methodName)\n loadBalance := getLoadBalance(invokers[0], invocation)\n for i := 0; i <= retries; i++ {\n // 重要!這裏是集羣策略的體現,失敗後重試!\n //Reselect before retry to avoid a change of candidate `invokers`.\n //NOTE: if `invokers` changed, then `invoked` also lose accuracy.\n if i > 0 {\n if err := invoker.checkWhetherDestroyed(); err != nil {\n return &protocol.RPCResult{Err: err}\n }\n invokers = invoker.directory.List(invocation)\n if err := invoker.checkInvokers(invokers, invocation); err != nil {\n return &protocol.RPCResult{Err: err}\n }\n }\n // 這裏是負載均衡策略的體現!選擇特定ivk進行調用。\n ivk := invoker.doSelect(loadBalance, invocation, invokers, invoked)\n if ivk == nil {\n continue\n }\n invoked = append(invoked, ivk)\n //DO INVOKE\n result = ivk.Invoke(ctx, invocation)\n if result.Error() != nil {\n providers = append(providers, ivk.GetUrl().Key())\n continue\n }\n return result\n }\n ...\n}"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看了很多Invoke函數的實現,所有類似的Invoker函數都包含兩個方向,一個是用戶方向的invcation,一個是函數方向的底層invokers。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而集羣策略的invoke函數本身作爲接線員,把invocation一步步解析,根據調用需求和集羣策略,選擇特定的invoker來執行"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"proxy函數也是這樣,一個是用戶方向的ins[] reflect.Type, 一個是函數方向的invoker。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"proxy函數負責將ins轉換爲invocation,調用對應invoker的invoker函數,實現連通。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而出於這樣的設計,可以在一步步Invoker封裝的過程中,每個Invoker只關心自己負責操作的部分,從而使整個調用棧解耦。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"秒啊!!!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,我們理解了failoverClusterInvoker 的Invoke函數實現,也正是和這個集羣策略Invoker被返回,接受來自上方的調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"已完成圖(一)中的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b5/b59ea1bddfdd392fb303c99f85fd4ab7.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.2.5 在zookeeper上註冊當前client"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"拿到invokers後,可以回到這個函數了:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: config/refrence_config.go: Refer()\n\nif len(c.urls) == 1 {\n // 這一步訪問到registry/protocol/protocol.go registryProtocol.Refer\n c.invoker = extension.GetProtocol(c.urls[0].Protocol).Refer(*c.urls[0])\n // (一)拿到了真實的invokers\n } else {\n // 如果有多個註冊中心,即有多個invoker,則採取集羣策略\n invokers := make([]protocol.Invoker, 0, len(c.urls))\n ...\n cluster := extension.GetCluster(hitClu)\n // If 'zone-aware' policy select, the invoker wrap sequence would be:\n // ZoneAwareClusterInvoker(StaticDirectory) ->\n // FailoverClusterInvoker(RegistryDirectory, routing happens here) -> Invoker\n c.invoker = cluster.Join(directory.NewStaticDirectory(invokers))\n }\n // (二)create proxy,爲函數配置代理\n if c.Async {\n callback := GetCallback(c.id)\n c.pxy = extension.GetProxyFactory(consumerConfig.ProxyFactory).GetAsyncProxy(c.invoker, callback, cfgURL)\n } else {\n // 這裏c.invoker已經是目的addr了\n c.pxy = extension.GetProxyFactory(consumerConfig.ProxyFactory).GetProxy(c.invoker, cfgURL)\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們有了可以打通的invokers,但還不能直接調用,因爲invoker的入參是invocation,而調用函數使用的是具體的參數列表。需要通過一層proxy來規範入參和出參。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來新建一個默認proxy,放置在c.proxy內,以供後續使用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,完成了圖(一)中最後的操作"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/28/2818614794c118b396ab004f8412ff6f.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.3 將調用邏輯以代理函數的形式寫入rpc-service"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面完成了config.Refer操作"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回到"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"config/config_loader.go: loadConsumerConfig()"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/88/881256613ed8dbe6e37f46188d34e15b.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下一個重要的函數是Implement,他完的操作較爲簡單:旨在使用上面生成的c.proxy代理,鏈接用戶自己定義的rpcService到clusterInvoker的信息傳輸。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"函數較長,只選取了重要的部分:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: common/proxy/proxy.go: Implement()\n\n// Implement\n// proxy implement\n// In consumer, RPCService like:\n// type XxxProvider struct {\n// Yyy func(ctx context.Context, args []interface{}, rsp *Zzz) error\n// }\n// Implement 實現的過程,就是proxy根據函數名和返回值,通過調用invoker 構造出擁有遠程調用邏輯的代理函數\n// 將當前rpc所有可供調用的函數註冊到proxy.rpc內\nfunc (p *Proxy) Implement(v common.RPCService) {\n // makeDubboCallProxy 這是一個構造代理函數,這個函數的返回值是func(in []reflect.Value) []reflect.Value 這樣一個函數\n // 這個被返回的函數是請求實現的載體,由他來發起調用獲取結果\n makeDubboCallProxy := func(methodName string, outs []reflect.Type) func(in []reflect.Value) []reflect.Value {\n return func(in []reflect.Value) []reflect.Value {\n // 根據methodName和outs的類型,構造這樣一個函數,這個函數能將in 輸入的value轉換爲輸出的value\n // 這個函數具體的實現如下:\n ...\n // 目前拿到了 methodName、所有入參的interface和value,出參數reply\n // (一)根據這些生成一個 rpcinvocation\n inv = invocation_impl.NewRPCInvocationWithOptions(\n invocation_impl.WithMethodName(methodName),\n invocation_impl.WithArguments(inIArr),\n invocation_impl.WithReply(reply.Interface()),\n invocation_impl.WithCallBack(p.callBack),\n invocation_impl.WithParameterValues(inVArr))\n for k, value := range p.attachments {\n inv.SetAttachments(k, value)\n }\n // add user setAttachment\n atm := invCtx.Value(constant.AttachmentKey) // 如果傳入的ctx裏面有attachment,也要寫入inv\n if m, ok := atm.(map[string]string); ok {\n for k, value := range m {\n inv.SetAttachments(k, value)\n }\n }\n // 至此構造inv完畢\n // (二)觸發Invoker 之前已經將cluster_invoker放入proxy,使用Invoke方法,通過getty遠程過程調用\n result := p.invoke.Invoke(invCtx, inv)\n // 如果有attachment,則加入\n if len(result.Attachments()) > 0 {\n invCtx = context.WithValue(invCtx, constant.AttachmentKey, result.Attachments())\n }\n ...\n }\n }\n numField := valueOfElem.NumField()\n for i := 0; i < numField; i++ {\n t := typeOf.Field(i)\n methodName := t.Tag.Get(\"dubbo\")\n if methodName == \"\" {\n methodName = t.Name\n }\n f := valueOfElem.Field(i)\n if f.Kind() == reflect.Func && f.IsValid() && f.CanSet() { // 針對於每個函數\n outNum := t.Type.NumOut()\n // 規定函數輸出只能有1/2個\n if outNum != 1 && outNum != 2 {\n logger.Warnf(\"method %s of mtype %v has wrong number of in out parameters %d; needs exactly 1/2\",\n t.Name, t.Type.String(), outNum)\n continue\n }\n // The latest return type of the method must be error.\n // 規定最後一個返回值一定是error\n if returnType := t.Type.Out(outNum - 1); returnType != typError {\n logger.Warnf(\"the latest return type %s of method %q is not error\", returnType, t.Name)\n continue\n }\n // 獲取到所有的出參類型,放到數組裏\n var funcOuts = make([]reflect.Type, outNum)\n for i := 0; i < outNum; i++ {\n funcOuts[i] = t.Type.Out(i)\n }\n // do method proxy here:\n // (三)調用make函數,傳入函數名和返回值,獲得能調用遠程的proxy,將這個proxy替換掉原來的函數位置\n f.Set(reflect.MakeFunc(f.Type(), makeDubboCallProxy(methodName, funcOuts)))\n logger.Debugf(\"set method [%s]\", methodName)\n }\n }\n ...\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正如之前所說,proxy的作用是將用戶定義的函數參數列表,轉化爲抽象的invocation傳入Invoker,進行調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中已標明有三處較爲重要的地方:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在代理函數中實現由參數列表生成Invocation的邏輯"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在代理函數實現調用Invoker的邏輯"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"將代理函數替換爲原始rpc-service對應函數"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,也就解決了一開始的問題:"}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// file: client.go: main()\n\nconfig.Load()\nuser := &User{}\nerr := userProvider.GetUser(context.TODO(), []interface{}{\"A001\"}, user)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏直接調用用戶定義的rpcService的函數GetUser,這裏實際調用的是經過重寫入的函數代理,所以就能實現遠程調用了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"3. 從client到server的invoker嵌套鏈 - 小結"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在閱讀dubbo-go源碼的過程中,我能發現一條清晰的invoker-proxy嵌套鏈,我希望通過圖的形式來展現:"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5f/5ff59ddeb3dc646ee3de5bbd22e91dc0.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章