摘要

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

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

在这一篇文章中,我们将从代码的角度来分析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
}

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

因为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 {
// 先查看是否在配置中存在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的底层连接管理。同样的,我应该也会用一篇文章来介绍应该怎么设计,然后再用一篇文章来介绍具体的实现,我们下篇文章再见。

再次感谢你的阅读!

gRPC-go源码(2):ClientConn的更多相关文章

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

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

  2. Go合集,gRPC源码分析,算法合集

    年初时,朋友圈见到的最多的就是新的一年新的FlAG,年末时朋友圈最多的也是xxxx就要过去了,你的FLAG实现了吗? 这个公众号2016就已经创建了,但截至今年之前从来没发表过文章,现在想想以前很忙, ...

  3. 源码解析Grpc拦截器(C#版本)

    前言 其实Grpc拦截器是我以前研究过,但是我看网上相关C#版本的源码解析相对少一点,所以笔者借这篇文章给大家分享下Grpc拦截器的实现,废话不多说,直接开讲(Grpc的源码看着很方便,包自动都能还原 ...

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

    1 写在前面 在这个系列的文章中,我们将会从源码的层面学习和理解gRPC. 整个系列的文章的计划大概是这样的:我们会先从客户端开始,沿着调用路径逐步分析到服务端,以模块为粒度进行学习,考虑这个模块是为 ...

  5. gRPC源码分析0-导读

    gRPC是Google开源的新一代RPC框架,官网是http://www.grpc.io.正式发布于2016年8月,技术栈非常的新,基于HTTP/2,netty4.1,proto3.虽然目前在工程化方 ...

  6. gRPC源码分析(c++)

    首先需要按照grpc官网上说的办法从github上下载源码,编译,然后跑一跑对应的测试代码.我分析的代码版本为v1.20.0. 在cpp的helloworld例子中,client端,第一个函数是创建c ...

  7. grpc源码分析之域名解析

    环境: win7_x64,VS2015.grpc_1.3.1 场景: 在客户端中使用grpc连接服务器,在多次输入非法的地址后,再次输入正确的地址连出现连接超时的现象.侯捷先生说过“源码面前,了无秘密 ...

  8. Fabric1.4源码解析: 链码容器启动过程

    想写点东西记录一下最近看的一些Fabric源码,本文使用的是fabric1.4的版本,所以对于其他版本的fabric,内容可能会有所不同. 本文想针对Fabric中链码容器的启动过程进行源码的解析.这 ...

  9. 菜鸟学习Fabric源码学习 — 背书节点和链码容器交互

    Fabric 1.4 源码分析 背书节点和链码容器交互 本文档主要介绍背书节点和链码容器交互流程,在Endorser背书节点章节中,无论是deploy.upgrade或者调用链码,最后都会调用Chai ...

随机推荐

  1. Java_web项目中在Java文件里面通过类装载器对资源文件读取

    承接上一节:在eclipse完成对Java_web项目里面资源文件的读取 我们首先在src目录下创建一个资源文件db.properties 内容如下: url=127.0.0.1 name=root ...

  2. 【noi 2.6_7627】鸡蛋的硬度(DP)

    题意:其中n表示楼的高度,m表示你现在拥有的鸡蛋个数. 解法:f[i][j]表示 i 层楼有 j 个鸡蛋时,至少要扔多少次.3重循环,k为测试的楼层,分这时扔下去的鸡蛋碎和不碎的情况.要注意初始化. ...

  3. 牛客的两道dfs

    1.传送门:牛客13594-选择困难症 题意:给你k类物品,每类物品有a[i]个每个物品都有一个value,每类物品最多选一个,要求有多少种选法使得总value>m(没要求每类物品都必须选) 题 ...

  4. C++11 Java基本数据类型以及转换

    写在前面: 母语是Java,后来学了C++11,这两个语言的基本数据类型隐式转换不太一样,有点晕,整理一下 整理自网络和书籍,标明出处 C++ 基本数据类型 --http://www.cnblogs. ...

  5. 二叉树增删改查 && 程序实现

    二叉排序树定义 一棵空树,或者是具有下列性质的二叉树:(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值:(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值:(3)左.右子树也 ...

  6. HDU 6390 GuGuFishtion(莫比乌斯反演 + 欧拉函数性质 + 积性函数)题解

    题意: 给定\(n,m,p\),求 \[\sum_{a=1}^n\sum_{b=1}^m\frac{\varphi(ab)}{\varphi(a)\varphi(b)}\mod p \] 思路: 由欧 ...

  7. 记一次 Billu_b0x渗透

    目录: 0x01 寻找ip 1.这边我们是使用的nmap来寻找我们的靶机IP地址,开始Ip是1,结束是254,153是我kali的ip,所以158就是我们的靶机的ip地址了. 2. 查看端口服务 这边 ...

  8. μC/OS-III---I笔记9---任务等待多个内核对象和任务内建信号量与消息队列

    在一个任务等待多个内核对象在之前,信号量和消息队列的发布过程中都有等待多个内核对象判断的函数,所谓任务等待多个内核对象顾名思义就是一任务同时等待多个内核对象而被挂起,在USOC-III中一个任务等待多 ...

  9. R语言学习2:绘图

    本系列是一个新的系列,在此系列中,我将和大家共同学习R语言.由于我对R语言的了解也甚少,所以本系列更多以一个学习者的视角来完成. 参考教材:<R语言实战>第二版(Robert I.Kaba ...

  10. JavaScript Inheritance All in One

    JavaScript Inheritance All in One constructor inheritance prototype chain inheritance "use stri ...