gRPC-go源码(2):ClientConn

摘要

在上一篇文章中,我们聊了聊gRPC是怎么管理一条从Client到Server的连接的。

我们聊到了gRPC拥有Resolver,用来解析地址;拥有Balancer,用来做负载均衡。

在这一篇文章中,我们将从代码的角度来分析gRPC是怎么设计Resolver和Balancer的,并会从头到尾的梳理一遍连接是怎么建立的。

1 DialContext

DialContext是客户端建立连接的入口函数,我们看看在这个函数里面做了哪些事情:

func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {// 1.创建ClientConn结构体cc := &ClientConn{target:            target,...}// 2.解析targetcc.parsedTarget = grpcutil.ParseTarget(cc.target, cc.dopts.copts.Dialer != nil)// 3.根据解析的target找到合适的resolverBuilderresolverBuilder := cc.getResolver(cc.parsedTarget.Scheme)// 4.创建ResolverrWrapper, err := newCCResolverWrapper(cc, resolverBuilder)// 5.完事return cc, nil
}

显而易见,在省略了亿点点细节之后,我们发现建立连接的过程其实也很简单,我们梳理一遍:

因为gRPC没有提供服务注册,服务发现的功能,所以需要开发者自己编写服务发现的逻辑:也就是Resolver——解析器。

在得到了解析的结果,也就是一连串的IP地址之后,需要对其中的IP进行选择,也就是Balancer。

其余的就是一些错误处理、兜底策略等等,这些内容不在这一篇文章中讲解。

2 Resolver的获取

我们从Resolver开始讲起。

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 {// 先查看是否在配置中存在resolverfor _, 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
}

好,到了这里我们可以暂停一下。

我们停下来思考一下我们需要实现的功能:为了解耦Resolver和Balancer,我们希望能够有一个中间的部分,接收到Resolver解析到的地址,然后对它们进行负载均衡。因此,在接下来的代码阅读过程中,我们可以带着这个问题:Resolver和Balancer的通信过程是什么样的?

再看上面的代码,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。

这个时候逻辑就很清晰了,gRPC的ClientConn通过调用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,那么使用配置中的balancerif cc.sc != nil && cc.sc.lbConfig != nil {newBalancerName = cc.sc.lbConfig.name} // 否则的话,遍历解析结果中的地址,来判断应该使用哪种balancerelse {var isGRPCLB boolfor _, a := range addrs {if a.Type == resolver.GRPCLB {isGRPCLB = truebreak}}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.balancerWrapperuccsErr := 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连接的管理逻辑也比较的复杂,我们下篇文章见。

6 小结

到这里我们来总结一下:创建ClientConn的时候创建ResolverWrapper,由ClientConn通知ResolverWrapper进行域名解析。

此时,ResolverWrapper会将这个请求交给真正的Resolver,由真正的Resolver来处理域名解析。

解析完毕后,Resolver会将结果保存在ResolverWrapper中,ResolverWrapper再将这个结果返回给ClientConn。

当ClientConn发现解析的结果发生了改变,那么他就会去通知BalancerWrapper,重新进行负载均衡。 此时BalancerWrapper又会去让真正的Balancer做这件事,最终将结果返回给ClientConn。

我们画张图来展示这个过程:

