在上一篇文章中,咱们聊了聊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
的底层链接管理。一样的,我应该也会用一篇文章来介绍应该怎么设计,而后再用一篇文章来介绍具体的实现,咱们下篇文章再见。
再次感谢你的阅读!