grpc clientconn.go

这个文件有几个核心数据结构,我们逐个拆解。

// ClientConn represents a client connection to an RPC server.
type ClientConn struct {
	ctx    context.Context
	cancel context.CancelFunc

	target       string
	parsedTarget resolver.Target
	authority    string
	dopts        dialOptions
	csMgr        *connectivityStateManager

	balancerBuildOpts balancer.BuildOptions
	resolverWrapper   *ccResolverWrapper
	blockingpicker    *pickerWrapper

	mu    sync.RWMutex
	sc    ServiceConfig
	scRaw string
	conns map[*addrConn]struct{}
	// Keepalive parameter can be updated if a GoAway is received.
	mkp             keepalive.ClientParameters
	curBalancerName string
	preBalancerName string // previous balancer name.
	curAddresses    []resolver.Address
	balancerWrapper *ccBalancerWrapper

	channelzID          int64 // channelz unique identification number
	czmu                sync.RWMutex
	callsStarted        int64
	callsSucceeded      int64
	callsFailed         int64
	lastCallStartedTime time.Time
}
ctx    context.Context
cancel context.CancelFunc

grpc的数据结构中很多包含ctx和cancel方法,是在初始化ClientConn时利用上层的ctx衍生的孩子ctx和取消方法,让ctx在整个框架中树状延伸到所有对象,控制围绕所有对象可能存在的goroutine。在ClientConn中ctx在scWatcher中负责退出时机的工作。

target       string
parsedTarget resolver.Target
authority    string

target可以参考grpc_naming_doc,通过该字段解析出app配置的哪个resolver,并让该resolver运行起来,从target中的地址定期得到address列表,并刷新到内存中,辅助连接的管理。

parsedTarget是target解析后结果,没想到真的有数据结构这么设计,我之前的设计思路是针对这种二次产物,通过方法得到,每次调用方法都重新解析,代码清晰些。之后这块观念需要有改变了。

authority的来源有3种,之间有优先级别。之前一般不会这么设计,就是希望app开发模式单一,防止出现不同来源之间的优先级对rd造成困扰。从target字符串中解析出该字段是最低优先级的。我觉得这中设计是历史问题导致,设计者不断推出更好的设计思路,但好要保持历史兼容。=这块的设计方式,给框架类项目升级带来一点启示,就是一定要在保持兼容的情况下,优化各种配置的来源。=

csMgr        *connectivityStateManager

csMgr是工具类数据结构,用于维护连接状态,先看下该结构体:

type connectivityStateManager struct {
	mu         sync.Mutex
	state      connectivity.State
	notifyChan chan struct{}
}

数据结构基本就能反映设计者的目的,利用mu保护state,并通过notifyChan将某些状态变化通知出去。这里唯一值得学习的是notifyChan的设计方法,每当updateState被调用,且连接状态改变时,且发现有其他对象关注这个channel的时候(关注的意思就是,对象被初始化,可以参考getNotifyChan),就关闭该channel,把状态变化的动作广播出去,外部自己进行其他需要的判断,类似是否变为某个状态。

看这块的时候还有一个比较常见的设计是针对select关键字的,看下面代码:

select {
	case <-ctx.Done():
		return false
	case <-ch:
		return true
}

上面代码比较常见,当case中没有条件满足的时候会阻塞,当增加default关键字的时候就会退出,简单,但考验设计者对于select的了解程度。

balancerBuildOpts balancer.BuildOptions
resolverWrapper   *ccResolverWrapper
blockingpicker    *pickerWrapper

这块设计包含的内容比较多,包括:naming和负载均衡。

先说下naming的设计,在后端服务化体系中,这块技术是老生长谈了,一般框架都使用dns服务,简单实用,没必要做过多花哨的东西。client端配置dns地址,定时获取ip和port的列表并加载到内存中即可。当然你可以自己实现一套类似的东西,用etcd或者zookeeper都行。小米网围绕etcd实现的,服务发现和更新。简单查了下dns的相关东西,我没有产生出任何需要公司内部利用etcd或者zookeeper实现同样功能的需求,我说的是仅仅在服务器列表刷新这块,而不是利用dns服务器进行负载均衡,可能自己实现的目的主要是性能原因,可以看看blog

resolver在grpc中是一个独立的目录,包含文件:resolver.go 目录:dns/manual/passthrough,这种包的管理方法需要借鉴,其实就是strategy模式的一种目录划分方式。首先,要根据需求支持不同的协议,协议设计起来简单,主要是字符串和ip port列表的对应关系,这些不同需求、不同协议就封装在各子目录中。其次,要支持上层通过某种方式决定使用那种strategy,grpc实现是通过target中的scheme来决定的,当然在各pkg中的init方法中,都要在初始化阶段注册自己的strategy实现到上层。

resolver.go中包含几个interface的设计,interface的设计一定是与外部交互用的,下面看看resolver都需要与哪些外部对象交互,怎么交互。

// Builder creates a resolver that will be used to watch name resolution updates.
type Builder interface {
	// Build creates a new resolver for the given target.
	//
	// gRPC dial calls Build synchronously, and fails if the returned error is
	// not nil.
	Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error)
	// Scheme returns the scheme supported by this resolver.
	// Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md.
	Scheme() string
}
// Resolver watches for the updates on the specified target.
// Updates include address updates and service config updates.
type Resolver interface {
	// ResolveNow will be called by gRPC to try to resolve the target name
	// again. It's just a hint, resolver can ignore this if it's not necessary.
	//
	// It could be called multiple times concurrently.
	ResolveNow(ResolveNowOption)
	// Close closes the resolver.
	Close()
}

