引言
继上篇对 Kratos 服务发现核心逻辑及解析器适配的深度拆解后,我们已经成功让 Grpc 拿到了注册中心推送的节点地址。但这仅仅是完成了寻址的第一步。在分布式环境下,拿到一堆 IP 地址数据,真正的挑战在于:这些地址如何变成活的 TCP 连接,面对成百上千个可用节点,Grpc 又是如何根据 Kratos 提供的策略(如 P2C、Random)选出最优路径的?
本篇将正式进入客户端侧的负载均衡。我们将穿透 Kratos 的封装,直达 Grpc 底层源码,深度解析 SubConn 状态机的流转、无界缓冲队列的保序执行,以及 Kratos 如何巧妙地通过 Picker 机制将高级负载均衡算法缝合进 Grpc 的请求链路中。如果你曾好奇过一个 RPC 请求从发起到落地到底经历了多少次筛选与握手,本文将为你揭开这个黑盒。
全局解释器
我们先从 Kratos 注册负载均衡器到 Grpc 看起。
这一层是 Kratos 定义的标准。Builder 负责创建选择器,而 Node 就是对服务节点的抽象(包含 IP、权重、元数据等)。所有想参与负载均衡的节点都得长这样。
go
// 与服务发现一样 也是
type Builder interface {
Build() Selector
}
// Node is node interface.
type Node interface {
// Scheme is service node scheme
Scheme() string
// Address is the unique address under the same service
Address() string
// ServiceName is service name
ServiceName() string
// InitialWeight is the initial value of scheduling weight
// if not set return nil
InitialWeight() *int64
// Version is service node version
Version() string
// Metadata is the kv pair metadata associated with the service instance.
// version,namespace,region,protocol etc..
Metadata() map[string]string
}
这里就是使用 SetGlobalSelector 方法去声明使用什么负载均衡算法, random 或者 p2c 等等, 由用户自己传。
go
var globalSelector = &wrapSelector{} // 全局变量,存储当前使用的 Builder 这个是 pickerBuilder
var _ Builder = (*wrapSelector)(nil)
type wrapSelector struct{ Builder }
// 用该函数得到全局选择器
func GlobalSelector() Builder {
if globalSelector.Builder != nil {
return globalSelector
}
return nil
}
// 设置负载均衡选择器
func SetGlobalSelector(builder Builder) {
globalSelector.Builder = builder
}
必要的一些接口
select 也是一个接口 这样写的好处就是自由度高,扩展性强,缺点就是理解有点麻烦,接口多了就可能会乱
这里体现了 Go 的组合思想。Selector 不仅要能选节点,还要能更新(Apply)节点列表。这种接口设计让扩展性变得极强。
go
type Selector interface {
Rebalancer // Go 的 "组合优于继承" 思想 匿名嵌套接口 代码复用 扩展性强
Select(ctx context.Context) (selected Node, done DoneFunc, err error)
}
type Rebalancer interface {
Apply(nodes []Node)
}
向 GlobalSelector 这里去放 builder
下面这俩对于实现的 pick 不一样, Node 不一样 ,但返回的都是这个 DefaultBuilder
这样无论是 Random 还是 P2C,它们最后返回的 DefaultBuilder 是一种典型的工厂模式,把具体的算法逻辑和节点包装逻辑解耦了,后续看完大家应该就理解了。
go
// random 的话
func NewBuilder() selector.Builder {
return &selector.DefaultBuilder{
Balancer: &Builder{}, // random自己定义的 只要实现pick接口就行
Node: &direct.Builder{},
}
}
// p2c 的话
func NewBuilder() selector.Builder {
return &selector.DefaultBuilder{
Balancer: &Builder{}, // p2c 自己定义的 只要实现pick接口就行
Node: &ewma.Builder{},
}
}
DefaultBuilder 结构体
后面会用到最能绕晕的函数,这里先简单看一下。
Apply 负责把 gRPC 传来的原始节点包装成带权重、带统计信息的高级节点。而 Build 则是真正初始化一个带算法逻辑的选择器。
这里的 Apply() 函数不要看错了,它是 DefaultBuilder.Build() 后 Default 结构体的函数。这里大家有个印象,先知道一下我们注册上去的这种套娃的形式,后续一层一层解开就不会太迷茫。放心后面都会讲一遍。
go
func (d *Default) Apply(nodes []Node) {
weightedNodes := make([]WeightedNode, 0, len(nodes))
for _, n := range nodes {
weightedNodes = append(weightedNodes, d.NodeBuilder.Build(n))
}
d.nodes.Store(weightedNodes)
}
type DefaultBuilder struct {
Node WeightedNodeBuilder
Balancer BalancerBuilder
}
// 重要 !!! 刚开始 InitBuilder 也就是下一个的函数
func (db *DefaultBuilder) Build() Selector {
return &Default{
NodeBuilder: db.Node,
Balancer: db.Balancer.Build(),
}
}
初始化 Builder
当然执行这个的时候需要先调用 SetGlobalSelector 方法 把定义的实例放进去,我放到补充里面了,我们这里用随机举例子,其实本质是一样的,无非是函数调用最后的底层 pick() 函数内容不同而已。
这里的 init 函数本质是在程序启动时,就把名为 selector 的策略注册到了 gRPC 的全局 map 里。这样 gRPC 拨号时只要看到配置里写了 selector,就知道调用自定义的负载均衡。
go
const (
balancerName = "selector"
)
func init() {
b := base.NewBalancerBuilder(
balancerName,
&balancerBuilder{
builder: selector.GlobalSelector(),
},
base.Config{HealthCheck: true},
)
balancer.Register(b)
}
// 补充
// random.NewBuilder() 上面有 返回的是 selector.DefaultBuilder 结构体
selector.SetGlobalSelector(random.NewBuilder())
grpc 的 base 结构
在 init() 函数中,kratos 直接拿来用 base.NewBalancerBuilder(),它的基础结构体已经够用,必要重复造轮子,grpc 也有其他的,扩展的,字段丰富的,但好像用不到就直接使用 base。
go
func NewBalancerBuilder(name string, pb PickerBuilder, config Config) balancer.Builder {
return &baseBuilder{
name: name,
pickerBuilder: pb, // 只把最核心的选择逻辑(PickerBuilder)换成了自己的。
config: config,
}
}
第二层 balancerBuilder (Kratos 侧)
这个 Build 方法是连接 gRPC 和 Kratos 的桥梁。它把 gRPC 那些 TCP 连接(ReadySCs)转换成 Kratos 认识的 Node,然后塞进我们自己的算法里。
后面用到会详细讲。
go
type balancerBuilder struct {
builder selector.Builder
}
// Build creates a grpc Picker.
func (b *balancerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
if len(info.ReadySCs) == 0 {
// Block the RPC until a new picker is available via UpdateState().
return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
}
nodes := make([]selector.Node, 0, len(info.ReadySCs))
for conn, info := range info.ReadySCs {
ins, _ := info.Address.Attributes.Value("rawServiceInstance").(*registry.ServiceInstance)
nodes = append(nodes, &grpcNode{
Node: selector.NewNode("grpc", info.Address.Addr, ins),
subConn: conn,
})
}
p := &balancerPicker{
selector: b.builder.Build(),
}
p.selector.Apply(nodes)
return p
}
balancer.Register() 函数
这里就是 gRPC 全局的注册地,想要实现负载均衡就要调用该函数,本质上就是 gRPC 内部维护了一个全局 Map。
go
func Register(b Builder) {
if strings.ToLower(b.Name()) != b.Name() {
logger.Warningf("Balancer registered with name %q. grpc-go will be switching to case sensitive balancer registries soon", b.Name())
}
// 就是放到全局 map 中 后面用到 Grpc就会从中去取出来
m[strings.ToLower(b.Name())] = b
}
在拨号的时候 用了这个方法 WithDefaultServiceConfig 创建Client 的时候就会解析
go
grpcOpts := []grpc.DialOption{
// 重点!!! options.balancerName: selector
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "` + options.balancerName + `"}`),
grpc.WithChainUnaryInterceptor(ints...),
grpc.WithChainStreamInterceptor(streamInts...),
}
NewClient() 函数 (相关部分)
这里重点介绍负载均衡相关以及后续需要的函数
我建议大家直接看完 parseServiceConfig() 函数之后再回过头看下面
在这主要关注三件事情,第一是解析拨号时我们传入的那段 JSON 配置。第二是启动连接状态管理器(csMgr)。第三是初始化负载均衡包装器(balancerWrapper)。
go
func NewClient(target string, opts ...DialOption) (conn *ClientConn, err error) {
// 解析我们传入的 负载均衡的 builder 最核心
if cc.dopts.defaultServiceConfigRawJSON != nil {
// maxCallAttempts 为 grpc 配置的最大调用重试次数
scpr := parseServiceConfig(*cc.dopts.defaultServiceConfigRawJSON, cc.dopts.maxCallAttempts)
if scpr.Err != nil {
return nil, fmt.Errorf("%s: %v", invalidDefaultServiceConfigErrPrefix, scpr.Err)
}
// 这个是重点 sc 给了 cc.dopts.defaultServiceConfig, sc.lbConfig 是 Grpc 的 baseBuilder
// 大家后续看完上面函数再到这里就明白了
cc.dopts.defaultServiceConfig, _ = scpr.Config.(*ServiceConfig)
}
// 创建连接状态管理器,负责维护 ClientConn 的整体状态(Idle/Connecting/Ready 等 只有 Ready 我们负载均衡才去选
cc.csMgr = newConnectivityStateManager(cc.ctx, cc.channelz)
// 初始化负载均衡选择器包装器
cc.pickerWrapper = newPickerWrapper()
// 管理自研Balancer的创建 / 销毁,接收解析器推送的节点列表,监听节点连接状态(如某个节点断连则标记不可用);
/*
cc.resolverWrapper = newCCResolverWrapper(cc)
cc.balancerWrapper = newCCBalancerWrapper(cc)
cc.firstResolveEvent = grpcsync.NewEvent()
*/
cc.initIdleStateLocked()
}
parseServiceConfig() 核心函数
该函数就是用于解析去 map 中拿,如果解析失败或者没填,它就会回退到默认的策略(轮询)。
这里只要记住 sc.lbConfig = cfg 这句代码就可以了,cfg 是下面函数返回值 &lbConfig{childBuilder: builder},提醒一下这里的 builder 是 Grpc 的 baseBuilder。
go
func parseServiceConfig(js string, maxAttempts int) *serviceconfig.ParseResult {
if len(js) == 0 { // 用于以防万一 我们是 {"loadBalancingPolicy":"selector"}
return &serviceconfig.ParseResult{Err: fmt.Errorf("no JSON service config provided")}
}
var rsc jsonSC // 这是个结构体 接收 我们的{"loadBalancingPolicy":"selector"}
err := json.Unmarshal([]byte(js), &rsc)
if err != nil {
logger.Warningf("grpc: unmarshalling service config %s: %v", js, err)
return &serviceconfig.ParseResult{Err: err}
}
// rsc.LoadBalancingPolicy 设为指向字符串 "selector" 的指针,其他字段均为 nil
// 你可以理解成 sc 有个 lbConfig 字段是我们重视的 这些都是扩展
sc := ServiceConfig{
Methods: make(map[string]MethodConfig), // 空
retryThrottling: rsc.RetryThrottling, // 空
healthCheckConfig: rsc.HealthCheckConfig, // 空
rawJSONString: js, // 原生的 留着当日志
}
// 这个没配置 这东西就是扩展
c := rsc.LoadBalancingConfig
// 这里没填 grpc就解析不了 下面就是把我们传入的变成grpc能够解析的
if c == nil {
// 设置一个 grpc 默认的 保底 下面就变成我们的 selector
name := pickfirst.Name
if rsc.LoadBalancingPolicy != nil {
name = *rsc.LoadBalancingPolicy // 覆盖为自己配置的 selector
}
// 校验策略是否已注册 核心! init注册的 这里防止没注册 但这里传来了 自定义 name
if balancer.Get(name) == nil { // 这个我们注册了 init函数
name = pickfirst.Name // 未注册则回退到默认策略
}
// 构建gRPC标准的LB配置格式
cfg := []map[string]any{{name: struct{}{}}} // 格式:[{"selector": {}}]
strCfg, err := json.Marshal(cfg)
if err != nil {
return &serviceconfig.ParseResult{Err: fmt.Errorf("unexpected error marshaling simple LB config: %w", err)}
}
r := json.RawMessage(strCfg) // 由于刚开始c为空 我们要构建c的类型
c = &r // 将构建好的LB配置赋值给c
}
// 解析LB配置为gRPC可识别的格式 重点方法 ParseConfig
cfg, err := gracefulswitch.ParseConfig(*c)
if err != nil {
return &serviceconfig.ParseResult{Err: err}
}
sc.lbConfig = cfg // 将解析后的LB配置存入ServiceConfig cfg 里面有我自己的 builder 负载均衡的
if rsc.MethodConfig == nil {
return &serviceconfig.ParseResult{Config: &sc}
}
/*
处理 MethodConfig 的详细逻辑,包括重试、超时、消息大小限制等 我们不走 以后再看
针对特定 RPC 方法的个性化配置(比如 {"methodConfig": [{"name": [{"service": "xxx", "method": "yyy"}], "timeout": "1s"}]})。
*/
paths := map[string]struct{}{}
for _, m := range *rsc.MethodConfig {
if m.Name == nil {
continue
}
mc := MethodConfig{
WaitForReady: m.WaitForReady,
Timeout: (*time.Duration)(m.Timeout),
}
if mc.RetryPolicy, err = convertRetryPolicy(m.RetryPolicy, maxAttempts); err != nil {
logger.Warningf("grpc: unmarshalling service config %s: %v", js, err)
return &serviceconfig.ParseResult{Err: err}
}
if m.MaxRequestMessageBytes != nil {
if *m.MaxRequestMessageBytes > int64(maxInt) {
mc.MaxReqSize = newInt(maxInt)
} else {
mc.MaxReqSize = newInt(int(*m.MaxRequestMessageBytes))
}
}
if m.MaxResponseMessageBytes != nil {
if *m.MaxResponseMessageBytes > int64(maxInt) {
mc.MaxRespSize = newInt(maxInt)
} else {
mc.MaxRespSize = newInt(int(*m.MaxResponseMessageBytes))
}
}
for i, n := range *m.Name {
path, err := n.generatePath()
if err != nil {
logger.Warningf("grpc: error unmarshalling service config %s due to methodConfig[%d]: %v", js, i, err)
return &serviceconfig.ParseResult{Err: err}
}
if _, ok := paths[path]; ok {
err = errDuplicatedName
logger.Warningf("grpc: error unmarshalling service config %s due to methodConfig[%d]: %v", js, i, err)
return &serviceconfig.ParseResult{Err: err}
}
paths[path] = struct{}{}
sc.Methods[path] = mc
}
}
// 重试限流(RetryThrottling)配置校验
if sc.retryThrottling != nil {
if mt := sc.retryThrottling.MaxTokens; mt <= 0 || mt > 1000 {
return &serviceconfig.ParseResult{Err: fmt.Errorf("invalid retry throttling config: maxTokens (%v) out of range (0, 1000]", mt)}
}
if tr := sc.retryThrottling.TokenRatio; tr <= 0 {
return &serviceconfig.ParseResult{Err: fmt.Errorf("invalid retry throttling config: tokenRatio (%v) may not be negative", tr)}
}
}
return &serviceconfig.ParseResult{Config: &sc}
}
gracefulswitch.ParseConfig() 重点函数
本质只有一点,就是拿到我们 init() 传入的 builder,其他的先不用了解。提醒一下这里的 builder 是 Grpc 的 baseBuilder
go
// 入参cfg:对于我们来说是 json.RawMessage 类型,值为 `[{"selector":{}}]` 的字节流
func ParseConfig(cfg json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
// 反序列化LB配置为 []map[string]json.RawMessage
var lbCfg []map[string]json.RawMessage
if err := json.Unmarshal(cfg, &lbCfg); err != nil {
return nil, err
}
// 遍历LB配置数组(对于现在来说 只有1个元素)
for i, e := range lbCfg {
// 校验每个配置项只能有1个策略名 gRPC 强制要求
if len(e) != 1 {
return nil, fmt.Errorf("expected a JSON struct with one entry; received entry %v at index %d", e, i)
}
// 提取策略名和对应的配置 map只有1个键值对,循环一次就拿到
var name string
var jsonCfg json.RawMessage
for name, jsonCfg = range e {
}
// 重点 从注册中心获取你注册的selector构建器
builder := balancer.Get(name)
if builder == nil {
// 未注册则跳过
continue
}
// 检查构建器是否需要解析专属配置 这里是看看需要扩展么 我们不需要
parser, ok := builder.(balancer.ConfigParser)
if !ok {
// kratos 没实现该接口,直接返回包含构建器的lbConfig 我们直接返回
return &lbConfig{childBuilder: builder}, nil
}
// 解析策略专属配置 没实现这个 也不需要额外配置
cfg, err := parser.ParseConfig(jsonCfg)
if err != nil {
return nil, fmt.Errorf("error parsing config for policy %q: %v", name, err)
}
return &lbConfig{childBuilder: builder, childConfig: cfg}, nil
}
// 遍历完没找到支持的策略,返回错误
return nil, fmt.Errorf("no supported policies found in config: %v", string(cfg))
}
newConnectivityStateManager() 函数
go
func newConnectivityStateManager(ctx context.Context, channel *channelz.Channel) *connectivityStateManager {
return &connectivityStateManager{
// gRPC 的监控/调试组件
channelz: channel,
// ★ 这个重要:发布-订阅模式的实现
pubSub: grpcsync.NewPubSub(ctx),
// 用于在状态变化时通知订阅者
}
}
NewPubSub() 函数
这里主要讲一下 Unbounded 这个结构体,chan 如果满了,发送方会阻塞,但是这里使用 chan + slice 的方式实现了一个永远不会阻塞发送方 的队列,精髓就是 Load() 函数取数据,在这里大致说一下原理要不然会很懵,当数据来的时候,比如第一个,它会直接放到 chan 中,以后来的都放到 backlog 后备队列中,重点是取,取是用 Get() 函数,该函数只从 chan 中取,一定会有,只要放进去过,每次执行完 chan,就一定会执行 load() 函数,会把队列里的放到 chan 中, 所以 load() 函数只是起到了从队列中放到 chan 的作用。
go
func NewPubSub(ctx context.Context) *PubSub {
return &PubSub{
cs: NewCallbackSerializer(ctx),
// subscribers 是订阅者集合 map[Subscriber]bool 用 map 来存储订阅者
subscribers: map[Subscriber]bool{},
}
}
func NewCallbackSerializer(ctx context.Context) *CallbackSerializer {
cs := &CallbackSerializer{
done: make(chan struct{}), // 用于通知 已经结束了
callbacks: buffer.NewUnbounded(), // 无界缓冲队列,用于存放待执行的回调函数
}
go cs.run(ctx) // 启动一个后台 goroutine,它会一直运行,等待队列里有回调就执行
return cs
}
// 对于返回的 callbacks 里面字段以及 方法
type Unbounded struct {
c chan any // 实际读取数据的通道,固定容量为 1
closed bool // 标记通道 c 是否已经执行了 close()
closing bool // 标记是否正在关闭 不再接受新数据
mu sync.Mutex // 互斥锁,保护 backlog 和状态位
backlog []any // 核心 后备队列,当 c 满时,数据暂存在这里
}
// Put 往缓冲区放数据
func (b *Unbounded) Put(t any) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.closing {
return errBufferClosed
}
// 如果后备队列里没东西,尝试直接塞进通道 c
if len(b.backlog) == 0 {
select {
case b.c <- t:
// 情况 A:通道 c 是空的,直接塞进去成功,发送方完全不阻塞。
return nil
default:
// 情况 B:通道 c 满了 里面已经有一个元素没被取走,进入下面的逻辑。
}
}
// 如果 c 满了,或者 backlog 已经有数据了,为了保证 FIFO
// 必须把新数据追加到 slice 后面。这永远不会阻塞调用者。
b.backlog = append(b.backlog, t)
return nil
}
// Get: 返回一个 channel,从这个 channel 可以取出数据
func (b *Unbounded) Get() <-chan any {
return b.c
}
// Load 精髓 取数据
func (b *Unbounded) Load() {
b.mu.Lock()
defer b.mu.Unlock()
if len(b.backlog) > 0 {
// 如果后备队列里有数据,把最前面的元素(index 0)取出来
select {
case b.c <- b.backlog[0]:
// 只有在通道 c 有空位(读取者取走了一个)时,这里才会成功
b.backlog[0] = nil // 避免内存泄漏
b.backlog = b.backlog[1:]
default:
// 如果 c 还是满的,什么也不做
}
} else if b.closing && !b.closed {
// 如果后备队列清空了,且处于正在关闭状态,则彻底关闭通道 c
b.closed = true
close(b.c)
}
}
// Close: 关闭队列
func (b *Unbounded) Close() { ... }
go cs.run() 开启协程的方法
这个会一直阻塞 等待后面我们放入 , 后面会放入,这里大家要明白这是干啥的,本质就是一个单线程串行执行器,有任务放上来就运行。
go
func (cs *CallbackSerializer) run(ctx context.Context) {
// 外部可以通过 <-cs.done 知道这个 goroutine 结束了
defer close(cs.done)
// 作用: 上层ctx 关闭时,这里也关闭,并且让下面的 for 循环退出
context.AfterFunc(ctx, cs.callbacks.Close)
// ★★★ 核心循环 ★★★
// cs.callbacks.Get() 返回一个 channel
// for range 会一直阻塞等待,直到 channel 关闭
for cb := range cs.callbacks.Get() { // cb 是从队列里取出的一个元素
cs.callbacks.Load() // 告诉队列已经取走了一个元素 把队列的放到 chan 中
// ★★★ 关键断言 ★★★
cb.(func(context.Context))(ctx)
// (func(context.Context)) 类型断言 必然成功因为后续我们放就是这样的函数 (ctx)进行传参调用
}
}
// 这个方法 是放入队列,不管顺序 以及 对错 主要应用于 cc.csMgr 这个的 打断上面循环阻塞用的
func (cs *CallbackSerializer) TrySchedule(f func(ctx context.Context)) {
cs.callbacks.Put(f)
}
// 这个方法 专门 用于 下面保证顺序性的
func (cs *CallbackSerializer) ScheduleOr(f func(ctx context.Context), onFailure func()) {
if cs.callbacks.Put(f) != nil {
onFailure()
}
}
小分支总结 初始化状态 cc.csMg r
我们上面看的函数,其实就是生成了下面的东西
go
cc.csMgr = connectivityStateManager {
state: Idle (0)
notifyChan: nil
channelz: ...
pubSub: PubSub {
msg: nil
subscribers: {} // 空 map,没有订阅者
cs: CallbackSerializer {
done: chan struct{} // (未关闭)
callbacks: Unbounded // (空队列)
}
}
}
newPickerWrapper() 函数
我们继续回到 NewClient() 函数,继续完成负载均衡需要的初始化实例,这里等到用的时候再回过头来细看也可以
可以想象成一个占位符。请求进来时如果还没选好节点,就先让它在 blockingCh 上等着。一旦 Picker 准备好了,直接关闭 channel,瞬间唤醒成千上万个等待的请求。 下面有它的结构体
go
func newPickerWrapper() *pickerWrapper {
pw := &pickerWrapper{} //初始化
// 调用原子指针的Store方法,原子安全地把一个新的 pickerGeneration 实例写入 pickerGen
pw.pickerGen.Store(&pickerGeneration{
// picker 设置为 nil 现在还没有 等到时候会调用 pickerWrapper 的方法 来 填充
blockingCh: make(chan struct{}), // 呼唤器:唤醒所有等它的 goroutine
})
return pw
}
type pickerGeneration struct {
picker balancer.Picker
// 当有新的Picker可用时,这个通道会被关闭,用来唤醒所有等待这个通道的 goroutine
// 从关闭的通道读取会立即返回,不会阻塞
blockingCh chan struct{}
}
type pickerWrapper struct {
// 原子指针类型
pickerGen atomic.Pointer[pickerGeneration] //泛型 这个指针 必须是 pickerGeneration 类型
}
newCCBalancerWrapper() 函数
这一层层包装,其实是为了平滑切换。比如你运行过程中想把随机算法换成轮询,这些包装层能保证老请求走老路,新请求走新路,切换过程不报错,支持动态修改。
go
func newCCBalancerWrapper(cc *ClientConn) *ccBalancerWrapper {
ctx, cancel := context.WithCancel(cc.ctx)
ccb := &ccBalancerWrapper{
cc: cc, // 主conn
opts: balancer.BuildOptions{ // 默认以及我们自己的配置
DialCreds: cc.dopts.copts.TransportCredentials,
CredsBundle: cc.dopts.copts.CredsBundle,
Dialer: cc.dopts.copts.Dialer,
Authority: cc.authority,
CustomUserAgent: cc.dopts.copts.UserAgent,
ChannelzParent: cc.channelz,
Target: cc.parsedTarget,
},
// 重要 与上面的 cc.csMgr 里面的pubsub创建的一样,都是队列用于串行执行的
serializer: grpcsync.NewCallbackSerializer(ctx),
serializerCancel: cancel,
}
// 后续会用到
ccb.balancer = gracefulswitch.NewBalancer(ccb, ccb.opts)
return ccb
}
// 补充一下 让大家明白
func NewCallbackSerializer(ctx context.Context) *CallbackSerializer {
cs := &CallbackSerializer{
done: make(chan struct{}), // 用于通知"我已经结束了
callbacks: buffer.NewUnbounded(), // 无界缓冲队列,用于存放待执行的回调函数
}
go cs.run(ctx) // 启动一个后台 goroutine,它会一直运行,等待队列里有回调就执行
return cs
}
NewBalancer() 函数
go
func NewBalancer(cc balancer.ClientConn, opts balancer.BuildOptions) *Balancer {
// gRPC 用来支持运行时平滑切换负载均衡策略的代理层。
return &Balancer{
cc: cc, // 直接把 ccBalancerWrapper 里面有父挂件 ClientConn 包装进来
bOpts: opts,
}
}
初始化工作全部完成,现在进入主逻辑
DialContext() 函数 (相关部分)
这里是告诉大家后续还需要讲那些函数,我们现在进行完成负载均衡相关的初始化,就可以继续回到 Grpc 内部 cc.UpdateState(resolver.State{Addresses: addrs}) 函数中。
提示一下,拨号不是瞬间完成的。它会启动一个 for 循环,只要状态不是 Ready,它就在那等。直到状态管理器检测到有 Ready,它才能用,默认是懒加载,也就是先返回不阻塞,等到真正来请求再等待,如果初始化早的话,等到来请求其实就已经好了。
go
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
// 这个重点函数 我们在服务发现中走了
if err := cc.idlenessMgr.ExitIdleMode(); err != nil {
return nil, err
}
if !cc.dopts.block {
return cc, nil // 默认走这里,直接返回
}
// 如果使用阻塞返回的话 也就是 只要返回了 就可以用了 上面是 先给你 等你调用的时候 再去查看 其实是一样的
for {
s := cc.GetState()
/*
s := cc.GetState():获取当前 ClientConn 的连接状态
其实就是 gRPC 定义的 connectivity.State 枚举,状态很重要,务必知道 Idle 和 Ready 贯穿核心路线
Idle:空闲(未尝试连接);
Connecting:正在连接;
Ready:就绪(可发起 RPC);
TransientFailure:临时失败(可重试);
Shutdown:已关闭。
*/
if s == connectivity.Idle {
//走这个 这个函数是
cc.Connect()
}
if s == connectivity.Ready {
return cc, nil
} else if cc.dopts.copts.FailOnNonTempDialError && s == connectivity.TransientFailure {
if err = cc.connectionError(); err != nil {
terr, ok := err.(interface {
Temporary() bool
})
if ok && !terr.Temporary() {
return nil, err
}
}
}
if !cc.WaitForStateChange(ctx, s) { //如果 为True 说明 Ready了 就直接 重新 s := cc.GetState() 返回了
// ctx got timeout or canceled.
if err = cc.connectionError(); err != nil && cc.dopts.returnLastError {
return nil, err
}
return nil, ctx.Err()
}
}
// 这里写一下 WaitForStateChange 这个函数 很重要 是阻塞用的
func (cc *ClientConn) WaitForStateChange(ctx context.Context, sourceState connectivity.State) bool {
ch := cc.csMgr.getNotifyChan() //等这个 channel 发通知
if cc.csMgr.getState() != sourceState { //这个传来的 一般会等于 除非 刚进来 然后协程TCP连接好 很极端
return true
}
select {
case <-ctx.Done():
return false
case <-ch:
return true
}
}
}
UpdateState() 函数
接着上篇文章,拿到服务后,我们回到 Grpc 的逻辑,因为我们当时从 ccResolverWrapper.start() 函数中调用的我们传入的 builder.Build() ,传入的 cc 就是 ccResolverWrapper
这里的参数 s,里面有我们的实例,转换成 Grpc 能接收的形式,下面的重点是进入到后面函数中
go
func (ccr *ccResolverWrapper) UpdateState(s resolver.State) error {
ccr.cc.mu.Lock()
ccr.mu.Lock()
if ccr.closed {
ccr.mu.Unlock()
ccr.cc.mu.Unlock()
return nil
}
// 兼容处理
if s.Endpoints == nil {
s.Endpoints = addressesToEndpoints(s.Addresses)
}
ccr.addChannelzTraceEvent(s)
ccr.curState = s
ccr.mu.Unlock()
// 把解析器状态转发给 ClientConn 的核心处理函数 解锁在函数里面 ccr.cc.mu.Lock()
return ccr.cc.updateResolverStateAndUnlock(s, nil)
}
updateResolverStateAndUnlock() 函数
这个函数会把地址列表和我们自己的 Builder 结合起来,然后扔给负载均衡包装器去处理。它是从找地址到建连接的转折点。
这个函数核心是拿到 cc.sc.lbConfig,再提醒一下 builder 是 Grpc 的 baseBuilder。
go
func (cc *ClientConn) updateResolverStateAndUnlock(s resolver.State, err error) error {
defer cc.firstResolveEvent.Fire()
// 如果 ClientConn 已关闭,直接返回
if cc.conns == nil {
cc.mu.Unlock()
return nil
}
// 分支1:解析器返回错误 比如注册中心宕机
if err != nil {
// 应用默认服务配置
cc.maybeApplyDefaultServiceConfig()
// 通知负载均衡器:解析器出错,触发失败逻辑
cc.balancerWrapper.resolverError(err)
// 无有效地址,返回错误
cc.mu.Unlock()
return balancer.ErrBadResolverState
}
var ret error
// 禁用服务配置 先跳过 我们也不走这条
if cc.dopts.disableServiceConfig {
channelz.Infof(logger, cc.channelz, "ignoring service config from resolver (%v) and applying the default because service config is disabled", s.ServiceConfig)
cc.maybeApplyDefaultServiceConfig()
// 分支3:resolver 只推了地址列表,没带 ServiceConfig 我们这个就是
} else if s.ServiceConfig == nil {
cc.maybeApplyDefaultServiceConfig()
} else {
/*
如果传入了下面这些就走分支4 可以动态指定负载均衡策略 可以热更新 先跳过
r.cc.UpdateState(resolver.State{
Addresses: addrs,
ServiceConfig: &serviceconfig.ParseResult{
Config: &ServiceConfig{
lbConfig: ..., // 动态指定负载均衡策略
Methods: ..., // 每个方法的超时、重试配置
},
},
})
*/
// 校验服务配置是否合法
if sc, ok := s.ServiceConfig.Config.(*ServiceConfig); s.ServiceConfig.Err == nil && ok {
// 获取服务配置选择器
configSelector := iresolver.GetConfigSelector(s)
if configSelector != nil {
if len(s.ServiceConfig.Config.(*ServiceConfig).Methods) != 0 {
channelz.Infof(logger, cc.channelz, "method configs in service config will be ignored due to presence of config selector")
}
} else {
configSelector = &defaultConfigSelector{sc}
}
cc.applyServiceConfigAndBalancer(sc, configSelector)
} else {
ret = balancer.ErrBadResolverState
if cc.sc == nil {
cc.applyFailingLBLocked(s.ServiceConfig)
cc.mu.Unlock()
return ret
}
}
}
// 请先看完下面函数 然后返回后再来看这里
// 核心 我的负载均衡 Builder 就是sc.lbConfig
balCfg := cc.sc.lbConfig
// 拿到 NewClient 里初始化的 balancerWrapper(负载均衡器包装器)
bw := cc.balancerWrapper
// 前面的 cc.mu.Lock 在这里释放
cc.mu.Unlock()
// 通知 balancerWrapper 更新客户端连接状态
uccsErr := bw.updateClientConnState(&balancer.ClientConnState{
ResolverState: s, // 是所有的地址列表 有serviceInstance 转成 grpc喜欢的地址列表
BalancerConfig: balCfg, // 负载均衡器 里面有builder
})
if ret == nil {
ret = uccsErr // 优先返回解析器相关错误
}
return ret
}
maybeApplyDefaultServiceConfig() 函数
中间层,Grpc 对于服务发现和负载均衡有非常多的中间层,所以绕来绕去很麻烦,但有利于后期扩展,这里只用于判断是用自定义的解析器还是默认的,真正逻辑取出在下一个函数中。
go
func (cc *ClientConn) maybeApplyDefaultServiceConfig() {
if cc.sc != nil {
// 第二次 进入 比如注册中心节点变化 直接用已有的 cc.sc,不需要重新解析默认配置
cc.applyServiceConfigAndBalancer(cc.sc, nil)
return
}
// 这个就是我们初始化解析的负载均衡赋的值
if cc.dopts.defaultServiceConfig != nil {
cc.applyServiceConfigAndBalancer(cc.dopts.defaultServiceConfig, &defaultConfigSelector{cc.dopts.defaultServiceConfig})
} else {
// 这里就是用 grpc内部的负载均衡 我们用自己的 也就是上面的 其实他们调用的都是一个函数
cc.applyServiceConfigAndBalancer(emptyServiceConfig, &defaultConfigSelector{emptyServiceConfig})
}
}
applyServiceConfigAndBalancer() 函数
这里的参数 sc,是我们在 NewClient() 函数中解析我们传入的负载均衡 builder,返回了 cc.sc.lbConfig,赋值给 cc.dopts.defaultServiceConfig ,sc 就是它,这里赋值给 cc.sc 本质上就是 cc.dopts.defaultServiceConfig,这里对于我们来说,没有扩展,就做了这一件事。
go
func (cc *ClientConn) applyServiceConfigAndBalancer(sc *ServiceConfig, configSelector iresolver.ConfigSelector) {
// 防御性检查
if sc == nil {
return
}
// 保存服务配置到 ClientConn
cc.sc = sc
// 更新配置选择器 跳过
if configSelector != nil {
cc.safeConfigSelector.UpdateConfigSelector(configSelector)
}
// 重试限流器配置 跳过
// 如果配置了重试限流,就创建 throttler
if cc.sc.retryThrottling != nil {
newThrottler := &retryThrottler{
tokens: cc.sc.retryThrottling.MaxTokens,
max: cc.sc.retryThrottling.MaxTokens,
thresh: cc.sc.retryThrottling.MaxTokens / 2,
ratio: cc.sc.retryThrottling.TokenRatio,
}
cc.retryThrottler.Store(newThrottler)
} else {
cc.retryThrottler.Store((*retryThrottler)(nil))
}
}
updateClientConnState() 函数
知识补充,定义一个闭包时,Go 会把它需要的外部变量一起打包保存起来: ccb 和 ccs 等。
这里的函数是: func(ctx context.Context) 还记得我们上面介绍的串行无阻塞队列么,就是用来执行这里函数。
这里补充一下怕遗忘,我们当时解析出来的: &lbConfig{childBuilder: builder, childConfig: cfg}。
builder 就是我们注册的。
go
func (ccb *ccBalancerWrapper) updateClientConnState(ccs *balancer.ClientConnState) error {
// 创建 channel 用于同步等待结果 因为下面的逻辑是异步执行的,需要阻塞等待 闭包里面运行完就解开
errCh := make(chan error)
// 定义真正的更新逻辑(闭包)
uccs := func(ctx context.Context) {
// 函数结束时关闭 channel 放开阻塞
defer close(errCh)
// 防御检查
if ctx.Err() != nil || ccb.balancer == nil {
return
}
// 从配置中取出负载均衡策略名 只是用于判断与之前的算法一不一样 用于动态负载均衡 跳过
// ccs.BalancerConfig 是 lbConfig
// gracefulswitch.ChildName() 从中提取策略名(如 "selector")
name := gracefulswitch.ChildName(ccs.BalancerConfig)
// 策略名变了 打日志 服务动态负载均衡的 我们先不了解动态修改
// 比如从 "round_robin" 切换到 "selector"
if ccb.curBalancerName != name {
ccb.curBalancerName = name
channelz.Infof(logger, ccb.cc.channelz, "Channel switches to new LB policy %q", name)
}
// ★ 调用 balancer 的 UpdateClientConnState ★
err := ccb.balancer.UpdateClientConnState(*ccs)
if logger.V(2) && err != nil {
logger.Infof("error from balancer.UpdateClientConnState: %v", err)
}
// 把结果发回 channel
errCh <- err
}
// 失败时的回调(只是关闭 channel)
onFailure := func() { close(errCh) }
// 进行调度 就是之前讲解的串行队列
ccb.serializer.ScheduleOr(uccs, onFailure)
// 阻塞等待结果
return <-errCh
}
ScheduleOr() 函数
Put() 函数我给大家介绍过,以及 Get() 和 Load(),共同组成了串行的函数调用队列。
go
func (cs *CallbackSerializer) ScheduleOr(f func(ctx context.Context), onFailure func()) {
if cs.callbacks.Put(f) != nil {
//put 有锁 两个 协程会阻塞
onFailure()
}
}
balancer.UpdateClientConnState() 函数
我们先只考虑静态负载均衡,重要的是先过一遍捋清楚,再去了解动态负载均衡以及配置
这里给大家捋一下逻辑:
首先:调用 parseJsonConfig 得到 首先返回 &lbConfig{childBuilder: builder} 赋值给 sc.lbConfig ,所以 sc 是 ServiceConfig{} ,然后 把 sc 赋值给 cc.dopts.defaultServiceConfig。 所以我们的 builder 是在 cc.dopts.defaultServiceConfig.lbConfig.childBuilder 函数传过来的参数 state 是:
go
&balancer.ClientConnState{
ResolverState: s, // 是所有的地址列表 有serviceInstance 转成 grpc喜欢的地址列表
BalancerConfig: balCfg, // 负载均衡配置器lbConfig
}
这样大家在看下面函数会清晰一点
go
func (gsb *Balancer) UpdateClientConnState(state balancer.ClientConnState) error {
// 第一次返回 nil
balToUpdate := gsb.latestBalancer()
gsbCfg, ok := state.BalancerConfig.(*lbConfig)
if ok {
// 首次调用 balToUpdate 为空 或者 策略名不一样 需要从 A 策略切换到 B 策略
if balToUpdate == nil || gsbCfg.childBuilder.Name() != balToUpdate.builder.Name() {
var err error
balToUpdate, err = gsb.switchTo(gsbCfg.childBuilder)
if err != nil {
return fmt.Errorf("could not switch to new child balancer: %w", err)
}
}
// 通用容器 替换成 子策略专属配置
state.BalancerConfig = gsbCfg.childConfig
}
if balToUpdate == nil {
return errBalancerClosed
}
// balToUpdate 就是 bw 刚创建的 不是cc.balancerWrapper
// 但是balancerWrapper类型没有这个方法 由于嵌入了 bw.Balancer = newBalancer
// bw.Balancer这个是匿名字段 解析器就会执行balToUpdate.Balancer.UpdateClientConnState 方法
return balToUpdate.UpdateClientConnState(state)
}
latestBalancer() 函数
首次调用 都为 nil 啥都没赋值,直接返回 nil, 这个函数是给动态负载均衡用的,先不用了解。
go
func (gsb *Balancer) latestBalancer() *balancerWrapper {
gsb.mu.Lock()
defer gsb.mu.Unlock()
if gsb.balancerPending != nil {
return gsb.balancerPending
}
return gsb.balancerCurrent
}
switchTo() 函数
这个函数是核心,真正调用了我们的负载均衡 builder.Build() 函数,创建了 balancerWrapper 实例,要区别 ccbalancerWrapper ,我在这叫它 真正的 balancerWrapper ,它内部字段 Picker 和 subconns 是负载均衡的核心,一个是选哪一个,一个是所有的连接。
go
func (gsb *Balancer) switchTo(builder balancer.Builder) (*balancerWrapper, error) {
// 加锁
gsb.mu.Lock()
// 检查是否已关闭
if gsb.closed {
gsb.mu.Unlock()
return nil, errBalancerClosed
}
// ★★★ 创建真正的 balancerWrapper★★★
bw := &balancerWrapper{
ClientConn: gsb.cc, // ClientConn 包装器 本质上是 ccBalancerWrapper
builder: builder, // 我们注册的 Builder
gsb: gsb, // 指向父级 也就是 Balancer
lastState: balancer.State{
// 初始状态:Connecting + 返回错误的 Picker 因为还没有执行我们的 Builder
ConnectivityState: connectivity.Connecting,
Picker: base.NewErrPicker(balancer.ErrNoSubConnAvailable),
},
subconns: make(map[balancer.SubConn]bool), // SubConn 集合 这个后续就用了 真正的连接
}
// 保存旧的 pending 跳过
balToClose := gsb.balancerPending
// 这就是动态负载均衡要用的 这次名字不一样那就把当前的改为这次的 我们第一次直接设置 以后就不变了
if gsb.balancerCurrent == nil {
// 第一次调用直接设为 current
gsb.balancerCurrent = bw
} else {
// 已有 current:设为 pending(等待切换)
gsb.balancerPending = bw
}
gsb.mu.Unlock()
balToClose.Close()
// ★★★ 调用 builder.Build 创建真正的子 balancer ★★★
newBalancer := builder.Build(bw, gsb.bOpts)
// 防御检查
if newBalancer == nil {
gsb.mu.Lock()
if gsb.balancerPending != nil {
gsb.balancerPending = nil
} else {
gsb.balancerCurrent = nil
}
gsb.mu.Unlock()
return nil, balancer.ErrBadResolverState
}
// 把 builder.Build 返回的 baseBalancer 挂到 真正的 balancerWrapper
bw.Balancer = newBalancer
return bw, nil
}
关于balancerWrapper的层级关系
csharp
CCBalancerWrapper 内部有个 balancer: 这个 balancer 调用的 switchTo 方法 生成一个真正的 balancerWrapper 负载均衡包装器 把这个挂载到 balancerCurrent 字段上 然后真正的 balancerWrapper 里面有我们自己定义最重要的pickerBuilder
CCBalancerWrapper{
balancer {
balancerCurrent: 真正balancerWrapper{
我们用的base 的 builder 执行 build 后的 baseBalancer {
pickerBuilder (大家注意我们注册的init函数的话这个 pickerBuilder 就是 kratos 的 balancerBuilder)
}
}
}
}
builder.Build() 函数
因为 kratos 用的 grpc 的基础Balancer 所以调用也是基础的 Build(),这里记住两件事,初始化存储地址到底层连接的映射的 map,还有初始化记录每个连接的状态的 map,这个时候依旧没执行 kratos 内部写的 picker,所以这里初始化依旧错误的 picker, 说明这个时候还不能进行负载均衡。
go
func (bb *baseBuilder) Build(cc balancer.ClientConn, _ balancer.BuildOptions) balancer.Balancer {
bal := &baseBalancer{
// cc 刚创建的 balancerWrapper
cc: cc,
pickerBuilder: bb.pickerBuilder, // 这个是重点 是我们自己写的 balancerBuilder
// 地址 → SubConn 的映射 核心 这里只是初始化 到时候填充
// key: resolver.Address 也就是我们 serviceInstance 变成grpc喜欢的
// value: balancer.SubConn 底层 TCP 连接 每一个连接
subConns: resolver.NewAddressMapV2[balancer.SubConn](),
// 记录每个 SubConn 当前是 Idle/Connecting/Ready/TransientFailure
scStates: make(map[balancer.SubConn]connectivity.State),
// 状态评估器 比如:有一个 Ready 就整体 Ready,全部 Failure 就整体 Failure
csEvltr: &balancer.ConnectivityStateEvaluator{},
// 配置 跳过
config: bb.config
// 这是 baseBalancer 自己的状态,初始是 Connecting
state: connectivity.Connecting,
}
// 初始化 picker 在没有 Ready 的 SubConn 之前,返回错误
bal.picker = NewErrPicker(balancer.ErrNoSubConnAvailable)
return bal
}
balToUpdate.UpdateClientConnState() 函数
这里的逻辑是 Balancer.UpdateClientConnState() 函数第一次进入返回nil,我们初始化 真正的 balancerWrapper,赋值给 balToUpdate ,然后它字段里有 Balancer,也就是我们刚才生成的 baseBalancer,按道理应该执行的是 balancerWrapper.UpdateClientConnState() 函数,但这个函数没有,而它的字段的 baseBalancer 有,所以我们执行的是它的。
这个函数会遍历所有的 IP 地址。发现新 IP会立刻创建 SubConn 并执行 Connect()。它是真正的行动的地方。
go
func (b *baseBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
// 清除之前的错误
b.resolverErr = nil
// 创建一个临时 Set,用于快速查找哪些地址是新推送的
addrsSet := resolver.NewAddressMapV2[any]()
// addrsSet 现在是空的 {}
// 重点 遍历新地址列表 这里是我们服务发现里拿到的地址 这里才真正用到的
for _, a := range s.ResolverState.Addresses {
// 把地址加入 Set
addrsSet.Set(a, nil)
// 举例 addrsSet = {
// "10.216.150.34:8020": nil,
// "192.168.1.101:8080": nil,
// "192.168.1.102:8080": nil,
// }
// 检查这个地址是否已经有 SubConn
if _, ok := b.subConns.Get(a); !ok {
// 第一次调用,b.subConns 是空的,所以每个地址都走这里
// 创建 SubConn 选项
var sc balancer.SubConn
opts := balancer.NewSubConnOptions{
HealthCheckEnabled: b.config.HealthCheck, // 是否健康检查
// 当 SubConn 状态变化时,gRPC 会调用这个函数 核心 必须记住这个后面会用
StateListener: func(scs balancer.SubConnState) {
b.updateSubConnState(sc, scs)
},
}
// ★★★ 创建 SubConn ★★★
//sc 是 acBalancerWrapper里面字段 addrConn,这个才是真正底层
sc, err := b.cc.NewSubConn([]resolver.Address{a}, opts)
if err != nil {
continue
}
// 保存到 subConns map
b.subConns.Set(a, sc)
// b.subConns = {
// "10.216.150.34:8020": SubConn1,
// "192.168.1.101:8080": SubConn2,
// "192.168.1.102:8080": SubConn3,
// }
// 初始化状态为 Idle
b.scStates[sc] = connectivity.Idle
// b.scStates = {
// SubConn1: Idle,
// SubConn2: Idle,
// SubConn3: Idle,
// }
// csEvltr 用来计算整体状态
b.csEvltr.RecordTransition(connectivity.Shutdown, connectivity.Idle)
// ★★★ 开始连接 ★★★
// 也就是 acBalancerWrapper.Connect 函数
sc.Connect()
// 此时 SubConn1/2/3 开始 TCP 握手 同时 用的协程 每一个都是一个地址
}
}
// 清理已下线的地址(第一次调用时 subConns 和 addrsSet 一样,不会清理)
// 如果第n次执行 本来有三个 subConns 现在 address 有两个 说明我要关闭一个 subConns
for _, a := range b.subConns.Keys() {
sc, _ := b.subConns.Get(a)
if _, ok := addrsSet.Get(a); !ok {
// 地址不在新列表里了 → 服务下线了
sc.Shutdown()
b.subConns.Delete(a)
}
}
// 如果没有地址,返回错误
if len(s.ResolverState.Addresses) == 0 {
b.ResolverError(errors.New("produced zero addresses"))
return balancer.ErrBadResolverState
}
// 重新生成 Picker 但此时还没有 Ready 的 SubConn
// 之前 bal.picker = NewErrPicker(balancer.ErrNoSubConnAvailable)
b.regeneratePicker()
// b.picker = ErrPicker 因为没有 Ready 的
// 通知上层状态 调用的是 balancerWrapper.UpdateState 向上通知 这是第一次
b.cc.UpdateState(balancer.State{
ConnectivityState: b.state, // Connecting
// 如果有 ready的 那就重新赋值成 kratos 的 balancerPicker 后续大家会明白
Picker: b.picker,
})
return nil
}
真正的 balancerWrapper.NewSubConn() 函数
从 ccBalancerWrapper 到 ClientConn,再到真正的 addrConn。这么多层封装是为了确保:只有在算法没过期、客户端没关闭的情况下,连接才会被创建。
go
func (bw *balancerWrapper) NewSubConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
bw.gsb.mu.Lock()
if !bw.gsb.balancerCurrentOrPending(bw) {
// 检查这个 bw 是否还是 current 或 pending
// 如果不是(比如已经被新策略替换了),就不能创建新连接
bw.gsb.mu.Unlock()
return nil, fmt.Errorf("%T at address %p that called NewSubConn is deleted", bw, bw)
}
bw.gsb.mu.Unlock()
var sc balancer.SubConn
// 包装 StateListener
oldListener := opts.StateListener // 保存原来的监听器 也就是上面哪个函数
// 无非是 变化了 调用哪个函数去更新, bw.gsb 是 CCbalancerWrapper.balancer 也就是多包了一层
opts.StateListener = func(state balancer.SubConnState) { bw.gsb.updateSubConnState(sc, state, oldListener) }
sc, err := bw.gsb.cc.NewSubConn(addrs, opts)
if err != nil {
return nil, err
}
bw.gsb.mu.Lock()
// 二次检查
/*
t0: 第一次检查通过
t1: bw.gsb.cc.NewSubConn(addrs, opts) 开始执行(可能耗时)
t2: 策略切换发生,bw 被淘汰
t3: bw.gsb.cc.NewSubConn 返回 sc
t4: 如果不再次检查,sc 会被加到已废弃的 bw.subconns 里
所以需要双重检查
*/
if !bw.gsb.balancerCurrentOrPending(bw) {
sc.Shutdown()
bw.gsb.mu.Unlock()
return nil, fmt.Errorf("%T at address %p that called NewSubConn is deleted", bw, bw)
}
// 记录到 map中 表示存在 存的是 acBalancerWrapper
bw.subconns[sc] = true
bw.gsb.mu.Unlock()
return sc, nil
}
bw.gsb.cc.NewSubConn() 函数
本质上是 ccBalancerWrapper 的函数
go
func (ccb *ccBalancerWrapper) NewSubConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
ccb.cc.mu.Lock()
defer ccb.cc.mu.Unlock()
ccb.mu.Lock()
if ccb.closed { //看看关闭了么
ccb.mu.Unlock()
return nil, fmt.Errorf("balancer is being closed; no new SubConns allowed")
}
ccb.mu.Unlock()
if len(addrs) == 0 { // 没有地址 我们肯定有地址
return nil, fmt.Errorf("grpc: cannot create SubConn with empty address list")
}
// 创建底层连接
ac, err := ccb.cc.newAddrConnLocked(addrs, opts)
if err != nil {
channelz.Warningf(logger, ccb.cc.channelz, "acBalancerWrapper: NewSubConn: failed to newAddrConn: %v", err)
return nil, err
}
// 创建 acBalancerWrapper
acbw := &acBalancerWrapper{
ccb: ccb, // 指向 ccBalancerWrapper
ac: ac, // 指向 addrConn(底层连接)
producers: make(map[balancer.ProducerBuilder]*refCountedProducer),
stateListener: opts.StateListener, // 状态监听器(已经被包装过了)
healthData: newHealthData(connectivity.Idle), // 健康检查数据
}
ac.acbw = acbw // 反向引用,ac 需要通过 acbw 通知状态变化
/*
addrConn 也就是ac 是 gRPC 内部的底层实现,有很多内部细节
acBalancerWrapper 是一层封装,把内部细节隐藏起来
只暴露 balancer.SubConn 接口需要的方法
*/
return acbw, nil
}
ccb.cc.newAddrConnLocked() 函数
本质上是 NewClient的主Conn 的函数
go
func (cc *ClientConn) newAddrConnLocked(addrs []resolver.Address, opts balancer.NewSubConnOptions) (*addrConn, error) {
if cc.conns == nil {
return nil, ErrClientConnClosing
}
// 创建 addrConn
ac := &addrConn{
state: connectivity.Idle, // 初始状态:空闲
cc: cc, // 指向 ClientConn
addrs: copyAddresses(addrs), // 地址列表 拷贝一份防止调用者后续修改影响到内部
scopts: opts, // SubConn 选项 上面传下来的
dopts: cc.dopts, // 我们自己传给 NewClient() 的 比如 TLS 配置、超时等
channelz: channelz.RegisterSubChannel(cc.channelz, ""), // 监控
resetBackoff: make(chan struct{}), // 重置退避的信号
}
// 创建 context cc.ctx 是 NewClient的 ctx 是最大的那个
ac.ctx, ac.cancel = context.WithCancel(cc.ctx)
// 设置监控目标 跟踪相关
ac.channelz.ChannelMetrics.Target.Store(&addrs[0].Addr)
channelz.AddTraceEvent(logger, ac.channelz, 0, &channelz.TraceEvent{
Desc: "Subchannel created",
Severity: channelz.CtInfo,
Parent: &channelz.TraceEvent{
Desc: fmt.Sprintf("Subchannel(id:%d) created", ac.channelz.ID),
Severity: channelz.CtInfo,
},
})
// 把 ac 加入 ClientConn 的连接池 用于追踪所有活跃的 SubConn
cc.conns[ac] = struct{}{}
return ac, nil
}
为什么创建连接需要这么多层
markdown
balancerWrapper 策略切换管理
确保 SubConn 只在有效的 balancer 里创建
包装 StateListener 让 gracefulswitch 能拦截
ccBalancerWrapper 连接生命周期管理
检查 ClientConn 是否关闭
创建 acBalancerWrapper 封装
ClientConn 底层连接创建
创建真正的 addrConn
注册到连接池 cc.conns
acBalancerWrapper.Connect() 函数
这里的逻辑是 baseBalancer.UpdateClientConnState() 函数执行完 NewSubConn() 函数初始化 addrConn 后,开始建立底层连接
go
func (acbw *acBalancerWrapper) Connect() {
// 协程 执行 真正的 connect 这层只是包装器 然后直接返回
go acbw.ac.connect()
}
addrConn.connect() 函数
到了这里,才是真正的网络 IO。它会根据退避算法(防止重试太快压垮服务端)尝试建立 TCP 连接和 HTTP/2 会话。
go
func (ac *addrConn) connect() error {
ac.mu.Lock()
if ac.state == connectivity.Shutdown {
if logger.V(2) {
logger.Infof("connect called on shutdown addrConn; ignoring.")
}
ac.mu.Unlock()
return errConnClosing
}
if ac.state != connectivity.Idle {
if logger.V(2) {
logger.Infof("connect called on addrConn in non-idle state (%v); ignoring.", ac.state)
}
ac.mu.Unlock()
return nil
}
ac.resetTransportAndUnlock()
return nil
}
ac.resetTransportAndUnlock() 函数
这里最重要的就是 tryAllAddrs() 函数,流程如下: 遍历每个地址,然后调用 createTransport 尝试连接 如果失败,记录第一个错误,继续尝试下一个地址 (我们这就一个地址) 只有在DNS 解析出多个 ip 才会有好几个 只要一个对了就行 如果有地址连接成功,返回 nil。如果所有地址都失败,返回第一个遇到的错误。第一个错误就差不多代表所有错误 updateConnectivityState(Ready) 把状态变为 Ready 里面的 createTransport 函数 流程: 获取服务名称 比如 user-service 你服务端注册到 Consul 的服务名 创建健康检查 开一个协程 周期性调用服务端的 grpc.health.v1.Health/Watch 方法 定义连接关闭回调 当连接断开时会被调用 如果是正常关闭(context 已取消),什么都不做 将连接状态更新为 Idle,等待负载均衡器决定是否重连 建立真正的 HTTP/2 连接,连接成功后 保存当前地址到 curAddr 保存 transport 引用,后续发送 RPC 要用
go
ac.curAddr = addr // ac 就是 addrConn
ac.transport = newTr // 它代表一条真实的 TCP 连接 + HTTP/2 会话
最重要的 健康检查返回正确后 会把状态改成 Ready 也就是调用第三次 balancerWrapper.UpdateState() 向上通知 解释: 第一次 是 connect()协程, 主协程继续执行 由于没有 ready,picker 初始化的错误,picker 把状态从 空闲变成 connecting 第二次 是 上边 ac.updateConnectivityState(connectivity.Connecting, nil) 这个函数 里 第三次 就是健康检查结束 执行 acbw.stateListener(scs) 状态为 Ready 就可以后续 picker 正确
go
func (ac *addrConn) resetTransportAndUnlock() {
acCtx := ac.ctx
if acCtx.Err() != nil {
ac.mu.Unlock()
return
}
addrs := ac.addrs
// 计算退避时间 ac.backoffIdx 是重试次数 ackoff 返回的是指数退避时间
// 第0次: 0s 第1次: 1s 第2次: 2s 第3次: 4s ...
backoffFor := ac.dopts.bs.Backoff(ac.backoffIdx)
// 最小连接超时时间 20s
dialDuration := minConnectTimeout
if ac.dopts.minConnectTimeout != nil {
// 自己设置了 就用自己的
dialDuration = ac.dopts.minConnectTimeout()
}
if dialDuration < backoffFor {
// 超时了就用这个 退避时间
dialDuration = backoffFor
}
// 计算截止时间
connectDeadline := time.Now().Add(dialDuration)
// 重点 这是第二次 改变状态 结果和第一次一样 这里面有 UpdateState 函数 下面有
ac.updateConnectivityState(connectivity.Connecting, nil)
ac.mu.Unlock()
// ★★★ 尝试连接 ★★★
/*
ClientConn
└── conns (map[*addrConn]struct{})
└── addrConn
├── curAddr: "192.168.1.100:8080"
├── state: Ready
└── transport: newTr ← RPC 调用时从这里取出 transport,创建 HTTP/2 流,发送请求、接收响应
*/
if err := ac.tryAllAddrs(acCtx, addrs, connectDeadline); err != nil {
// 请求重新解析(让 resolver 刷新地址列表)
ac.cc.resolveNow(resolver.ResolveNowOptions{})
ac.mu.Lock()
if acCtx.Err() != nil {
// addrConn was torn down.
ac.mu.Unlock()
return
}
// 更新状态为 TransientFailure
ac.updateConnectivityState(connectivity.TransientFailure, err)
// Backoff.
b := ac.resetBackoff
ac.mu.Unlock()
timer := time.NewTimer(backoffFor)
select {
case <-timer.C:
ac.mu.Lock()
ac.backoffIdx++
ac.mu.Unlock()
case <-b:
timer.Stop()
case <-acCtx.Done():
timer.Stop()
return
}
ac.mu.Lock()
if acCtx.Err() == nil {
ac.updateConnectivityState(connectivity.Idle, err)
}
ac.mu.Unlock()
return
}
// Success; reset backoff.
ac.mu.Lock()
ac.backoffIdx = 0
ac.mu.Unlock()
}
addrConn.updateConnectivityState() 函数
这里不要搞混了,我们走的是 err == nil 的路,是 createTransport() 这个函数中需要执行这个函数变成 Ready 状态,是决定性的核心,只有 Ready 后,才能去选择节点,证明 Rpc 可以连接。
我们现在走的是 go connect() 这个协程里的,是跟主程序一块走的,所以在这里调用的 acBalancerWrapper.updateState() 函数,绝对不是第一次,因为主进程也调用,每一个 connect() 也都会调用一遍,该函数会调用非常多边,直到这一次,Ready 了,就可以进行后续操作了,不然一直 picker 是错误。
go
func (ac *addrConn) updateConnectivityState(s connectivity.State, lastErr error) {
if ac.state == s {
return
}
// ac.state = Ready
// 更新状态
ac.state = s
ac.channelz.ChannelMetrics.State.Store(&s)
if lastErr == nil {
channelz.Infof(logger, ac.channelz, "Subchannel Connectivity change to %v", s)
} else {
channelz.Infof(logger, ac.channelz, "Subchannel Connectivity change to %v, last error: %s", s, lastErr)
}
// 调用 acBalancerWrapper.updateState 也就是上层的
ac.acbw.updateState(s, ac.curAddr, lastErr)
}
ac.acbw.updateState() 函数
这里依旧是熟悉的 serializer 的永远不会阻塞发送方 的队列,放进去串行执行,本质上就是判断是否是 Ready,不是就相当于没做什么,是的话后续才有意义。
这里的 setConnectedAddress(&scs, curAddr) 就是在 Ready 状态下,把连上了谁这个信息打包进 Balancer.SubConnState 字段,让 balancer 的监听器能感知到具体连接地址,后续可以通过 GetConnectedAddress(scs) 拿到地址
go
func (acbw *acBalancerWrapper) updateState(s connectivity.State, curAddr resolver.Address, err error) {
// acbw.ccb 是 ccBalancerWrapper 放进去串行执行
acbw.ccb.serializer.TrySchedule(func(ctx context.Context) { //闭包
if ctx.Err() != nil || acbw.ccb.balancer == nil {
return
}
acbw.closeProducers()
// balancer.SubConnState 结构体 构建参数
scs := balancer.SubConnState{ConnectivityState: s, ConnectionError: err}
if s == connectivity.Ready {
setConnectedAddress(&scs, curAddr)
}
acbw.healthMu.Lock()
// 如果是 Ready,健康检查可以开始
// 如果是其他状态,健康检查会停止
acbw.healthData = newHealthData(scs.ConnectivityState)
acbw.healthMu.Unlock()
/* 重要 acbw.stateListener 就是下面这个 是包装的旧的
opts.StateListener = func(state balancer.SubConnState) { bw.gsb.updateSubConnState(sc, state, oldListener) }
*/
acbw.stateListener(scs)
})
}
bw.gsb.updateSubConnState() 函数
本质上是 CCbalancerWrapper.balancer.updateSubConnState() 函数,我们执行的 acbw.stateListener(scs) 是这个,这只是个壳子,本质是这个函数,这里的 cb 就是包装的旧的 oldListener 。
前面看看就可以,主要是应对负载均衡策略热切换,我们现在是静态,先不需要知道。
go
func (gsb *Balancer) updateSubConnState(sc balancer.SubConn, state balancer.SubConnState, cb func(balancer.SubConnState)) {
gsb.currentMu.Lock()
defer gsb.currentMu.Unlock()
gsb.mu.Lock()
// 找到这个 SubConn 属于哪个 balancerWrapper
var balToUpdate *balancerWrapper
// 看看 current 是不是 balancerCurrent 是真正 balancerWrapper
if gsb.balancerCurrent != nil && gsb.balancerCurrent.subconns[sc] {
balToUpdate = gsb.balancerCurrent
} else if gsb.balancerPending != nil && gsb.balancerPending.subconns[sc] {
// 看看 balancerPending 是不是
balToUpdate = gsb.balancerPending
}
if balToUpdate == nil {
gsb.mu.Unlock()
return
}
// 如果是 Shutdown,从 subconns 中删除 从哪里初始化? 在真正的 balancerWrapper.NewSubConn 这个函数中增加的
if state.ConnectivityState == connectivity.Shutdown {
delete(balToUpdate.subconns, sc)
}
gsb.mu.Unlock()
if cb != nil {
cb(state)
} else {
// // cb 为 nil 的情况(一般不会发生)
balToUpdate.UpdateSubConnState(sc, state)
}
}
cb(state) 函数
其实是 我们旧的 oldListener 函数 也就是第一层 baseBalancer 函数 用于填充字段的那个函数,我已经放下面了,我们这里是 Ready 状态, 我说过这些函数会执行很多次,只有真正建立完连接是 Ready 后,才会继续向下走,这里如果是还没准备好或者有问题了,就再执行 sc.Connect() 函数重新建立连接。
go
StateListener: func(scs balancer.SubConnState) {
b.updateSubConnState(sc, scs) // setConnectedAddress(&scs, curAddr) 这个的时候已经放进去了
}
大家要明白我们从服务发现中拿到了很多服务实例,这些每一个服务实例,我都需要建立连接,也就是 sc.Connect() ,所以说每一个执行好了都会走这些函数,去更新状态, Picker 是负载均衡选哪一个的,我们初始化是 err 没问题,因为连接没建立好,啥时候去修改 Picker 呢,显然是 Ready 后,所以接下来就去改变 Picker, 终于到了我们自己写的 InitBuilder(),传入的函数了。
go
func (b *baseBalancer) updateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
s := state.ConnectivityState // Ready
if logger.V(2) {
logger.Infof("base.baseBalancer: handle SubConn state change: %p, %v", sc, s)
}
// 初始化的 是 空闲
oldS, ok := b.scStates[sc]
if !ok {
if logger.V(2) {
logger.Infof("base.baseBalancer: got state changes for an unknown SubConn: %p, %v", sc, s)
}
return
}
if oldS == connectivity.TransientFailure &&
(s == connectivity.Connecting || s == connectivity.Idle) {
// 这里就是 connect 失败了 我们忽略 如果是 Idle 状态 那就调用 Connect 连接
if s == connectivity.Idle {
sc.Connect()
}
return
}
// 更新状态记录 我们当时初始化所有地址都是 Idle 现在这个有连接了所以更新状态
b.scStates[sc] = s
switch s {
case connectivity.Idle:
sc.Connect() // 这里重新建立连接
case connectivity.Shutdown:
delete(b.scStates, sc)
case connectivity.TransientFailure:
b.connErr = state.ConnectionError
}
b.state = b.csEvltr.RecordTransition(oldS, s)
if (s == connectivity.Ready) != (oldS == connectivity.Ready) ||
b.state == connectivity.TransientFailure {
// 重要!!!! Ready 就可以执行这个函数了 前两次都是错误的 picker
b.regeneratePicker()
}
b.cc.UpdateState(balancer.State{ConnectivityState: b.state, Picker: b.picker})
}
baseBalancer.regeneratePicker() 函数
这里初始化我们的负载均衡器,选出所有 Ready 的连接,让我们写的算法能够挑出一个使用,下面我给大家放一下我们注册的负载均衡控制器的层级:
go
const (
balancerName = "selector"
)
func init() {
b := base.NewBalancerBuilder(
balancerName,
&balancerBuilder{
builder: selector.GlobalSelector(),
},
base.Config{HealthCheck: true},
)
balancer.Register(b)
}
base.NewBalancerBuilder(...) 函数返回的是 baseBalancer 实例,也就是当前的结构体,这时候 b.pickerBuilder 就是 kratos 的 balancerBuilder,然后它里面有个 builder 才是我们注册的,其实里面还嵌套了一层,后续大家就会明白。
go
func (b *baseBalancer) regeneratePicker() {
// 1. 如果整体状态是失败,返回错误 Picker
if b.state == connectivity.TransientFailure {
b.picker = NewErrPicker(b.mergeErrors())
return
}
// 收集所有 Ready 的 SubConn
readySCs := make(map[balancer.SubConn]SubConnInfo)
// b.subConns.Keys() 返回 []resolver.Address,就是所有已知的服务地址列表
// b.subConns.Get(addr) 返回对应的 balancer.SubConn,也就是那个地址的底层 TCP 连接对象
for _, addr := range b.subConns.Keys() {
sc, _ := b.subConns.Get(addr)
if st, ok := b.scStates[sc]; ok && st == connectivity.Ready {
readySCs[sc] = SubConnInfo{Address: addr}
}
}
// 执行 kratos 也就是我们自己的函数了
b.picker = b.pickerBuilder.Build(PickerBuildInfo{ReadySCs: readySCs})
}
kratos 的 PickerBuilder.Build() 函数
这里的参数 info base.PickerBuildInfo 就是我们筛选出来 Ready 的 SubConn (底层连接)和我们服务发现获得的那些实例,同样的服务发现的手法,先把这些实例转换成自己的结构体,方便后续使用,后续我有介绍了,可继续向下走。
go
func (b *balancerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
// 没有 Ready 的 SubConn,返回错误 Picker
if len(info.ReadySCs) == 0 {
return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
}
// 如果这一次调用 已经有 Ready的话
// 把 gRPC 的 SubConn 转成自己的 Node
nodes := make([]selector.Node, 0, len(info.ReadySCs))
for conn, info := range info.ReadySCs {
// 从 Attributes 取出我们 resolver 里存的 ServiceInstance 这样就不用转换了
ins, _ := info.Address.Attributes.Value("rawServiceInstance").(*registry.ServiceInstance)
// 创建 grpcNode(包装 SubConn 和 Node)
nodes = append(nodes, &grpcNode{
Node: selector.NewNode("grpc", info.Address.Addr, ins),
subConn: conn, // ★ 保存 SubConn,后面 Pick 要用
})
}
// 用 selector.Builder 创建 Selector
p := &balancerPicker{
selector: b.builder.Build(), // GlobalSelector().Build() 返回 Default
}
// 把节点列表应用到 Selector
p.selector.Apply(nodes)
return p
}
b.builder.Build() 函数
比如用 随机,这里用什么取决于 SetGlobalSelector(builder Builder) 传入的是什么,传入的是随机就是下面代码。
之后会介绍 Default 结构体。
go
// 无论是随机 还是 p2c 还是 wrr 都走这个 因为 我们传递给globalselector 都是 用的一个 DefaultBuilder
func (db *DefaultBuilder) Build() Selector {
return &Default{
NodeBuilder: db.Node, // 这个只是用于将 node 变成 WeightedNode 后续大家就明白了
Balancer: db.Balancer.Build(), // 这个就是具体的 什么策略
}
}
// db.Balancer.Build()
func (b *Builder) Build() selector.Balancer {
return &Balancer{} //都是自己定义的结构体 比如 p2c也是返回他自己定义的Balancer 只要实现pick方法就好
}
build 调用链
rust
从 CCBalancerWrapper 的 updateClientConnState 开始执行到它内部吧 balancer 的 UpdateClientConnState 再到 balancer 内部的 balancerWrapper 的 UpdateClientConnState 函数(嵌套字段实现的方法)
这些时候 我们注册的负载均衡器的变化: 共四层
第一层:首先注册的是 base 的 NewBalancerBuilder 返回 baseBuilder,执行build方法 返回 baseBalancer
第二层:baseBalancer 里面有 PickerBuilder 执行它的 build 方法,生成 balancerPicker
第三层:balancerPicker 里面有 selector 字段执行 build 方法生成 Default
第四层:Default 里面才是具体的策略实现 builder,执行 build,生成 Balancer 里面有真正的 pick 方法 策略
首先 grpc的要求 Balancer(连接管理) 和 Picker(连接选择) 做分离设计
balancer.Builder → 实例化 balancer.Balancer
PickerBuilder → 实例化 balancer.Picker
前第一层好理解 框架需要
第二层是因为:需要把地址和底层连接的实例变成 kratos 内部的,相当于服务发现后换成grpc的,这时候再转回来
第三层:正常情况是到了执行的地方,但是你得写死,用轮询就轮询,用随机就写随机,一旦扩展或者更换得重写代码,这不合理,所以再来一层,这一层就是你传来什么算法,这里就去分发给具体的结构体比如轮询专门的结构体进行负载均衡算法,想要扩展就不需要动这主逻辑。
第四层:是 kratos 加了一个扩展 也就是用什么算法 + 统计延迟 两两组合 也可以不选择统计 后续看到就会明白。
Apply() 方法
Apply 负责把节点存储到原子变量里。如果是随机算法,节点就很简单;如果是 P2C/EWMA 算法,这里会给节点挂载复杂的数学统计逻辑。也就是上面我说的第四层要做的东西。
go
func (d *Default) Apply(nodes []Node) {
// 创建一个空的 WeightedNode 切片
// 为什么要转成 WeightedNode 因为后续 Pick 时需要权重信息
weightedNodes := make([]WeightedNode, 0, len(nodes))
// weightedNodes = []
// 遍历每个 Node,用 NodeBuilder 包装成 WeightedNode
for _, n := range nodes {
// d.NodeBuilder 是什么 每一个策略不一样 看你注册到globalselector 当时的参数
// - 如果用 random的话 NodeBuilder = direct.Builder{}
// - 如果用 p2c的话 NodeBuilder = ewma.Builder{}
// Build 是给节点加上权重计算能力
weightedNodes = append(weightedNodes, d.NodeBuilder.Build(n))
}
// 举例 random(direct.Builder),执行完后 因为随机不需要权重
// weightedNodes = [
// directNode{grpcNode1, lastPick: 0},
// directNode{grpcNode2, lastPick: 0},
// directNode{grpcNode3, lastPick: 0},
// ]
// 假设用 p2c(ewma.Builder),执行完后:
// weightedNodes = [
// ewmaNode{grpcNode1, weight: 100, 延迟统计信息...},
// ewmaNode{grpcNode2, weight: 100, 延迟统计信息...},
// ewmaNode{grpcNode3, weight: 100, 延迟统计信息...},
// ]
//原子存储到 d.nodes
// Store 保证线程安全 并且是覆盖式赋值
d.nodes.Store(weightedNodes)
}
结构体与接口
这里再次给大家看一下用到的结构体,文章开始的时候页贴过,这次再看应该会轻松点。
go
type Node interface {
Scheme() string
Address() string
ServiceName() string
InitialWeight() *int64
Version() string
Metadata() map[string]string
}
type WeightedNode interface {
Node // 直接放这里面 下面的都是扩展
Raw() Node
Weight() float64
Pick() DoneFunc
PickElapsed() time.Duration
}
type DefaultNode struct { //这个实现了 Node 接口 selector.NewNode("grpc", info.Address.Addr, ins) 赋值上
scheme string
addr string
weight *int64
version string
name string
metadata map[string]string
}
// Default 字段
type Default struct {
NodeBuilder WeightedNodeBuilder //这个是一个接口 看下面 就是 专门 Node 变成 WeightedNode 的
Balancer Balancer // 这个就具体的算法
nodes atomic.Value // 用于存储的
}
// 实现这个方法才行 直接调用 Build 就可以 转换 也就是上面的 d.NodeBuilder.Build(n)
type WeightedNodeBuilder interface {
Build(Node) WeightedNode
}
// 我们举简单例子 random 策略 也就是 NodeBuilder = direct.Builder{} 他的build 方法
// 因为内部的 Node 实现了 WeightedNode 接口,相当于啥都没干,因为这个本来就不是给随机算法用的,这里只是兼容主逻辑
func (*Builder) Build(n selector.Node) selector.WeightedNode {
return &Node{Node: n, lastPick: 0} //这里的node 是内部的 node 看下面这个结构体
}
// 它内部的 Node 实现了 Raw、Weight、Pick、PickElapsed 方法 所以可以多态进行使用
type Node struct {
selector.Node //匿名结构体可以实现 WeightedNode 的 Node 这个的方法
// last lastPick timestamp
lastPick int64
}
真正的 balancerWrapper 的 UpdateState() 函数 (向上反馈)调用好几次
这个时候 picker 初始化完成了,接下来就是去继续 grpc 内部逻辑了,其实就是去通知已经准备好了,并且让DialContext 函数结束死循环,等到需要 Rpc 的时候,就执行我们初始化的 picker.pick 方法选择一个使用。
继续向下走是在 balToUpdate.UpdateClientConnState() 函数里面 。
go
func (bw *balancerWrapper) UpdateState(state balancer.State) {
// 加锁,保证原子性
bw.gsb.mu.Lock()
defer bw.gsb.mu.Unlock()
// 保存最新状态
bw.lastState = state
// bw.lastState = {
// ConnectivityState: ?, 第一 二次 是Conecting 当TCP完成后 在经过这个函数就是 Ready 了
// Picker: balancerPicker{...}
// }
// 检查这个 balancerWrapper 是否是当前生效的(current 或 pending)
if !bw.gsb.balancerCurrentOrPending(bw) {
return // 如果已经被淘汰了,就不往上报告了
}
/*
在 switchTo()函数中已经赋值
func (gsb *Balancer) balancerCurrentOrPending(bw *balancerWrapper) bool {
return bw == gsb.balancerCurrent || bw == gsb.balancerPending
}
*/
// 分支1: 这个 balancerWrapper 是 current 我们没有热切换策略 所以一直是 balancerCurret
if bw == bw.gsb.balancerCurret {
// 如果当前状态不是 Ready,且有 pending 的新策略在等待
if state.ConnectivityState != connectivity.Ready && bw.gsb.balancerPending != nil {
// 直接切换到 pending
bw.gsb.swap()
return
}
// 否则,继续往上报告状态 我们走这个 因为已经 Ready 完了,也就是拿到 Node 了
bw.gsb.cc.UpdateState(state) // ← 继续往上层传递
return
}
// 分支2: 这个 balancerWrapper 是 pending(正在切换中的新策略)
// 如果 pending 不是 Connecting,或者 current 不是 Ready
// 就执行切换
if state.ConnectivityState != connectivity.Connecting ||
bw.gsb.balancerCurrent.lastState.ConnectivityState != connectivity.Ready {
bw.gsb.swap() // 把 pending 变成 current
}
}
辅助理解
go
/*
t0: 主流程执行完,3 个 SubConn 都在后台连接
ReadySCs = {}
调用第一次 balancerWrapper 的 UpdateState方法 由于 ReadySCs = {} 空 返回错误picker 不去下面执行
connect 协程开始的时候 会调用第二次 balancerWrapper 的 UpdateState方法 还是一样的结果
t1: SubConn1 连接成功,状态变成 Ready
触发 updateSubConnState(SubConn1, {Ready})
b.scStates = {SubConn1: Ready, SubConn2: Connecting, SubConn3: Connecting}
regeneratePicker()
ReadySCs = {SubConn1}
→ kratos 的 balancerBuilder.Build() 被调用
→ Apply([grpcNode1])
→ Picker 有 1 个节点
t2: SubConn2 连接成功,状态变成 Ready
触发 updateSubConnState(SubConn2, {Ready})
b.scStates = {SubConn1: Ready, SubConn2: Ready, SubConn3: Connecting}
regeneratePicker()
ReadySCs = {SubConn1, SubConn2}
→ kratos的 balancerBuilder.Build() 被调用
→ Apply([grpcNode1, grpcNode2])
→ Picker 有 2 个节点
t3: SubConn3 连接成功,状态变成 Ready
触发 updateSubConnState(SubConn3, {Ready})
b.scStates = {SubConn1: Ready, SubConn2: Ready, SubConn3: Ready}
regeneratePicker()
ReadySCs = {SubConn1, SubConn2, SubConn3}
→ kratos的 balancerBuilder.Build() 被调用
→ Apply([grpcNode1, grpcNode2, grpcNode3])
*/
bw.gsb.cc.UpdateState() 函数
go
func (ccb *ccBalancerWrapper) UpdateState(s balancer.State) {
ccb.cc.mu.Lock()
defer ccb.cc.mu.Unlock()
// 检查 ClientConn 是否已关闭
if ccb.cc.conns == nil {
return
}
ccb.mu.Lock()
if ccb.closed {
ccb.mu.Unlock()
return
}
ccb.mu.Unlock()
// ★★★ 更新 Picker ★★★
// ccb.cc.pickerWrapper 其实就是 NewClient 返回的 ClientConn 的 pickerWrapper
ccb.cc.pickerWrapper.updatePicker(s.Picker) // s.picker 就是 balancerPicker
// ★★★ 更新连接状态 ★★★
// 把状态(Connecting/Ready/...)通知给状态管理器
ccb.cc.csMgr.updateState(s.ConnectivityState) // s.ConnectivityState 值是 Ready
}
pickerWrapper.updatePicker() 函数
go
func (pw *pickerWrapper) updatePicker(p balancer.Picker) {
// 我们在 newPickerWrapper 只是初始化 指针并没有东西 这个时候就填入
// Store 是把新值放进去,拿不到旧值;Swap是 把新值放进去,同时把旧值拿出来
old := pw.pickerGen.Swap(&pickerGeneration{
picker: p, // balancerPicker 里面字段 selector = default 结构体
blockingCh: make(chan struct{}),
})
close(old.blockingCh) // 关闭旧实例的通道,唤醒等待的goroutine 知道 旧 Picker 失效了,该用新的了
}
cc.csMgr.updateState() 函数
这个时候就是 Ready 了,那些阻塞的都可以继续了。
go
func (csm *connectivityStateManager) updateState(state connectivity.State) {
csm.mu.Lock()
defer csm.mu.Unlock()
// 检查: 如果已经 Shutdown(ClientConn 已关闭),就不再更新
if csm.state == connectivity.Shutdown { // 初始化是 0
return
}
// 检查: 如果状态没变化,也不更新
if csm.state == state { // 初始化是 0 也就是空闲 传来的是 Ready
return
}
// ★ 更新状态 最重要 因为 cc.GetState() 就可以查到了
csm.state = state
// 从 Connecting → Ready 这函数也是调用多次 之前从 idle → Connecting
// 更新 channelz 监控指标
csm.channelz.ChannelMetrics.State.Store(&state)
// 发布状态变化事件
csm.pubSub.Publish(state)
// 打印日志
channelz.Infof(logger, csm.channelz, "Channel Connectivity change to %v", state)
// ★★★ 关键:通知等待状态变化的 goroutine ★★★
if csm.notifyChan != nil {
close(csm.notifyChan) // 关闭 channel,唤醒所有等待者 也就是我们最初进来的 DialContext 函数
csm.notifyChan = nil
}
// 这个时候 就结束了返回ClientConn, 第一阶段结束了,剩下的就是调用函数了
}
Publish() 函数
这里的订阅者是 Grpc 给用户的扩展,这里直接跳过就行,本意可能是想如果自己的业务需要得到通知的话,可以使用这里的逻辑,只需要实现一个接口也就是订阅者的接口就行,这里就不多介绍了,因为 kratos 内部的服务发现负载均衡已经足够学习。
go
func (ps *PubSub) Publish(msg any) {
ps.mu.Lock()
defer ps.mu.Unlock()
ps.msg = msg // 保存最新的消息 Ready
// 遍历所有订阅者 刚开始为空直接跳过
for sub := range ps.subscribers {
s := sub // 捕获变量(闭包问题,必须这样写)
// 这里调用 TrySchedule
ps.cs.TrySchedule(func(context.Context) {
// 这个匿名函数会被放入队列
// 后台的 run goroutine 会取出来执行
ps.mu.Lock()
defer ps.mu.Unlock()
// 再次检查订阅者是否还存在(可能在等待期间被取消订阅了)
if !ps.subscribers[s] {
return
}
// 调用订阅者的 OnMessage 方法
s.OnMessage(msg)
})
}
}
// 提示一下 初始化
cc.csMgr = connectivityStateManager {
state: Idle (0)
notifyChan: nil
channelz: ...
pubSub: PubSub {
msg: nil // 看上面函数保存到这里
subscribers: {} // 空 map,没有订阅者
cs: CallbackSerializer {
done: chan struct{} // (未关闭)
callbacks: Unbounded // (空队列)
}
}
}
cc.WaitForStateChange(ctx, s) 函数
DialContext() 函数中 for 循环的逻辑,用到的函数一并写在下面了,主要是查看是否有 Ready 的连接,如果有,循环就退出,RPC 就可以调用了
go
func (cc *ClientConn) WaitForStateChange(ctx context.Context, sourceState connectivity.State) bool {
// 这个是获得 chan 下面用于阻塞
ch := cc.csMgr.getNotifyChan()
// 第一次是从 idle 到 connecting 第二次是从 connecting 到 ready 都会结束当前一轮
if cc.csMgr.getState() != sourceState {
return true
}
select {
case <-ctx.Done():
return false
case <-ch:
return true
}
}
func (csm *connectivityStateManager) getNotifyChan() <-chan struct{} {
csm.mu.Lock()
defer csm.mu.Unlock()
if csm.notifyChan == nil {
csm.notifyChan = make(chan struct{})
}
return csm.notifyChan
}
// 就是单纯查状态 idle connecting ready
func (csm *connectivityStateManager) getState() connectivity.State {
csm.mu.Lock()
defer csm.mu.Unlock()
return csm.state
}
开启RPC调用
这里模拟发送 RPC 请求。 我们要把连接注入到 gRPC 生成的 Stub ,因为gRPC 生成的客户端 Stub 必须依赖 *grpc.ClientConn,因为 RPC 调用时,会通过 ClientConn 内部的负载均衡器选择 subconn,从而实现负载均衡。这个设计是 gRPC 连接管理的标准范式。
go
clientConn, err := DialInsecure(context.Background()) //举例子
if err != nil {
panic(err)
}
client := v1.NewUserClient(clientConn) // v1 是 protobuf 的 package
resp, err := client.GetUserList(ctx, req)
protobuf 生成的代码
go
func (c *userClient) GetUserList(ctx context.Context, in *PageInfo, opts ...grpc.CallOption) (*UserListResponse, error) {
out := new(UserListResponse)
err := c.cc.Invoke(ctx, "/User/GetUserList", in, out, opts...) // 发起 RPC 调用的核心方法
if err != nil {
return nil, err
}
return out, nil
}
Invoke() 函数
go
func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply any, opts ...CallOption) error {
opts = combine(cc.dopts.callOptions, opts)
//去运行拦截器
if cc.dopts.unaryInt != nil {
return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
}
// 如果没有拦截器 直接执行这个,运行完拦截器 然后执行这个
return invoke(ctx, method, args, reply, cc, opts...)
}
流的概念以及为啥使用
diff
HTTP/1.1 :一个 TCP 连接同时只能处理一个请求,如果同时有很多请求 用户体验差
grpc 用的是 HTTP/2,它最重要的是 多路复用 一个 TCP 连接可以同时处理多个请求:
客户端 服务端
│ │
│ TCP 连接(transport) │
│══════════════════════════════│
│ │
│ stream 1: GetUser │
│ ─────────────────────────→ │
│ │
│ stream 3: CreateOrder │
│ ─────────────────────────→ │
│ │
│ stream 5: GetProduct │
│ ─────────────────────────→ │
│ │
│ ←───── stream 3 响应 ───── │ 响应可以乱序返回
│ ←───── stream 1 响应 ───── │
│ ←───── stream 5 响应 ───── │
Transport(TCP 连接)是管道,Stream(流)是管道里的独立通道。
HTTP/2 帧的结构
+-----------------------------------------------+
| Length (24 bits) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+---------------+
|R| Stream ID (31) | ← 这里标识属于哪个流
+-+---------------------------------------------+
| Payload |
+-----------------------------------------------+
HTTP/2 协议的要求
先发 HEADERS 帧,再发 DATA 帧。
HEADERS 帧:告诉服务端「我要调用什么方法」
DATA 帧:实际的请求数据
invoke() 函数
下面画的图是便于理解,真正的是 SendMsg() 函数直接发两个,构造 HEADERS 帧发送,然后接着发送 DATA 帧,并不是收到后再发,但是逻辑是那个样子。
go
func invoke(ctx context.Context, method string, req, reply any, cc *ClientConn, opts ...CallOption) error {
cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...) //创建流式 RPC 连接
/* 这个 newClientStream 逻辑有点多 核心重点:
包装流式拦截器 执行拦截器的前置逻辑
内部两大重点 1. 调用 getTransport() 函数 2. newStream() 创建流
getTransport 内部调用 cc.pickerWrapper.pick() 函数 cc就是NewClient返回的 主Conn
返回正常挑选出的节点后,就可以创建流了
并且 内部 transport.NewStream() 方法 → 发送 HEADERS 帧 也就是HTTP/2 协议的要求:先发 HEADERS 帧
*/
if err != nil {
return err
}
// - 把 proto 对象变成字节
// - 构造 HTTP/2 DATA 帧
// - 加上 stream ID
// - 写入 TCP 连接
if err := cs.SendMsg(req); err != nil {
return err
}
// RecvMsg 内部会:
// - 从 TCP 连接读取 HTTP/2 帧
// - 根据 stream ID 找到属于这个请求的数据
// - 返回 DATA 帧的 payload
// - 把字节反序列回 proto 对象
return cs.RecvMsg(reply)
// ★★ 最后别忘了 有个回调函数 ★★
}
/* SendMsg 和 RecvMsg
客户端 服务端
│ │
│ client.GetUser(ctx, &GetUserRequest{UserId: 123}) │
│ │
│ 1. 序列化:proto → bytes │
│ 2. 创建 stream(分配 stream ID = 1) │
│ 3. 构造 HEADERS 帧: │
│ - stream ID = 1 │
│ - :method = POST │
│ - :path = /user.UserService/GetUser │
│ 4. 发送 HEADERS 帧 │
│ ─────────────────────────────────────────────────────→│
│ │ 5. 读取 HEADERS 帧
│ 6. 构造 DATA 帧: │ 根据 :path 找到 handler
│ - stream ID = 1 │
│ - payload = [序列化的请求数据] │
│ 7. 发送 DATA 帧 │
│ ─────────────────────────────────────────────────────→│
│ │ 8. 读取 DATA 帧
│ │ 9. 反序列化
│ │ 10. 调用 GetUser(req)
│ │ 11. 得到响应
│ │ 12. 序列化响应
│ │ 13. 发送 HEADERS + DATA 帧
│ ←─────────────────────────────────────────────────────│
│ 14. 读取响应帧(根据 stream ID = 1) │
│ 15. 反序列化 │
│ 16. 返回给用户 │
*/
getTransport() 函数
go
func (a *csAttempt) getTransport() error {
cs := a.cs // cs 是 clientStream
//构建 PickInfo
pickInfo := balancer.PickInfo{
Ctx: a.ctx, // 请求的 context
FullMethodName: cs.callHdr.Method, // 比如 "/user.UserService/GetUser"
}
// ★★★ 重点 调用 pick 选择节点 ★★★
pick, err := cs.cc.pickerWrapper.pick(a.ctx, cs.callInfo.failFast, pickInfo)
// pick 返回结果包含: 看完 pickerWrapper.pick 调用完 再回过头看这个
// - transport: HTTP/2 transport,用于发送请求
// - result: {SubConn, Done}
// - blocked: 是否曾经阻塞等待过
// 保存结果
a.transport = pick.transport // 保存 transport
a.pickResult = pick.result // 保存 PickResult(包含 Done 回调)
// 错误处理
if err != nil {
if de, ok := err.(dropError); ok {
// dropError 表示这个请求应该被丢弃
// 比如负载均衡策略决定丢弃这个请求(限流等)
err = de.error
a.drop = true // 标记为丢弃
}
return err
}
// 记录远程地址(用于 trace)
if a.trInfo != nil {
a.trInfo.firstLine.SetRemoteAddr(a.transport.RemoteAddr())
// 例子:RemoteAddr() = "192.168.1.100:8080"
}
//统计:如果曾经阻塞过,通知 statsHandler
if pick.blocked && a.statsHandler != nil {
a.statsHandler.HandleRPC(a.ctx, &stats.DelayedPickComplete{})
// 用于监控统计,表示这次 pick 曾经等待过
}
return nil
}
cc.pickerWrapper.pick() 函数
它从原子变量里拿出最新的 Picker,执行算法。如果算法说没节点可用,它会循环等待;如果选到了,就拿到底层 transport 发射请求。
go
func (pw *pickerWrapper) pick(ctx context.Context, failfast bool, info balancer.PickInfo) (pick, error) {
var ch chan struct{}
var lastPickErr error
pickBlocked := false
for {
// 加载当前的 pickerGeneration 这个在 updatePicker() 函数中 更新了 是 kratos 的 balancerPicker
pg := pw.pickerGen.Load()
if pg == nil {
return pick{}, ErrClientConnClosing
}
if pg.picker == nil {
ch = pg.blockingCh // 这里标记一下 需要阻塞 因为当前没 负载均衡选择器
}
// 这个是 没连上 或者 断开了 需要等待 走这个
if ch == pg.blockingCh {
select {
case <-ctx.Done():
var errStr string
if lastPickErr != nil {
errStr = "latest balancer error: " + lastPickErr.Error()
} else {
errStr = fmt.Sprintf("%v while waiting for connections to become ready", ctx.Err())
}
switch ctx.Err() {
case context.DeadlineExceeded:
return pick{}, status.Error(codes.DeadlineExceeded, errStr)
case context.Canceled:
return pick{}, status.Error(codes.Canceled, errStr)
}
case <-ch:
}
continue
}
// 标记是否阻塞过 正常如果刚开始那边 pickerWrapper.updatePicker 函数早就执行完了 ch 还是初始的状态为nil
// 只有 刚开始 pg.picker == nil 是这个 ch就被赋值
if ch != nil {
pickBlocked = true
}
// 这里是害怕下面会 continue 说明有问题 这里一直是死循环 所以 continue 后可以进入上面的阻塞
ch = pg.blockingCh
p := pg.picker // // 是我们自己的 balancerPicker
pickResult, err := p.Pick(info)
if err != nil {
// 没有可用连接
if err == balancer.ErrNoSubConnAvailable {
// 继续去阻塞等待
continue
}
// 其他错误
if st, ok := status.FromError(err); ok {
if istatus.IsRestrictedControlPlaneCode(st) {
err = status.Errorf(codes.Internal, "received picker error with illegal status: %v", err)
}
return pick{}, dropError{error: err}
}
if !failfast {
lastPickErr = err
continue
}
return pick{}, status.Error(codes.Unavailable, err.Error())
}
// 成功后 类型断言,获取 acBalancerWrapper
acbw, ok := pickResult.SubConn.(*acBalancerWrapper)
if !ok {
logger.Errorf("subconn returned from pick is type %T, not *acBalancerWrapper", pickResult.SubConn)
continue
}
// 获得底层 addrConn 的 Transport 也就是底层http2 连接实例 用于发送 RPC
// 这个函数就是 我们之前保存了 返回保存的 没保存 返回 nil
if t := acbw.ac.getReadyTransport(); t != nil {
if channelz.IsOn() {
doneChannelzWrapper(acbw, &pickResult)
}
// 这边是没问题的 t就不能为 nil
return pick{transport: t, result: pickResult, blocked: pickBlocked}, nil
}
// t 为 nil 有问题
if pickResult.Done != nil {
pickResult.Done(balancer.DoneInfo{})
}
logger.Infof("blockingPicker: the picked transport is not ready, loop back to repick")
}
}
balancerPicker.Pick() 函数
开始执行负载均衡算法。
go
func (p *balancerPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
// n: 选中的节点(类型 grpcNode 是grpc内部的) done: 完成回调函数
n, done, err := p.selector.Select(info.Ctx)
if err != nil {
return balancer.PickResult{}, err
}
return balancer.PickResult{
SubConn: n.(*grpcNode).subConn,
// 包装一层 因为下游写好了 done 接收的参数 是ctx 和 selector.DoneInfo 这是在传参 然后上游传 di 的参数
Done: func(di balancer.DoneInfo) { // di 是 gRPC 提供的完成信息
done(info.Ctx, selector.DoneInfo{
Err: di.Err, // RPC 是否出错
BytesSent: di.BytesSent, // 发送字节数
BytesReceived: di.BytesReceived, // 接收字节数
ReplyMD: Trailer(di.Trailer), // 响应的 metadata
})
},
}, nil
}
p.selector.Select() 函数
也就是 Default.Select() 函数
go
func (d *Default) Select(ctx context.Context) (selected Node, done DoneFunc, err error) {
var (
candidates []WeightedNode
)
// 之前 apply 存了
nodes, ok := d.nodes.Load().([]WeightedNode)
if !ok {
return nil, nil, ErrNoAvailable
}
candidates = nodes
if len(candidates) == 0 {
return nil, nil, ErrNoAvailable
}
// 真正算法逻辑
wn, done, err := d.Balancer.Pick(ctx, candidates)
if err != nil {
return nil, nil, err
}
// 用于让用户获取选中的节点信息 可选 先不了解
p, ok := FromPeerContext(ctx)
if ok {
p.Node = wn.Raw()
}
/*
随机算法 没啥逻辑 就是返回 原始节点
func (n *Node) Raw() selector2.Node {
return n.Node
}
*/
return wn.Raw(), done, nil
}
d.Balancer.Pick() 函数 (最后一层)真正逻辑
go
// 用随机举例子
func (p *Balancer) Pick(_ context.Context, nodes []selector2.WeightedNode) (selector2.WeightedNode, selector2.DoneFunc, error) {
if len(nodes) == 0 {
return nil, nil, selector2.ErrNoAvailable
}
cur := rand.Intn(len(nodes)) // 随机选一个下标
selected := nodes[cur]
d := selected.Pick() //由于我们当时注册的是 Node: &direct.Builder{} 用的是直接的 pick 下一个函数介绍
/*
为什么要这一层 是因为不同类型的节点有不同的行为。 如果只是满足 random 那根本不需要有不同的 Node
每一种算法有自己的Node
random + direct(节点) = 纯随机,不统计
p2c + ewma(节点) = 根据延迟和负载选择,自动统计
p2c + direct(节点) = 根据权重选择,但不统计(可以这样组合)
random + ewma(节点) = 随机选择,但统计延迟(虽然没意义,但可以)
*/
return selected, d, nil
}
Node.Pick() 函数 (对应的Node 由初始化决定)
这里说一下,我们再最后的底层是分成了两部分,一部分是负载均衡选节点,另一部分是选择的这个结点自己应该做些事情,虽然随机算法不需要,我给大家举一个P2C 算法,它的核心是:优先选 并发请求数最少的节点,优先选 延迟最低的节点,优先选健康状态最好的节点,那问题来了,这些数据(并发数、延迟、健康度)谁来统计呢,显然只有一部分选节点不太行,当然如果把这些东西放进去,这就耦合了,如果想随意组合或者后续换了,就得改代码了,如果分开,各干各的这样解耦,想扩展或者修改不会动主逻辑。
go
func (n *Node) Pick() selector2.DoneFunc {
now := time.Now().UnixNano()
atomic.StoreInt64(&n.lastPick, now)
// 什么都不做,因为 direct 节点不需要统计信息 如果是 p2c算法 非常多东西
return func(ctx context.Context, di selector2.DoneInfo) {}
}
整体流程
我们自己 用 selector.SetGlobalSelector(p2c.NewBuilder()) 或者 selector.SetGlobalSelector(random.NewBuilder()) 填上 全局 slector 然后执行 InitBuilder 注册到 grpc 用户拨号 进入 DialContext 函数: 1: NewClient 作用初始化一系列东西 重点: A: defaultServiceConfigRawJSON 解析我们送进来的 {"loadBalancingPolicy":"selector"} 还有个性化配置 本质上就是找到我们注册进去的 BalanceBuilder B: 还有初始化 CCBalancerWrapper 这个上面有详细流程图 后续用于 ccb.balancer.UpdateClientConnState 函数 C: 创建连接状态管理器等一系列管理器
2: ExitIdleMode 函数 调用 cc.resolverWrapper.start() 会调用我们自己的服务发现的build方法 ccr.resolver, err = ccr.cc.resolverBuilder.Build(ccr.cc.parsedTarget, ccr, opts)返回实现Resolver接口的结构体
我们调用真正服务发现的watch方法,因为这层类似接口层不会写死用什么 当时注册什么就用它的watch,要注册必须实现这个方法。 拿consul举例子,返回 watcher 代表一个 订阅方,详细去看服务发现。 执行本地update方法,作用把自己拿到的服务实例地址端口啥的,变成grpc能接收的结构体。 然后调用 它的 UpdateState 在这个方法里调用updateResolverStateAndUnlock这个方法 首先第一个点它会解析你是不是动态负载均衡,无论是不是本质上都是拿到我们自己注册的负载均衡的 InitBuilder
拿到后进入 ccbalancer 的 updateClientConnState 方法 这个时候 函数的形参从原来的只有服务实例列表变成服务实例列表 + lbConfig里面是我们的负载均衡builder 这个方法 有2件事情: 第一: 执行ccbalancer.balancer.UpdateClientConnState 这个方法 第二:执行调度 ccbalancer.serializer.Schedule 首先第一个函数: 首次调用或者动态策略变更 要执行 switchTo 函数 用于创建策略或者更新策略,执行 switchTo 方法 创建真正的 balancerWrapper 并且把我们传入的负载均衡builder 在这进行build方法 由于我们使用的 base方法 就直接返回的是 basebalancer 结构体挂到balancerWrapper 上,参数很重要有:地址 → SubConn 的映射和记录SubConn什么状态的字段等等,这个是首次调用或者变更需要的初始化 这个时候已经拿到了真正的balancerWrapper和basebalancer 需要执行basebalancer.UpdateClientConnState(state) 参数依然是那些,这个其实是我们自己的代码 但是由于我们用的是grpc的base 所以还是调用grpc的 如果你自己写一个initBuilder 返回的是自己定义的符合接口的 那这个地方就会调用你自己写的这个函数 ,这个函数主要是 填充刚才新建的字段 地址 → SubConn 的映射还有空闲字段,回答一个疑问 为啥不在初始化的时候就填上,如果第二次来,而不是第一次,那不会走switchTo 这个函数 正好有个实例下线了 咋更新呢,所以一定要分开,switchTo 只负责新建 UpdateClientConnState 负责填入或者更新 , UpdateClientConnState函数取出我们当时存的ServiceInstance 然后包装成自己的Node方便使用,最重要的这个函数会执行pickerBuilder的build方法,为什么这么多层以及啥时候调用,上面已经详细介绍过了,这里就不再多说了。
总结
纵观全篇,我们从 Kratos 的 GlobalSelector 入手,一路追踪到了 Grpc 底层的 addrConn 连接建立与 Picker 选路逻辑。可以看到,Kratos 并没有重新发明轮子,而是通过实现 Grpc 标准的 balancer.Builder 和 Picker 接口,将复杂的工业级负载均衡算法(如自适应限流、P2C、EWMA 等)优雅地注入到了 Grpc 的连接生命周期中。
负载均衡的核心其实不在于那几行随机或权重的算法,而在于其背后严谨的连接管理机制 :利用无界缓冲队列保证状态更新的串行安全、通过 SubConn 状态机确保只对 Ready 的节点发起请求、以及通过 Done 回调实现的闭环数据反馈。