gRPC-go源碼(2):ClientConn

摘要

在上一篇文章中,咱們聊了聊gRPC是怎麼管理一條從ClientServer的鏈接的。數據結構

咱們聊到了gRPC擁有Resolver,用來解析地址;擁有Balancer,用來作負載均衡。app

在這一篇文章中,咱們將從代碼的角度來分析gRPC是怎麼設計ResolverBalancer的,並會從頭至尾的梳理一遍鏈接是怎麼創建的。負載均衡

1 DialContext

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

2 Resolver的獲取

咱們從Resolver開始講起。對象

cc.parsedTarget = grpcutil.ParseTarget(cc.target, cc.dopts.copts.Dialer != nil)

關於ParseTarget的邏輯咱們用簡單一句話來歸納:獲取開發者傳入的target參數的地址類型,在後續查找適合這種類型地址的Resolverblog

而後咱們來看查找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的細節:

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
}

好,到了這裏咱們能夠暫停一下。

咱們停下來思考一下咱們須要實現的功能:爲了解耦ResolverBalancer,咱們但願可以有一箇中間的部分,接收到Resolver解析到的地址,而後對它們進行負載均衡。所以,在接下來的代碼閱讀過程當中,咱們能夠帶着這個問題:ResolverBalancer的通訊過程是什麼樣的?

再看上面的代碼,ClientConn的建立已經結束了。那麼咱們能夠推測,剩下的邏輯就在rb.Build(cc.parsedTarget, ccr, rbo)這一行代碼裏面。

4 Resolver的建立

其實,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

這個時候邏輯就很清晰了,gRPCClientConn經過調用ccResolverWrapper來進行域名解析,而具體的解析過程則由開發者本身決定。在解析完畢後,將解析的結果返回給ccResolverWrapper

5 Balancer的選擇

咱們所以也能夠進行推測:在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的建立過程當中,涉及到了鏈接的管理。咱們一樣的把這部份內容放在下一篇中。在這篇文章中咱們的主線任務仍是ResolverBalancer的交互是怎麼樣的。

在建立完相應的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

咱們畫張圖來展現這個過程:

寫在最後

首先,謝謝你能看到這裏。

這是一篇純源碼解讀的文章,做爲上一篇純理論文章的補充。建議兩篇文章配合一塊兒食用:)

若是在這個過程當中,你有任何的疑問,均可以留言給我,或者在公衆號「紅雞菌」中找到我。

在下一篇文章中,我將向你介紹Balancer中的具體細節,也就是gRPC的底層鏈接管理。一樣的,我應該也會用一篇文章來介紹應該怎麼設計,而後再用一篇文章來介紹具體的實現,咱們下篇文章再見。

再次感謝你的閱讀!

相關文章
相關標籤/搜索