在上一篇文章中,咱們聊了聊gRPC
是怎麼管理一條從Client
到Server
的鏈接的。數據結構
咱們聊到了gRPC
擁有Resolver
,用來解析地址;擁有Balancer
,用來作負載均衡。app
在這一篇文章中,咱們將從代碼的角度來分析gRPC
是怎麼設計Resolver
和Balancer
的,並會從頭至尾的梳理一遍鏈接是怎麼創建的。負載均衡
DialContext
是客戶端創建鏈接的入口函數,咱們看看在這個函數裏面作了哪些事情:函數
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { // 1.建立ClientConn結構體 cc := &ClientConn{ target: target, ... } // 2.解析target cc.parsedTarget = grpcutil.ParseTarget(cc.target, cc.dopts.copts.Dialer != nil) // 3.根據解析的target找到合適的resolverBuilder resolverBuilder := cc.getResolver(cc.parsedTarget.Scheme) // 4.建立Resolver rWrapper, err := newCCResolverWrapper(cc, resolverBuilder) // 5.完事 return cc, nil }
顯而易見,在省略了億點點細節以後,咱們發現創建鏈接的過程其實也很簡單,咱們梳理一遍:ui
由於gRPC沒有提供服務註冊,服務發現的功能,因此須要開發者本身編寫服務發現的邏輯:也就是Resolver
——解析器。插件
在獲得瞭解析的結果,也就是一連串的IP地址以後,須要對其中的IP進行選擇,也就是Balancer
。設計
其他的就是一些錯誤處理、兜底策略等等,這些內容不在這一篇文章中講解。code
咱們從Resolver
開始講起。對象
cc.parsedTarget = grpcutil.ParseTarget(cc.target, cc.dopts.copts.Dialer != nil)
關於ParseTarget
的邏輯咱們用簡單一句話來歸納:獲取開發者傳入的target參數的地址類型,在後續查找適合這種類型地址的Resolver
。blog
而後咱們來看查找Resolver
的這部分操做,這部分代碼比較簡單,我在代碼中加了一些註釋:
resolverBuilder := cc.getResolver(cc.parsedTarget.Scheme) func (cc *ClientConn) getResolver(scheme string) resolver.Builder { // 先查看是否在配置中存在resolver for _, rb := range cc.dopts.resolvers { if scheme == rb.Scheme() { return rb } } // 若是配置中沒有相應的resolver,再從註冊的resolver中尋找 return resolver.Get(scheme) } // 能夠看出,ResolverBuilder是從m這個map裏面找到的 func Get(scheme string) Builder { if b, ok := m[scheme]; ok { return b } return nil }
看到這裏咱們能夠推測:對於每一個ResolverBuilder
,是須要提早註冊的。
咱們找到Resolver
的代碼中,果真發現他在init()
的時候註冊了本身。
func init() { resolver.Register(&passthroughBuilder{}) } // 註冊Resolver,便是把本身加入map中 func Register(b Builder) { m[b.Scheme()] = b }
至此,咱們已經研究完了Resolver的註冊和獲取。
回到ClientConn
的建立過程當中,在獲取到了ResolverBuilder
以後,進行下一步的操做:
rWrapper, err := newCCResolverWrapper(cc, resolverBuilder)
gRPC
爲了實現插件式的Resolver
,所以採用了裝飾器模式,建立了一個ResolverWrapper
。
咱們看看在建立ResolverWrapper
的細節:
func newCCResolverWrapper(cc *ClientConn, rb resolver.Builder) (*ccResolverWrapper, error) { ccr := &ccResolverWrapper{ cc: cc, done: grpcsync.NewEvent(), } // 根據傳入的Builder,建立resolver,並放入wrapper中 ccr.resolver, err = rb.Build(cc.parsedTarget, ccr, rbo) return ccr, nil }
好,到了這裏咱們能夠暫停一下。
咱們停下來思考一下咱們須要實現的功能:爲了解耦Resolver
和Balancer
,咱們但願可以有一箇中間的部分,接收到Resolver
解析到的地址,而後對它們進行負載均衡。所以,在接下來的代碼閱讀過程當中,咱們能夠帶着這個問題:Resolver
和Balancer
的通訊過程是什麼樣的?
再看上面的代碼,ClientConn
的建立已經結束了。那麼咱們能夠推測,剩下的邏輯就在rb.Build(cc.parsedTarget, ccr, rbo)
這一行代碼裏面。
其實,Build
並非一個肯定的方法,他是一個接口。
type Builder interface { Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error) }
在建立Resolver
的時候,咱們須要在Build
方法裏面初始化Resolver
的各類狀態。而且,由於Build
方法中有一個target
的參數,咱們會在建立Resolver
的時候,須要對這個target
進行解析。
也就是說,建立Resolver
的時候,會進行第一次的域名解析。而且,這個解析過程,是由開發者本身設計的。
到了這裏咱們會天然而然的接着考慮,解析以後的結果應該保存爲何樣的數據結構,又應該怎麼去將這個結果傳遞下去呢?
咱們拿最簡單的passthroughResolver
來舉例:
func (*passthroughBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { r := &passthroughResolver{ target: target, cc: cc, } // 建立Resolver的時候,進行第一次的解析 r.start() return r, nil } // 對於passthroughResolver來講,正如他的名字,直接將參數做爲結果返回 func (r *passthroughResolver) start() { r.cc.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: r.target.Endpoint}}}) }
咱們能夠看到,對於一個Resolver
,須要將解析出的地址,傳入resolver.State
中,而後調用r.cc.UpdateState
方法。
那麼這個r.cc.UpdateState
又是什麼呢?
他就是咱們上面提到的ccResolverWrapper
。
這個時候邏輯就很清晰了,gRPC
的ClientConn
經過調用ccResolverWrapper
來進行域名解析,而具體的解析過程則由開發者本身決定。在解析完畢後,將解析的結果返回給ccResolverWrapper
。
咱們所以也能夠進行推測:在ccResolverWrapper
中,會將解析出的結果以某種形式傳遞給Balancer
。
咱們接着往下看:
func (ccr *ccResolverWrapper) UpdateState(s resolver.State) { ... // 將Resolver解析的最新狀態保存下來 ccr.curState = s // 對狀態進行更新 ccr.poll(ccr.cc.updateResolverState(ccr.curState, nil)) }
關於poll
方法這裏就不提了,重點咱們看ccr.cc.updateResolverState(ccr.curState, nil)
這部分。
這裏的ccr.cc
中的cc
,就是咱們建立的ClientConn
對象。
也就是說,此時Resolver
解析的結果,最終又回到了ClientConn
中。
注意,對於updateResolverState
方法,在源碼中邏輯比較深,主要是爲了處理各類狀況。在這裏我直接把核心的那部分貼出來,因此這部分的代碼你能夠理解爲是僞代碼實現,和本來的代碼是有出入的。若是你但願看到具體的實現,你能夠去閱讀gRPC
的源碼。
func (cc *ClientConn) updateResolverState(s resolver.State, err error) error { var newBalancerName string // 假設已經配置好了balancer,那麼使用配置中的balancer if cc.sc != nil && cc.sc.lbConfig != nil { newBalancerName = cc.sc.lbConfig.name } // 不然的話,遍歷解析結果中的地址,來判斷應該使用哪一種balancer else { var isGRPCLB bool for _, a := range addrs { if a.Type == resolver.GRPCLB { isGRPCLB = true break } } if isGRPCLB { newBalancerName = grpclbName } else if cc.sc != nil && cc.sc.LB != nil { newBalancerName = *cc.sc.LB } else { newBalancerName = PickFirstBalancerName } } // 具體的balancer邏輯 cc.switchBalancer(newBalancerName) // 使用balancerWrapper更新Client的狀態 bw := cc.balancerWrapper uccsErr := bw.updateClientConnState(&balancer.ClientConnState{ResolverState: s, BalancerConfig: balCfg}) return ret }
咱們再來康康switchBalancer
到底作了什麼:
func (cc *ClientConn) switchBalancer(name string) { ... builder := balancer.Get(name) cc.curBalancerName = builder.Name() cc.balancerWrapper = newCCBalancerWrapper(cc, builder, cc.balancerBuildOpts) }
是否是有一種似曾相識的感受?
沒錯,這部分的代碼,跟ResolverWrapper
的建立過程很接近。都是獲取到對應的Builder Name
,而後經過name
來獲取對應的Builder
,而後建立wrapper
。
func newCCBalancerWrapper(cc *ClientConn, b balancer.Builder, bopts balancer.BuildOptions) *ccBalancerWrapper { ccb := &ccBalancerWrapper{ cc: cc, scBuffer: buffer.NewUnbounded(), done: grpcsync.NewEvent(), subConns: make(map[*acBalancerWrapper]struct{}), } go ccb.watcher() ccb.balancer = b.Build(ccb, bopts) return ccb }
這裏的ccb.watcher
咱們先無論他,這個是跟鏈接的狀態有關的內容,咱們將在下一篇文章在進行分析。
一樣的,Build
具體的Balancer
的過程,也是由開發者本身決定的。
在Balancer的建立過程當中,涉及到了鏈接的管理。咱們一樣的把這部份內容放在下一篇中。在這篇文章中咱們的主線任務仍是Resolver
和Balancer
的交互是怎麼樣的。
在建立完相應的BalancerWrapper
以後,就來到了bw.updateClientConnState
這行了。
注意,這裏的bw
就是咱們上面建立的balancer
。也就是說這裏又來到了真正的Balancer
邏輯。
可是這其中的代碼咱們在這篇文章中先不進行介紹,gRPC
對於真正的HTTP/2
鏈接的管理邏輯也比較的複雜,咱們下篇文章見。
到這裏咱們來總結一下:建立ClientConn
的時候建立ResolverWrapper
,由ClientConn
通知ResolverWrapper
進行域名解析。
此時,ResolverWrapper
會將這個請求交給真正的Resolver,由真正的Resolver
來處理域名解析。
解析完畢後,Resolver會將結果保存在ResolverWrapper
中,ResolverWrapper
再將這個結果返回給ClientConn
。
當ClientConn
發現解析的結果發生了改變,那麼他就會去通知BalancerWrapper
,從新進行負載均衡。
此時BalancerWrapper
又會去讓真正的Balancer
作這件事,最終將結果返回給ClientConn
。
咱們畫張圖來展現這個過程:
首先,謝謝你能看到這裏。
這是一篇純源碼解讀的文章,做爲上一篇純理論文章的補充。建議兩篇文章配合一塊兒食用:)
若是在這個過程當中,你有任何的疑問,均可以留言給我,或者在公衆號「紅雞菌」中找到我。
在下一篇文章中,我將向你介紹Balancer
中的具體細節,也就是gRPC
的底層鏈接管理。一樣的,我應該也會用一篇文章來介紹應該怎麼設計,而後再用一篇文章來介紹具體的實現,咱們下篇文章再見。
再次感謝你的閱讀!