gRPC-go源码(2):ClientConn相关推荐

  1. gRPC源码分析2-Server的建立

    gRPC中,Server.Client共享的Class不是很多,所以我们可以单独的分别讲解Server和Client的源码. 通过第一篇,我们知道对于gRPC来说,建立Server是非常简单的,还记得 ...

  2. loraserver 源码解析 (六) lora-app-server

    目录 下载源码 升级 npm 安装一些必要的依赖库 pq_trgm extension run 调用 handleDataDownPayloads 开启一个Goroutine  G1 run再调用 s ...

  3. gRPC-go源码(1):连接管理

    gRPC-go源码(1):连接管理 1 写在前面 在这个系列的文章中,我们将会从源码的层面学习和理解gRPC. 整个系列的文章的计划大概是这样的:我们会先从客户端开始,沿着调用路径逐步分析到服务端,以 ...

  4. 从源码透析gRPC调用原理

    导语 gRPC是什么,不用多说了. gRPC如何用,也不用多说了 . 但是,gRPC是如何work的,清楚的理解其调用逻辑,对于我们更好.更深入的使用gRPC很有必要.因此我们必须深度解析下gRPC的 ...

  5. GRPC golang版源码分析之客户端(二)

    Table of Contents 1. 前言 2. 负载均衡 3. 相关链接 1 前言 前面一篇文章分析了一个grpc call的大致调用流程,顺着源码走了一遍,但是grpc中有一些特性并没有进行分 ...

  6. GRPC golang版源码分析之客户端(一)

    Table of Contents 1. 前言 2. 源码目录浏览 3. 客户端 4. 相关链接 1 前言 grpc是一个通用的rpc框架,用google实现,当然也有go语言的版本.在工作中主要用到 ...

  7. java如何通过grpc连接etcd_grpc通过 etcd 实现服务发现与注册-源码分析

    介绍 下面介绍 jupiter-0.2.7 版本中 grpc 通过 etcd 实现服务发现与注册. 服务发现与注册的实现解析 服务注册 服务注册的流程图: etcd的服务注册代码模块在 jupiter ...

  8. gRPC服务注册发现及负载均衡的实现方案与源码解析

    今天聊一下gRPC的服务发现和负载均衡原理相关的话题,不同于Nginx.Lvs或者F5这些服务端的负载均衡策略,gRPC采用的是客户端实现的负载均衡.什么意思呢,对于使用服务端负载均衡的系统,客户端会 ...

  9. 在Windows和Linux上编译gRPC源码操作步骤(C++)

    gRPC最新发布版本为v1.23.0,下面以此版本为例说明在Windows和Linux下编译过程. Windows7/10 vs2103编译gRPC源码操作步骤: 1. 需要本机已安装Git.CMak ...

最新文章

  1. 从最强AI算力到“元脑”2.0,智算加速产业变革
  2. 聚焦与发散——浅谈编程的发展方向
  3. Spark的RDD行动算子
  4. Git show-branch显示提交信息
  5. 【转】[iOS] 关于 self = [super init];
  6. python读取mysql数据库行数_使用python读取mysql数据库并进行数据的操作
  7. 软件行业正面临一场新的变革——SaaS软件
  8. 经典python题目练习
  9. mod函数在vb中怎么用?
  10. RV-LINK:输出非预期响应向 GDB 报告错误
  11. Android安全相关
  12. 22届春季校招实习试水之路2(前端)
  13. Linux下按照时间段过滤日志
  14. UCOSII操作系统(四)--任务管理
  15. 解决Vagrant启动虚拟机内存爆满
  16. channel 的底层原理
  17. 【论文阅读】A Survey on Dynamic Neural Networks for Natural Language Processing
  18. Mysql最全笔记,快速入门,干货满满,爆肝
  19. maven 如何移除无用的依赖
  20. 你的工作就是最好的面试-邹欣

热门文章

  1. 试解释如下两个概念:CLR和CTS
  2. C# 取二位小数点(四舍五入)
  3. 链接生成动态二维码图片显示在页面上
  4. Eclipse导入Solr源码Version5.5.3
  5. mos管电路_三极管和MOS管原来这样用,混用代价高,电路设计中需谨慎
  6. 模拟实现HashMap
  7. cif文件服务器搭建,在linux下搭建NFS服务器实现文件共享
  8. win10如何截屏_win10系统电脑截屏的多种操作方法
  9. 安装配置Exchange 问题集
  10. 架构中的设计原则之单一职责原则 - 《java开发技术-在架构中体验设计模式和算法之美》...