1. 程式人生 > >gRPC-go原始碼(2):ClientConn

gRPC-go原始碼(2):ClientConn

## 摘要 在上一篇文章中,我們聊了聊`gRPC`是怎麼管理一條從`Client`到`Server`的連線的。 我們聊到了`gRPC`擁有`Resolver`,用來解析地址;擁有`Balancer`,用來做負載均衡。 在這一篇文章中,我們將從程式碼的角度來分析`gRPC`是怎麼設計`Resolver`和`Balancer`的,並會從頭到尾的梳理一遍連線是怎麼建立的。 ## 1 DialContext `DialContext`是客戶端建立連線的入口函式,我們看看在這個函式裡面做了哪些事情: ```go 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 } ``` 顯而易見,在省略了億點點細節之後,我們發現建立連線的過程其實也很簡單,我們梳理一遍: 因為gRPC沒有提供服務註冊,服務發現的功能,所以需要開發者自己編寫服務發現的邏輯:也就是`Resolver`——**解析器**。 在得到了解析的結果,也就是一連串的IP地址之後,需要對其中的IP進行選擇,也就是`Balancer`。 其餘的就是一些錯誤處理、兜底策略等等,這些內容不在這一篇文章中講解。 ## 2 Resolver的獲取 我們從`Resolver`開始講起。 ```go cc.parsedTarget = grpcutil.ParseTarget(cc.target, cc.dopts.copts.Dialer != nil) ``` 關於`ParseTarget`的邏輯我們用簡單一句話來概括:獲取開發者傳入的target引數的地址型別,在後續查詢適合這種型別地址的`Resolver`。 然後我們來看查詢`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的註冊和獲取。 ## 3 ResolverWrapper的建立 回到`ClientConn`的建立過程中,在獲取到了`ResolverBuilder`之後,進行下一步的操作: ``` rWrapper, err := newCCResolverWrapper(cc, resolverBuilder) ``` `gRPC`為了實現外掛式的`Resolver`,因此採用了裝飾器模式,建立了一個`ResolverWrapper`。 我們看看在建立`ResolverWrapper`的細節: ```go 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)`這一行程式碼裡面。 ## 4 Resolver的建立 其實,`Build`並不是一個確定的方法,他是一個介面。 ```go type Builder interface { Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error) } ``` 在建立`Resolver`的時候,我們需要在`Build`方法裡面初始化`Resolver`的各種狀態。並且,因為`Build`方法中有一個`target`的引數,我們會在建立`Resolver`的時候,需要對這個`target`進行解析。 也就是說,建立`Resolver`的時候,會進行**第一次**的域名解析。並且,這個解析過程,是由開發者自己設計的。 到了這裡我們會自然而然的接著考慮,解析之後的結果應該儲存為什麼樣的資料結構,又應該怎麼去將這個結果傳遞下去呢? 我們拿最簡單的`passthroughResolver`來舉例: ```go 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`。 ## 5 Balancer的選擇 我們因此也可以進行推測:在`ccResolverWrapper`中,會將解析出的結果以某種形式傳遞給`Balancer`。 我們接著往下看: ```go 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`的原始碼。 ```go 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`到底做了什麼: ```go 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`。 ```go 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`連線的管理邏輯也比較的複雜,我們下篇文章見。 ## 6 小結 到這裡我們來總結一下:建立`ClientConn`的時候建立`ResolverWrapper`,由`ClientConn`通知`ResolverWrapper`進行域名解析。 此時,`ResolverWrapper`會將這個請求交給真正的Resolver,由真正的`Resolver`來處理域名解析。 解析完畢後,Resolver會將結果儲存在`ResolverWrapper`中,`ResolverWrapper`再將這個結果返回給`ClientConn`。 當`ClientConn`發現解析的結果發生了改變,那麼他就會去通知`BalancerWrapper`,重新進行負載均衡。 此時`BalancerWrapper`又會去讓真正的`Balancer`做這件事,最終將結果返回給`ClientConn`。 我們畫張圖來展示這個過程: ![](https://img2020.cnblogs.com/blog/1998080/202103/1998080-20210301234438217-742218048.png) ## 寫在最後 首先,謝謝你能看到這裡。 這是一篇純原始碼解讀的文章,作為上一篇純理論文章的補充。建議兩篇文章配合一起食用:) 如果在這個過程中,你有任何的疑問,都可以留言給我,或者在公眾號**“紅雞菌”**中找到我。 在下一篇文章中,我將向你介紹`Balancer`中的具體細節,也就是`gRPC`的底層連線管理。同樣的,我應該也會用一篇文章來介紹應該怎麼設計,然後再用一篇文章來介紹具體的實現,我們下篇文章再見。 再次感謝你的