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的底层链接管理。一样的,我应该也会用一篇文章来介绍应该怎么设计,而后再用一篇文章来介绍具体的实现,咱们下篇文章再见。

再次感谢你的阅读!

相关文章
相关标签/搜索