Builder定义每个想要嵌入grpc中的自定义resolver需要实现什么方法,首先你要有唯一表示标识(scheme),其次要有Build方法,参数中传入target(resolver的唯一数据来源)。该接口给出了strategy模式的具体构成之一:对象生成接口。设计时需要你关注的具体点都有哪些,基本可以平移到很多场合进行使用。

Resolver接口用于定义允许使用方怎么控制resolver,主要包含开启和关闭,开启的内部的实现形式很多,可以允许外部在各种时机调用,也可以只调用一次。对于resolver,是需要允许随时进行刷新的。触发点包括:

  • DialContext,创建ClientConn之后
  • transportMonitor,当transport出现错误后,重置transport的时候
  • createTransport,创建传输层对象之后

transport的实现在后续说明。

resolver_conn_wrapper.go中的ccResolverWrapper是方便clientconn.go使用而设计的一个包装器,该struct实现了resolver.go中定义的ClientConn接口,该接口是resolver向外输出地址列表的出口。wrapper设计围绕select建立,开启goroutine监听地址相关的chan,有的时候就通知上层clientconn.go。

具体resolver的实现,不具体描述了,有兴趣就直接看源码,很短,理解接口的定义之后,实现方式就是细枝末节了。

接下来聊负载均衡,balancer相关的东西比较多。具体的balancer实现方法就是在地址列表上为每次client发出的请求选择一个服务器端地址,老生常谈了,你可以把roundrobin或者稍微复杂的选择算法说的头头是道,但是当你面对需要支持很多种不同策略的balancer包的时候,怎么设计,这个问题不见的人人能规划好。

balancer.go包含设计思想的核心。Balancer可以理解为balancer的定义。

roundrobin

HandleSubConnStateChange,允许外界的SubConn状态变化被传入进来,=interface不在乎具体实现,只定义balancer的通用行为=,例如roundrobin中由状态变化触发的动作包括:状态机变化、各状态连接数量统计、是否要刷新picker中的地址列表。picker和balancer的关系,需要注意,picker是具体封装算法的工人,而balancer更像是监工,帮picker屏蔽外界的干扰,让picker只关注从地址列表中选择下一个SubConn即可,balancer从外界感知变化,并操控picker生产内容。

HandleResolvedAddrs,当resolver导致addr列表有变化时,addr和SubConn对应关系需要变更,新的补充,旧的删除。注意SubConn本身和状态都有map维护,且删除操作不在一起。状态只有变为Shutdown才会被清除。

balancer的接口设计分两块:对外提供的api以及需要外部提供的api。对外提供的api封装在Balancer中,需要外部提供的api封装在ClientConnSubConn中。当你设计一个pkg的时候,从pkg的需求出发,定义好访问外部以及外部访问内部的接口,然后就写就行了。这块是我第一次感觉到go interface的强大。pkg的设计可以很任性或者说是内聚,我就这么支持balancer的功能,外部需要实现我的interface,让我调用,同时外部也要根据我提供的interface控制我,或者通知我做些事情。这种类型的pkg都可以参考这种设计思路。

base

该pkg应该是开发人员发现roundrobin中除了picker之外的东西可以公用,或者说picker可以更灵活一点,迁移roundrobin.go的代码到base pkg中,并设计PickerBuilder这个接口规范picker的生成环节。每个go文件都围绕一个或多个核心数据结构打造,这种对上生成的抽象方法,在需要灵活度的地方可以考虑使用。

这次不聊grpclb,之后单独聊,主要看看这个设计是怎么支持外部负载均衡服务的。继续对于ClientConn的解析。

mu    sync.RWMutex
sc    ServiceConfig
scRaw string
conns map[*addrConn]struct{}
// Keepalive parameter can be updated if a GoAway is received.
mkp             keepalive.ClientParameters
curBalancerName string
preBalancerName string // previous balancer name.
curAddresses    []resolver.Address
balancerWrapper *ccBalancerWrapper

sc和scRaw之前讨论过这种设计方法。

conns的类型,推断addrConnClientConn是多对一的包含关系,具体addrConn封装了什么,有什么独特的功能,这就描述下。

// addrConn is a network connection to a given address.
type addrConn struct {

从注视看,该连接对象是对应单独addr的,struct内部包含curAddr字段,就是当前正在使用的地址,但同时也维护了addrs这个地址列表,看样子addrConn不是对应与特定的某个地址,而是在地址列表中选择一个addr进行连接。struct内部主要包含积累东西:

  • 资源类:主要是地址列表,当前地址,地址列表idx,还有transport接口
  • 控制类:cancel方法,dopts配置项,backoffDeadline和connectDeadline两个时间上的限制
  • 统计类:channelzID下面的属性都是用于累计conn工作时的各种数据

该结构体处于clientconn.go的最下面,代码量还挺大,应该算是ClientConn下游实际干活的核心struct了。大体浏览了下addrConn支持的方法,主要是负责transport的重连、异常、状态管理等功能,addr不是该对象的核心,transport是核心,它会做具体的工作,而addrConn负责包装transport,封装一些复杂的操控工作。

transport的具体工作会之后和grpclb一起讨论。调用方的东西还远远没有讨论完成。

ClientConn的上游是call.go,call.go是提供给pb文件使用的,这些部分都会在后续慢慢搞定。

grpc clientconn.go
Share this