go 进阶 go-zero相关: 六. 负载均衡与请求的发出

目录

  • 一. 负载均衡底层
    • 负载均衡器的注册
    • 运行时负载均衡器的初始化获取
    • 详解 p2cPicker 负载均衡器
      • 1. 获取目标服务节点信息封装为subConn
      • 2. p2cPicker的执行相关
  • 二. go-zero中是怎么执行负载均衡发出请求的
  • 三. 自定义负载均衡策略
  • 四. 总结

一. 负载均衡底层

  1. 在使用微服务时同一个服务我们会提供多个节点,在上层请求当前服务时选择指定节点请求,这个选择过程就指负载均衡,常见的负载均衡算法:
  1. 随机
  2. 轮询: 通过取模算法举例:假设有n台机器,n取模上次调用的第几台机器,得出的就是当前需要调用的
  3. 加权: 考虑到不同机器负载能力不同,给不同机器设置不同的权重,让性能更高的承载更多的请求
  4. P2C: go-zero中默认采用的负载均衡算法
  1. go-zero中默认采用P2C+EWMA
  1. P2C: 随机从所有可用节点中选择两个节点,然后计算这两个节点的负载情况,选择负载较低的一个节点来服务本次请求。为了避免某些节点一直得不到选择导致不平衡,会在超过一定的时间后强制选择一次
  2. EWMA指数移动加权平均的算法,表示是一段时间内的均值。该算法相对于算数平均来说对于突然的网络抖动没有那么敏感,突然的抖动不会体现在请求的lag中,从而可以让算法更加均衡
  1. 了解go-zero负载均衡首先要了解gprc下的几个接口
  1. gprc中针对负载均衡提供了Picker负载均衡器接口,该接口内部有一个Pick()方法,这个就是负载均衡算法
  2. grpc中针对负载均衡提供了用于创建Picker负载均衡器的接口PickerBuilder,该接口内部有一个Build方法,通过这个Bulid()方法获取服务节点的地址列表,负载均衡策略类型创建指定的Picker负载均衡器并返回
  3. grpc针对负载均衡提供了一个Builder接口在grpc的balancer包下,用于封装注册保存各种负载均衡器,
  4. 项目启动时会先创建PickerBuilder的Bulid,将负载均衡器封装为Builder保存到一个map中,在负载均衡器初始化时会执行PickerBuilder的Build()方法,创建指定的负载均衡器,后续例如发起调用时会根据指定标识,例如负载均衡器名称获取到指定负载均衡器,执行Picker中的Pick()方法进行负载均衡拿到指定的服务地址进行调用,所以想要实现负载均衡,要实现Picker接口,实现Picker接口中的Pick()负载均衡算法,要实现PickerBuilder接口,实现PickerBuilder接口中封创建指定负载均衡器的Bulid()方法
  5. go-zero中目前支持roundrobin轮询、random随机、consistenthash一致性哈希和p2c最小负载四种策略,默认使用p2c,针对p2c提供了p2cPickerBuilder与p2cPicker,后续后讲解
type PickerBuilder interface {
	Build(info PickerBuildInfo) balancer.Picker
}

type Picker interface {
	Pick(info PickInfo) (PickResult, error)
}
  1. 可以从负载均衡初始化启动,与均衡算法的触发执行两个角度去理解底层

负载均衡器的注册

  1. 先说明go-zero中默认使用p2c负载均衡算法,针对p2c供了p2cPickerBuilder与p2cPicker
  2. 接下来了解初始化逻辑: 在github.com\zeromicro\go-zero\zrpc\internal\balancer\p2c\p2c.go文件中通过一个init()初始函数,结合grpc注册了p2cPickerBuilder负载均衡器
  1. 这里内部调用了grpc下的函数,可能要结合grpc去看,
  2. 会调用grpc下的NewBalancerBuilder()函数,封装对应grpc负载均衡器的结构变量balancer.Builder,将这个Bulider存储到map中,实现负载均衡器的注册
  3. 默认情况下会以p2c_ewma为name,以p2cPickerBuilder为对应的负载均衡器封装balancer.Builder,将这个Builder存储到一个map中,map的key就是当前p2cPickerBuilder负载均衡器的名称"p2c_ewma"
func init() {
	//1.调用newBuilder()创建balancer.Builder
	//2.调用grpc下的Register()函数将balancer.Builder存储到一个map中
	balancer.Register(newBuilder())
}

func newBuilder() balancer.Builder {
	//3.调用grpc下的NewBalancerBuilder()函数,当前name为常量p2c_ewma,
	//创建p2cPickerBuilder负载均衡器,然后包装为balancer.Builder
	return base.NewBalancerBuilder(Name, new(p2cPickerBuilder), base.Config{HealthCheck: true})
}


//google.golang.org\grpc\balancer.go
func Register(b Builder) {
	m[strings.ToLower(b.Name())] = b
}

//google.golang.org\grpc\baes.go文件中
func NewBalancerBuilder(name string, pb PickerBuilder, config Config) balancer.Builder {
	return &baseBuilder{
		name:          name,
		pickerBuilder: pb,
		config:        config,
	}
}

运行时负载均衡器的初始化获取

  1. 再复习一下go-zero服务发现底层, 在编写go-zero服务消费方时需要调用zrpc下的MustNewClient()函数创建一个客户端,该函数中会调用一个NewClient(),解析target协议,注册拦截器,最终会调用到grpc下的DialContext()函数,在该函数中
  1. 先封装了一个grpc下的ClientConn结构体变量,该结构体表示一个客户端和服务端之间的连接,封装了连接的创建,管理,状态,配置,拦截器等功能
  2. 执行chainUnaryClientInterceptors() 与 chainStreamClientInterceptors()函数处理拦截器,将拦截器串联起来
  3. 调用parseTargetAndFindResolver()会再次解析target协议,将target解析成一个Target 结构体变量,该结构体中存在一个Scheme属性,内部存储的就是当前rpc服务发现模式,通过这个Scheme在m中获取到对应的Builder,在使用etcd作为注册中心时获取到的就是etcdBuilder
  4. 执行newCCBalancerWrapper()创建负载均衡器包装器ccResolverWrapper
  5. 执行newCCResolverWrapper(),在该函数中通过前面拿到的Builder,在使用etcd时也就是etcdBuilder,执行他的Builder()方法,获取resolverWrapper, resolverWrapper实现了resolver.ClientConn,通过resolver.ClientConn实现服务地址的更新
  1. 在grpc下的DialContext()函数中调用了newCCBalancerWrapper()创建负载均衡器包装器ccResolverWrapper,查看该函数
  1. 创建了一个ccBalancerWrapper
  2. 启动协程调用ccBalancerWrapper上的watcher()
func newCCBalancerWrapper(cc *ClientConn, bopts balancer.BuildOptions) *ccBalancerWrapper {
	ccb := &ccBalancerWrapper{
		cc:       cc,
		updateCh: buffer.NewUnbounded(),
		resultCh: buffer.NewUnbounded(),
		closed:   grpcsync.NewEvent(),
		done:     grpcsync.NewEvent(),
	}
	go ccb.watcher()
	ccb.balancer = gracefulswitch.NewBalancer(ccb, bopts)
	return ccb
}
  1. 在watcher()函数中开启了一个无限for循环,内部通过select_case监听updateCh通道上的消息,根据监听到的不同消息对负载均衡器进行指定更新或设置,我们这里重点关注一下updateCh返回switchToUpdate时的处理:
  1. 当前服务消费方设置了负载均衡策略时,例如设置p2c负载均衡器,updateCh通道会返回switchToUpdate
  2. 当前服务消费方没有指定负载均衡策略,但是有多个可用的子连接,这时会创建一个默认的负载均衡器p2c,updateCh通道会返回switchToUpdate
  3. 当前服务消费方配置发生了变化,例如服务名,负载均衡策略,服务发现方式等,updateCh通道也会返回switchToUpdate
  4. 在updateCh通道会返回switchToUpdate时,会执行ccBalancerWrapper上的handleSwitchTo()方法
func (ccb *ccBalancerWrapper) watcher() {
	for {
		select {
		//从更新通道获取更新消息
		case u := <-ccb.updateCh.Get():
			//加载更新通道的值
			ccb.updateCh.Load()
			if ccb.closed.HasFired() {
				//如果关闭信号量已经触发,跳出循环
				break
			}

			//获取更新类型
			switch update := u.(type) {
			case *ccStateUpdate:
				//ccStateUpdate表示客户端连接状态发生了变化,例如READY或SHUTDOWN,
				//调用ccb.handleClientConnStateChange()更新连接状态,并通知负载均衡器
				ccb.handleClientConnStateChange(update.ccs)
			case *scStateUpdate:
				//scStateUpdate表示子连接状态发生了变化,例如CONNECTING或TRANSIENT_FAILURE,
				//调用ccb.handleSubConnStateChange()更新子连接状态,并通知负载均衡器
				ccb.handleSubConnStateChange(update)
			case *exitIdleUpdate:
				//exitIdleUpdate表示客户端连接需要退出空闲状态,
				//调用ccb.handleExitIdle()触发负载均衡器的pick操作
				ccb.handleExitIdle()
			case *resolverErrorUpdate:
				//resolverErrorUpdate表示解析器发生了错误,
				//调用ccb.handleResolverError()处理错误,并通知负载均衡器
				ccb.handleResolverError(update.err)
			case *switchToUpdate:
				//switchToUpdate表示设置或切换指定负载均衡策略,
				//调用ccb.handleSwitchTo()设置或切换,并通知负载均衡器
				ccb.handleSwitchTo(update.name)
			case *subConnUpdate:
				//subConnUpdate表示需要移除一个子连接,
				//调用ccb.handleRemoveSubConn()移除,并通知负载均衡器
				ccb.handleRemoveSubConn(update.acbw)
			default:
				logger.Errorf("ccBalancerWrapper.watcher: unknown update %+v, type %T", update, update)
			}
		case <-ccb.closed.Done():
		}

		if ccb.closed.HasFired() { //如果关闭信号量已经触发
			//处理关闭操作
			ccb.handleClose()
			return
		}
	}
}
  1. 查看ccBalancerWrapper上的handleSwitchTo()方法
func (ccb *ccBalancerWrapper) handleSwitchTo(name string) {
	//1.判断当前的负载均衡策略和要切换的负载均衡策略相同 
	if strings.EqualFold(ccb.curBalancerName, name) {
		//如果相同直接返回
		return
	}

	//2.根据name在获取注册的负载均衡器,这里就来到了初始化时通过一个init()初始函数,Register()函数
	//结合gprc注册了创建负载均衡器封装为balancer.Builder,将balancer.Builder保存一个map中,
	//此处就是通过这个map获取到balancer.Builder,默认情况下创建p2cPickerBuilder负载均衡器,
	//并封装为baseBuilder保存到map中,那么此处默认获取到的Builder也就是baseBuilder
	builder := balancer.Get(name)
	if builder == nil {
		channelz.Warningf(logger, ccb.cc.channelzID, "Channel switches to new LB policy %q, since the specified LB policy %q was not registered", PickFirstBalancerName, name)
		//如果没有获取到负载均衡器,通过newPickfirstBuilder()使用默认的pickfirstBuilder
		builder = newPickfirstBuilder()
	} else {
		//如果有打印日志提示要切换到新的负载均衡器
		channelz.Infof(logger, ccb.cc.channelzID, "Channel switches to new LB policy %q", name)
	}

	//3.执行Balancer上的SwitchTo()方法进行切换
	if err := ccb.balancer.SwitchTo(builder); err != nil {
		channelz.Errorf(logger, ccb.cc.channelzID, "Channel failed to build new LB policy %q: %v", name, err)
		return
	}
	//更新ccb.curBalancerName为新的负载均衡器的名称
	ccb.curBalancerName = builder.Name()
}
  1. 继续查看Balancer上的SwitchTo()方法
//builder入参就是当前使用的负载均衡器Builder,例如默认情况下的p2cBuilder
func (gsb *Balancer) SwitchTo(builder balancer.Builder) error {
	//保证并发安全加锁
	gsb.mu.Lock()
	if gsb.closed {
		gsb.mu.Unlock()
		return errBalancerClosed
	}
	//1.封创建初始化balancerWrapper用来包装新的负载均衡器
	bw := &balancerWrapper{
		gsb: gsb,
		lastState: balancer.State{
			ConnectivityState: connectivity.Connecting,
			Picker:            base.NewErrPicker(balancer.ErrNoSubConnAvailable),
		},
		subconns: make(map[balancer.SubConn]bool),
	}
	//2.将gsb.balancerPending赋值给balToClose变量,表示要关闭的旧的负载均衡器
	balToClose := gsb.balancerPending 
	//如果gsb.balancerCurrent为空,将gsb.balancerCurrent赋值为新的balancerWrapper
	if gsb.balancerCurrent == nil {
		gsb.balancerCurrent = bw
	} else {
		//否则将gsb.balancerPending赋值为新的balancerWrapper
		gsb.balancerPending = bw
	}
	gsb.mu.Unlock()
	//3.关闭旧的负载均衡器
	balToClose.Close()
	
	//4.创建新的负载均衡器,默认情况下执行baseBuilder上的Build()方法
	newBalancer := builder.Build(bw, gsb.bOpts)
	//如果创建负载均衡器失败返回空,加锁处理,并将gsb.balancerPending或gsb.balancerCurrent置为空,
	//然后返回balancer.ErrBadResolverState错误
	if newBalancer == nil {
		gsb.mu.Lock()
		if gsb.balancerPending != nil {
			gsb.balancerPending = nil
		} else {
			gsb.balancerCurrent = nil
		}
		gsb.mu.Unlock()
		return balancer.ErrBadResolverState
	}

	bw.Balancer = newBalancer
	return nil
}
  1. 在baseBuilder的Build()方法中,会封装baseBalancer并返回,自此负载均衡器创建初始化完毕
func (bb *baseBuilder) Build(cc balancer.ClientConn, opt balancer.BuildOptions) balancer.Balancer {
	//封装baseBalancer
	bal := &baseBalancer{
		cc:            cc,
		pickerBuilder: bb.pickerBuilder,

		subConns: resolver.NewAddressMap(),
		scStates: make(map[balancer.SubConn]connectivity.State),
		csEvltr:  &balancer.ConnectivityStateEvaluator{},
		config:   bb.config,
		state:    connectivity.Connecting,
	}

	bal.picker = NewErrPicker(balancer.ErrNoSubConnAvailable)
	return bal
}

详解 p2cPicker 负载均衡器

1. 获取目标服务节点信息封装为subConn

  1. go-zero中默认使用p2cPicker负载均衡器,要先了解p2cPicker与内部的subConn
  1. p2cPicker中有一个conns属性,是一个subConn类型数组,表示可用的子连接列表,每个子连接中包含了地址,权重,状态等信息也就是每个节点的详细信息
  2. 这里又来到了服务发现时的逻辑
type p2cPicker struct {
	//p2cPicker 实现了balancer.Picker 接口,conns保存了服务的所有节点信息
	conns []*subConn
	//随机数生成器,用来从子连接列表中随机选择两个候选者
	r     *rand.Rand
	//表示原子时长,用来记录上一次更新子连接列表的时间戳
	stamp *syncx.AtomicDuration
	//保证对子连接列表的并发访问和修改的安全性
	lock  sync.Mutex
}

type subConn struct {
	//记录子连接的延迟,单位是纳秒,延迟是指从发送请求到接收响应的时间差,延迟越低,表示子连接的性能越好
	lag      uint64
	//记录子连接的并发请求数,当前正在处理的请求的数量,并发请求数越高,表示当前子连接负载越重
	inflight int64
	//记录当前子连接的成功请求数
	success  uint64
	//表示子连接的总请求数,是指发送过的请求的数量,总请求数越高,表示子连接的活跃度越高
	requests int64
	//表示子连接的最后请求时间,单位是纳秒,也就是最后一次发送请求的时间戳,最后请求时间越近,表示子连接的可用性越高
	last     int64
	//表示子连接被选择次数,指被负载均衡选择器选择过的次数,次数越高,表示子连接的热度越高
	pick     int64
	//表示子连接的地址,包含了IP、端口、元数据等信息	
	addr     resolver.Address
	//表示子连接的连接对象,用来发送和接收请求
	conn     balancer.SubConn
}
  1. 再复习一下go-zero服务发现底层, 在编写go-zero服务消费方时,内部提供了对应各种服务发现模式的Builder,通过一个init()函数将这些Builder保存到了一个Map中.在调用zrpc下的MustNewClient()函数创建一个客户端,该函数中会调用一个NewClient(),解析target协议,注册拦截器,最终会调用到grpc下的DialContext()函数,在该函数中
  1. 先封装了一个grpc下的ClientConn结构体变量,该结构体表示一个客户端和服务端之间的连接,封装了连接的创建,管理,状态,配置,拦截器等功能
  2. 执行chainUnaryClientInterceptors() 与 chainStreamClientInterceptors()函数处理拦截器,将拦截器串联起来
  3. 调用parseTargetAndFindResolver()会再次解析target协议,将target解析成一个Target 结构体变量,该结构体中存在一个Scheme属性,内部存储的就是当前rpc服务发现模式,通过这个Scheme在m中获取到对应的Builder,在使用etcd作为注册中心时获取到的就是etcdBuilder
  4. 执行newCCBalancerWrapper()创建负载均衡器包装器ccResolverWrapper
  5. 执行newCCResolverWrapper(),在该函数中通过前面拿到的Builder,在使用etcd时也就是etcdBuilder,执行他的Builder()方法,获取resolverWrapper, resolverWrapper实现了resolver.ClientConn,通过resolver.ClientConn实现服务地址的更新
  1. 在etcdBuilder的Builder()方法内部封装了一个update()更新函数,查看封装的update()更新函数,内部会调用ccResolverWrapper上的UpdateState()方法,通过该方法,最终会调用到baseBalancer的UpdateClientConnState()方法,该方法中根据目标服务key,在注册中心获取目标服务的所有节点信息,封装SubConn,存储到负载均衡器上,以etcd作为注册中心时基于watch机制,在etcd上保存的目标服务KV发生变动时都会触发到这个函数更新本地的目标服务列表信息封装为SubConn,然后将目标服务节点信息SubConn保存到负载均衡器中,当前也就是p2cPicker的conns中,(在服务启动时也是执行baseBalancer的UpdateClientConnState()获取目标服务节点信息,封装到p2cPicker的conns中)
    go 进阶 go-zero相关: 六. 负载均衡与请求的发出_第1张图片

2. p2cPicker的执行相关

  1. p2cPicker的执行
  1. 项目启动时会先创建PickerBuilder的Bulid,将负载均衡器封装为Builder保存到一个map中,在负载均衡器初始化时会执行PickerBuilder的Build()方法,创建指定的负载均衡器,获取到所有节点信息,封装为 subConn结构,保存到p2cPicker的conns属性中,后续例如发起调用时会根据指定标识,例如负载均衡器名称获取到指定负载均衡器,执行Picker中的Pick()方法进行负载均衡拿到指定的服务地址进行调用
  2. 服务发起rpc调用时,需要获取目标服务的客户端,通过目标服务客户端调用指定接口,最终会执行到proto生成的invoke()
  3. 执行到ClientConn的invoke(),在invoke()中会调用getTransport()方法,获取Transport
  4. 在getTransport()方法中会执行pickerWrapper下的pick–>默认情况下执行p2cPicker下的
//gRPC在节点有更新的时候会调用 Build 方法
//传入所有节点信息,把每个节点信息用 subConn 结构保存起来
//并归并到一起用 p2cPicker 结构保存起来
func (b *p2cPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
	......
	var conns []*subConn
	for conn, connInfo := range readySCs {
		conns = append(conns, &subConn{
			addr:    connInfo.Address,
			conn:    conn,
			success: initSuccess,
		})
	}
	return &p2cPicker{
		conns: conns,
		r:     rand.New(rand.NewSource(time.Now().UnixNano())),
		stamp: syncx.NewAtomicDuration(),
	}
}

//选择指定节点返回PickResult
func (p *p2cPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
	p.lock.Lock()
	defer p.lock.Unlock()

	var chosen *subConn
	switch len(p.conns) {
	case 0: //没有节点,返回错误
		return emptyPickResult, balancer.ErrNoSubConnAvailable
	case 1: //有一个节点,直接返回这个节点
		chosen = p.choose(p.conns[0], nil)
	case 2: //有两个节点,计算负载,返回负载低的节点
		chosen = p.choose(p.conns[0], p.conns[1])
	default: //有多个节点,通过p2c 挑选两个节点,比较这两个节点的负载,返回负载低的节点
		var node1, node2 *subConn
		//3次随机选择两个节点
		for i := 0; i < pickTimes; i++ {
			a := p.r.Intn(len(p.conns))
			b := p.r.Intn(len(p.conns) - 1)
			if b >= a {
				b++
			}
			node1 = p.conns[a]
			node2 = p.conns[b]
			//如果这次选择的节点达到了健康要求, 就中断选择
			if node1.healthy() && node2.healthy() {
				break
			}
		}
		//比较两个节点的负载情况,选择负载低的
		chosen = p.choose(node1, node2)
	}

	atomic.AddInt64(&chosen.inflight, 1)
	atomic.AddInt64(&chosen.requests, 1)
	
	//首先会将节点信息封装到SubConn中
	//然后将SubConn封装为PickResult返回
	return balancer.PickResult{
		SubConn: chosen.conn,
		Done:    p.buildDoneFunc(chosen),
	}, nil
}

//请求结束,更新节点的 EWMA 等信息
//把节点正在处理请求的总数减 1
//保存处理请求结束的时间点,用于计算距离上次节点处理请求的差值,并算出 EWMA 中的 β 值
//计算本次请求耗时,并计算出 EWMA值 保存到节点的 lag 属性里
//计算节点的健康状态保存到节点的 success 属性中
func (p *p2cPicker) buildDoneFunc(c *subConn) func(info balancer.DoneInfo) {
	start := int64(timex.Now())
	return func(info balancer.DoneInfo) {
		//正在处理的请求数减 1
		atomic.AddInt64(&c.inflight, -1)
		now := timex.Now()
		//保存本次请求结束时的时间点,并取出上次请求时的时间点
		last := atomic.SwapInt64(&c.last, int64(now))
		td := int64(now) - last
		if td < 0 {
			td = 0
		}
		//用牛顿冷却定律中的衰减函数模型计算EWMA算法中的β值
		w := math.Exp(float64(-td) / float64(decayTime))
		//保存本次请求的耗时
		lag := int64(now) - start
		if lag < 0 {
			lag = 0
		}
		olag := atomic.LoadUint64(&c.lag)
		if olag == 0 {
			w = 0
		}
		//计算 EWMA 值
		atomic.StoreUint64(&c.lag, uint64(float64(olag)*w+float64(lag)*(1-w)))
		success := initSuccess
		if info.Err != nil && !codes.Acceptable(info.Err) {
			success = 0
		}
		osucc := atomic.LoadUint64(&c.success)
		atomic.StoreUint64(&c.success, uint64(float64(osucc)*w+float64(success)*(1-w)))

		stamp := p.stamp.Load()
		if now-stamp >= logInterval {
			if p.stamp.CompareAndSwap(stamp, now) {
				p.logStats()
			}
		}
	}
}

//选择比较节点方法
func (p *p2cPicker) choose(c1, c2 *subConn) *subConn {
	start := int64(timex.Now())
	if c2 == nil {
		atomic.StoreInt64(&c1.pick, start)
		return c1
	}

	if c1.load() > c2.load() {
		c1, c2 = c2, c1
	}

	pick := atomic.LoadInt64(&c2.pick)
	if start-pick > forcePick && atomic.CompareAndSwapInt64(&c2.pick, pick, start) {
		return c2
	}

	atomic.StoreInt64(&c1.pick, start)
	return c1
}

func (p *p2cPicker) logStats() {
	var stats []string

	p.lock.Lock()
	defer p.lock.Unlock()

	for _, conn := range p.conns {
		stats = append(stats, fmt.Sprintf("conn: %s, load: %d, reqs: %d",
			conn.addr.Addr, conn.load(), atomic.SwapInt64(&conn.requests, 0)))
	}

	logx.Statf("p2c - %s", strings.Join(stats, "; "))
}
  1. 在调用p2cPicker的pick()方法中首先会将节点封装为SubConn, 会执行p2cPicker下的choose()选择指定节点,在choose()方法内部会调用subConn的load()方法计算节点的负载情况,选择最低的一个,查看该方法

计算负载的公式是: load = ewma * inflight;
ewma 相当于平均请求耗时,inflight 是当前节点正在处理请求的数量,相乘大致计算出了当前节点的网络负载

func (c *subConn) load() int64 {
	// 通过 EWMA 计算节点的负载情况; 加 1 是为了避免为 0 的情况
	lag := int64(math.Sqrt(float64(atomic.LoadUint64(&c.lag) + 1)))
	load := lag * (atomic.LoadInt64(&c.inflight) + 1)
	if load == 0 {
		return penalty
	}
	return load
}

参考博客

二. go-zero中是怎么执行负载均衡发出请求的

  1. 下方是一个基于proto生成的go-zero grpc示例
import (
	"context"
	"flag"
	"github.com/zeromicro/go-zero/core/discov"
	"github.com/zeromicro/go-zero/zrpc"
	"go_cloud_demo/rpc/types/user"
	"log"
)

func main() {
	//1.创建zrpc客户端
	client := zrpc.MustNewClient(zrpc.RpcClientConf{
		Etcd: discov.EtcdConf{
			//etcd连接地址
			Hosts: []string{"127.0.0.1:2379"},
			//目标服务key
			Key: "user.rpc",
		},
	})

	//2.获取连接
	conn := client.Conn()

	//3.使用proto生成的模板文件,创建服务client客户端
	UserClient := user.NewUserClient(conn)
	//4.调用指定接口接收响应
	resp, err := UserClient.Auth(context.Background(), &user.UserAuthReq{Token: "数据"})
	if err != nil {
		log.Fatal(err)
	}
	log.Println(resp.Password)
}
  1. 注意,使用go-zero grpc时,可以基于go-zero整合proto生成的模板进行开发如下图,使用"/type/xxx/* “下生成的方法函数,也可以使用proto生成的原生模板,如下图使用”/userclient"下的即可(因为go-zero本来就是集成proto实现的),当前基于go-zero整合proto生成的模板进行演示
    go 进阶 go-zero相关: 六. 负载均衡与请求的发出_第2张图片
  2. 解释上方代码:
  1. 配置连接的注册中心地址,比如Etcd.Hosta,目标服务key等,调用MustLoad()函数读取配置文件到一个Config结构体变量上,需要调用zrpc下的MustNewClient()函数创建zrpc客户端
  2. 调用客户端的Conn()方法获取连接
  3. 在go-zero基于proto实现rpc时,当前服务中针对每个目标服务都会生成一个对应目标服务的Client客户端,调用对应的NewXxxxClient()函数创建目标服务客户端,例如当前的UserClient
  4. 目标服务中提供的接口都绑定在这个目标服务客户端上,通过目标服务客户端,调用对应的接口例如当前的Auth()接口
  1. 接下来我们看一下在对目标服务接口发起调用时,以上方的Auth()为例(proto生成的,当前是在user_grpc.pb.go文件中),会发现在内部主要通过执行ClientConn上的Invoke()方法实现实现
func (c *userClient) Auth(ctx context.Context, in *UserAuthReq, opts ...grpc.CallOption) (*UserAuthResp, error) {
	out := new(UserAuthResp)
	//调用ClientConn的Invoke()方法
	err := c.cc.Invoke(ctx, "/template.User/Auth", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}
  1. 先看一下go-zero 万总的请求发出流程图
    go 进阶 go-zero相关: 六. 负载均衡与请求的发出_第3张图片
  2. 构建zrpc服务时首先调用zrpc下的MustNewClient()函数创建zrpc客户端,调用客户端上的conn()方法获取到连接,会返回一个ClientConn结构体变量
//ClientConn结构体主要是用来表示一个客户端和服务端之间的连接,它封装了连接的创建,管理,状态,配置,拦截器等功能
type ClientConn struct {
	ctx                 context.Context              // 客户端连接的上下文,用于取消或超时
	cancel              context.CancelFunc           // 用于取消客户端连接的函数
	target              string                       // 客户端连接的目标地址,可以是dns或etcd等
	parsedTarget        resolver.Target              // 对target解析后的目标地址,包含scheme, authority, endpoint等信息
	authority           string                       // 客户端连接的授权信息,用于验证服务端身份 
	dopts               dialOptions                  // 客户端连接的拨号选项,包含各种配置参数和拦截器 
	channelzID          *channelz.Identifier         // 客户端连接的通道标识符,用于监控和调试 
	balancerWrapper     *ccBalancerWrapper           // 客户端连接的负载均衡器包装器,用于管理多个子连接和选择器 
	csMgr               *connectivityStateManager    // 客户端连接的状态管理器,用于跟踪和更新连接状态 
	blockingpicker      *pickerWrapper               // 客户端连接的阻塞选择器,用于在非阻塞模式下选择一个子连接 
	safeConfigSelector  iresolver.SafeConfigSelector // 客户端连接的安全配置选择器,用于从服务配置中选择一个子集 
	czData              *channelzData                // 客户端连接的通道数据,用于记录通道的统计信息 
	retryThrottler      atomic.Value                 // 客户端连接的重试节流器,用于控制重试频率和开关 
	firstResolveEvent   *grpcsync.Event              // 客户端连接的首次解析事件,用于标记是否已经完成首次解析 
	mu                  sync.RWMutex                 // 用于保护客户端连接的读写锁 
	resolverWrapper     *ccResolverWrapper           // 客户端连接的解析器包装器,用于监听目标地址变化和更新服务配置 
	sc                  *ServiceConfig               // 客户端连接的服务配置,包含负载均衡策略和方法配置等信息 
	conns               map[*addrConn]struct{}       // 客户端连接管理的子连接集合,以地址为键 
	mkp                 keepalive.ClientParameters   // 客户端连接的保活参数,包含超时时间和间隔时间等信息 
	lceMu               sync.Mutex                   // 用于保护客户端连接最后一次错误信息的互斥锁 
	lastConnectionError error                        // 客户端连接最后一次错误信息,用于记录拨号失败或断开原因 
}
  1. 查看 ClientConn 下的Invoke()方法,该方法内
  1. 调用newClientStream()创建http流
  2. 调用SendMsg()发送请求
  3. 调用RecvMsg()接收响应
  4. 当前只关注newClientStream()
func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error {
	// allow interceptor to see all applicable call options, which means those
	// configured as defaults from dial option as well as per-call options
	opts = combine(cc.dopts.callOptions, opts)

	if cc.dopts.unaryInt != nil {
		return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
	}
	//调用下方的invoke()函数
	return invoke(ctx, method, args, reply, cc, opts...)
}

func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error {
	//1.创建http2流
	cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)
	if err != nil {
		return err
	}
	//2.发出请求
	if err := cs.SendMsg(req); err != nil {
		return err
	}
	//3.接收响应
	return cs.RecvMsg(reply)
}
  1. 查看newClientStream()源码,内部调用newClientStreamWithParams()
func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) {
	
	//....省略...

	var newStream = func(ctx context.Context, done func()) (iresolver.ClientStream, error) {
		//当前关注这个函数(负载均衡的逻辑就在这个函数中)
		return newClientStreamWithParams(ctx, desc, cc, method, mc, onCommit, done, opts...)
	}
	
	//组装rpcInfo,配置信息
	rpcInfo := iresolver.RPCInfo{Context: ctx, Method: method}
	rpcConfig, err := cc.safeConfigSelector.SelectConfig(rpcInfo)
	if err != nil {
		return nil, toRPCErr(err)
	}

	if rpcConfig != nil {
		//....省略...
		if rpcConfig.Interceptor != nil {
			rpcInfo.Context = nil
			ns := newStream
			//创建http2流组装http2 header
			newStream = func(ctx context.Context, done func()) (iresolver.ClientStream, error) {
				cs, err := rpcConfig.Interceptor.NewStream(ctx, rpcInfo, done, ns)
				if err != nil {
					return nil, toRPCErr(err)
				}
				return cs, nil
			}
		}
	}
	//创建http2流组装http2 header
	return newStream(ctx, func() {})
}
  1. 在newClientStreamWithParams()函数中会封装一个op函数,在op函数内部存在一个csAttempt上的getTransport()方法,获取Transport
func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, mc serviceconfig.MethodConfig, onCommit, doneFunc func(), opts ...CallOption) (_ iresolver.ClientStream, err error) {

	//....省略...

	op := func(a *csAttempt) error {
		//重点关注csAttempt上的getTransport()方法,获取Transport
		if err := a.getTransport(); err != nil {
			return err
		}
		if err := a.newStream(); err != nil {
			return err
		}
		
		cs.attempt = a
		return nil
	}
	if err := cs.withRetry(op, func() { cs.bufferForRetryLocked(0, op) }); err != nil {
		return nil, err
	}

	//....省略...
	
	return cs, nil
}
  1. 查看csAttempt 的getTransport()方法
type csAttempt struct {
	ctx  context.Context
	cs   *clientStream
	t    transport.ClientTransport
	s    *transport.Stream
	p    *parser
	done func(balancer.DoneInfo)

	finished  bool
	dc        Decompressor
	decomp    encoding.Compressor
	decompSet bool

	mu sync.Mutex // guards trInfo.tr
	trInfo *traceInfo

	statsHandlers []stats.Handler
	beginTime     time.Time
	allowTransparentRetry bool
	drop bool
}

func (a *csAttempt) getTransport() error {
	cs := a.cs

	var err error
	//该方法内部会调用的ClientConn下的getTransport()
	a.t, a.done, err = cs.cc.getTransport(a.ctx, cs.callInfo.failFast, cs.callHdr.Method)
	if err != nil {
		if de, ok := err.(dropError); ok {
			err = de.error
			a.drop = true
		}
		return err
	}
	if a.trInfo != nil {
		a.trInfo.firstLine.SetRemoteAddr(a.t.RemoteAddr())
	}
	return nil
}
  1. 最终会调用到ClientConn下的getTransport(),调用到负载均衡相关的pickerWrapper下的pick()
func (cc *ClientConn) getTransport(ctx context.Context, failfast bool, method string) (transport.ClientTransport, func(balancer.DoneInfo), error) {
	//调用到负载均衡相关的pickerWrapper下的pick()
	return cc.blockingpicker.pick(ctx, failfast, balancer.PickInfo{
		Ctx:            ctx,
		FullMethodName: method,
	})
}
  1. 查看pickerWrapper下的pick()方法
func (pw *pickerWrapper) pick(ctx context.Context, failfast bool, info balancer.PickInfo) (transport.ClientTransport, func(balancer.DoneInfo), error) {
	var ch chan struct{}
	var lastPickErr error
	for {
		pw.mu.Lock()
		
		//....省略...
		
		ch = pw.blockingCh
		//获取到负载均衡的对象,默认是p2cPicker
		p := pw.picker
		pw.mu.Unlock()
		
		//调用p2cPicker下的Pick()选择指定节点封装为pickResult返回
		pickResult, err := p.Pick(info)

		//....省略...
	}
}
  1. 自此执行了go-zero默认提供的p2cPicker下的Pick()方法,获取到pickResult也就是选中的节点
    go 进阶 go-zero相关: 六. 负载均衡与请求的发出_第4张图片
  2. 忽略其他逻辑,然后调用SendMsg()发送请求,调用RecvMsg()接收响应

三. 自定义负载均衡策略

package balance

import (
	"fmt"
	"google.golang.org/grpc/balancer"
	"google.golang.org/grpc/balancer/base"
	"google.golang.org/grpc/resolver"
	"log"
	"math"
	"math/rand"
	"sync"
	"sync/atomic"
	"time"
)

// 本包是客户端负载均衡器,采用的算法是 p2c+ewma
// p2c 是 二选一
// ewma 指数移动加权平均值(体现一段时间内的平均值)
const (
	BalancerName    = "p2c_ewma"
	forcePick       = int64(time.Second) // 默认上次被选择的间隔时间
	initSuccess     = 1000
	throttleSuccess = initSuccess / 2
	decayTime       = int64(time.Second * 10)
)

var initTime = time.Now().AddDate(-1, -1, -1)

func Now() time.Duration {
	return time.Since(initTime)
}

// 1.首先实现 grpc/balancer/base.PickerBuilder接口
type p2cEwmaPickerBuilder struct{}

// 2.实现PickerBuilder接口的Build方法,
// 服务启动时会获取目标服务节点信息封装为svrConn
// 最终将目标服务节点信息保存到picker中
func (b *p2cEwmaPickerBuilder) Build(buildInfo base.PickerBuildInfo) balancer.Picker {
	log.Println("start p2c build")
	if len(buildInfo.ReadySCs) == 0 {
		return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
	}
	//保存所有的连接
	var allConn []*svrConn
	for k, v := range buildInfo.ReadySCs {
		allConn = append(allConn, &svrConn{
			addr:    v.Address,
			conn:    k,
			success: initSuccess,
		})
	}

	return &picker{
		conns: allConn,
		rand:  rand.New(rand.NewSource(time.Now().UnixNano())),
	}
}

// 3.定义用来保存目标服务节点信息的结构体,保存所有的连接
type svrConn struct {
	addr     resolver.Address
	conn     balancer.SubConn
	lag      uint64 // 用来保存 ewma 值
	inflight int64  // 用在保存当前正在使用此连接的请求总数
	success  uint64 // 用来标识一段时间内此连接的健康状态
	requests int64  // 用来保存请求总数
	last     int64  // 用来保存上一次请求耗时, 计算 ewma 值
	pick     int64  // 保存上一次被选中的时间点
}

// 4.保存所有svrConn计算负载率
func (s *svrConn) load() int64 {
	// 获取这个服务的 ewma, 加 1 的目的是为了防止lag等于0
	lag := int64(math.Sqrt(float64(atomic.LoadUint64(&s.lag) + 1)))
	// 获取是不是正在运行
	load := lag * (atomic.LoadInt64(&s.inflight) + 1)

	if load == 0 {
		return 1<<31 - 1
	}
	return load
}

func (s *svrConn) healthy() bool {
	return atomic.LoadUint64(&s.success) > throttleSuccess
}

// 5.实现Picker接口
type picker struct {
	conns []*svrConn
	rand  *rand.Rand
	lock  sync.Mutex
}

// 6.实现Picker中的 Pick()方法
// 在请求目标服务时,会执行该方法,通过该方法负载均衡,
// 选择指定的服务节点封装为balancer.PickResult返回
func (p *picker) Pick(info balancer.PickInfo) (result balancer.PickResult, err error) {
	log.Println("=======执行负载均衡策略========")
	p.lock.Lock()
	defer p.lock.Unlock()
	var chosen *svrConn
	switch len(p.conns) {
	case 0:
		return result, balancer.ErrNoSubConnAvailable
	case 1:
		chosen = p.choose(p.conns[0], nil)
	case 2:
		chosen = p.choose(p.conns[0], p.conns[1])
	default:
		var node1, node2 *svrConn
		for i := 0; i < 3; i++ {
			a := p.rand.Intn(len(p.conns))
			b := p.rand.Intn(len(p.conns) - 1)
			if b > a { // 说明选择的范围比较小
				b++
			}
			node1 = p.conns[a]
			node2 = p.conns[b]
			if node1.healthy() && node2.healthy() { // 说明上次成功请求耗时特别短, 优先选择这两个
				break
			}
		}

		chosen = p.choose(node1, node2)
	}
	// 正在处理请求数+1
	atomic.AddInt64(&chosen.inflight, 1)
	// 处理的总请求数+1
	atomic.AddInt64(&chosen.requests, 1)
	res := balancer.PickResult{
		SubConn: chosen.conn,
		//设置接目标接口调用完毕后统计函数
		Done: p.buildDoneFunc(chosen),
	}
	log.Println("pick server add : ", chosen.addr.Addr)
	return res, nil
}

// 7.编写多节点选择方法
func (p *picker) choose(c1, c2 *svrConn) *svrConn {
	start := int64(Now())
	if c2 == nil {
		atomic.StoreInt64(&c1.pick, start)
		return c1
	}

	//比较
	if c1.load() > c2.load() {
		c1, c2 = c2, c1
	}

	pick := atomic.LoadInt64(&c2.pick)
	if start-pick > forcePick && atomic.CompareAndSwapInt64(&c2.pick, pick, start) {
		return c2
	}
	atomic.StoreInt64(&c1.pick, start)
	return c1
}

// 8.编写统计函数,服务调用完毕后会执行该函数,供下次负载均衡时计算使用
func (p *picker) buildDoneFunc(s *svrConn) func(info balancer.DoneInfo) {
	fmt.Println("====接口调用完毕,记录=====")
	start := int64(Now())
	return func(info balancer.DoneInfo) {
		// 执行完了 把正在执行的总数减 1
		atomic.AddInt64(&s.inflight, -1)
		now := Now()
		// 存储当前执行完的时间节点
		last := atomic.SwapInt64(&s.last, int64(now))
		// 上一次请求结束距离这次请求结束的时间差
		td := int64(now) - last
		if td < 0 {
			td = 0
		}
		// 这个计算公式是 牛顿定律中的衰减函数模型
		w := math.Exp(float64(-td) / float64(decayTime))
		lag := int64(now) - start
		if lag < 0 { // 请求没有花费时间就执行完了,按理是不可能的
			lag = 0
		}
		olag := atomic.LoadUint64(&s.lag)
		if olag == 0 {
			w = 0
		}
		atomic.StoreUint64(&s.lag, uint64(float64(olag)*w+float64(lag)*(1-w)))
		success := 1000
		if info.Err != nil { // 如果有失败这次的值作废
			success = 0
		}

		osucc := atomic.LoadUint64(&s.success)
		// 存储成功的 ewma
		atomic.StoreUint64(&s.success, uint64(float64(osucc)*w+float64(success)*(1-w)))
	}
}

// 9. 设置使用当前自定义负载均衡策略
func newBuilder() balancer.Builder {
	return base.NewBalancerBuilder(BalancerName, new(p2cEwmaPickerBuilder), base.Config{HealthCheck: true})
}

// 10.启动服务时调用该函数,初始化
func NewBalanceEwma() {
	fmt.Println("初始化自定义负载均衡")
	balancer.Register(newBuilder())
}

四. 总结

  1. 首先go-zero中默认采用P2C+EWMA负责均衡算法
  1. P2C: 随机从所有可用节点中选择两个节点,然后计算这两个节点的负载情况,选择负载较低的一个节点来服务本次请求。为了避免某些节点一直得不到选择导致不平衡,会在超过一定的时间后强制选择一次
  2. EWMA指数移动加权平均的算法,表示是一段时间内的均值。该算法相对于算数平均来说对于突然的网络抖动没有那么敏感,突然的抖动不会体现在请求的lag中,从而可以让算法更加均衡
  1. 了解go-zero负载均衡首先要了解gprc下的几个接口
  1. gprc中针对负载均衡提供了Picker负载均衡器接口,该接口内部有一个Pick()方法,这个就是负载均衡算法
  2. grpc中针对负载均衡提供了用于创建Picker负载均衡器的接口PickerBuilder,该接口内部有一个Build方法,通过这个Bulid()方法获取服务节点的地址列表,负载均衡策略类型创建指定的Picker负载均衡器并返回
  3. grpc针对负载均衡提供了一个Builder接口在grpc的balancer包下,用于封装注册保存各种负载均衡器,
  4. go-zero中目前支持roundrobin轮询、random随机、consistenthash一致性哈)和p2c最小负载四种策略,默认使用p2c,针对p2c提供了p2cPickerBuilder与p2cPicker
  1. go-zero负载均衡可以在负载均衡器的注册, 负载均衡器的初始化获取,服务发现拉取服务提供方地址三个角度分析
  2. 负载均衡器的注册: github.com\zeromicro\go-zero\zrpc\internal\balancer\p2c\p2c.go文件中通过一个init()初始函数,函数内调用grpc包下的Register()函数,结合grpc创建了一个p2cPickerBuilder负载均衡器,封装为balancer下的Builder类型默认是baseBuilder,然后以负载均衡器名称默认是"p2c_ewma"为key,Builder为value存储到了一个map中
  3. 运行时负载均衡器的初始化获取, 这里要结合go-zero服务端启动去看,在编写go-zero服务消费方时需要调用zrpc下的MustNewClient()函数创建一个客户端,该函数中会调用一个NewClient(),解析target协议,注册拦截器,最终会调用到grpc下的DialContext()函数,在该函数中,重点关注调用了newCCBalancerWrapper()创建负载均衡器包装器ccResolverWrapper,启动协程调用了ccResolverWrapper的watcher(),在watcher()方法中开启了一个无限for循环,内部通过select_case监听updateCh通道上的消息,根据监听到的不同消息对负载均衡器进行指定更新或设置,我们这里重点关注一下updateCh返回switchToUpdate时的处理:
  1. 当前服务消费方设置了负载均衡策略时,例如设置p2c负载均衡器,updateCh通道会返回switchToUpdate
  2. 当前服务消费方没有指定负载均衡策略,但是有多个可用的子连接,这时会创建一个默认的负载均衡器p2c,updateCh通道会返回switchToUpdate
  3. 当前服务消费方配置发生了变化,例如服务名,负载均衡策略,服务发现方式等,updateCh通道也会返回switchToUpdate
  4. 在updateCh通道会返回switchToUpdate时,会执行ccBalancerWrapper上的handleSwitchTo()方法
  1. 查看ccBalancerWrapper上的handleSwitchTo()方法
  1. 通过name也就是"p2c_ewma"在一个map中获取到指定的负载均衡器,也就是前面通过一个init()初始化包装了p2cPickerBuilder的baseBuilder
  2. 如果获取到了执行SwitchTo()方法,该方法中会执行baseBuilder的Bulid()方法封装baseBalancer,自此负载均衡器创建初始化完毕
  1. 以p2cPicker 为例说一下怎么获取到目标服务地址,请求是怎么发出的,先看一下p2cPicker这个结构体内部有一个conns属性,是一个subConn类型数组,表示可用的子连接列表,每个子连接中包含了地址,权重,状态等信息也就是每个节点的详细信息,这里又来到了服务发现时的逻辑, 在编写go-zero服务消费方时内部提供了对应各种服务发现模式的Builder,通过一个init()函数将这些Builder保存到了一个Map中,要调用zrpc下的MustNewClient()函数创建一个客户端,该函数中会调用一个NewClient(),解析target协议,注册拦截器,最终会调用到grpc下的DialContext()函数,在该函数中重点关注:
  1. 调用parseTargetAndFindResolver()会再次解析target协议,将target解析成一个Target 结构体变量,该结构体中存在一个Scheme属性,内部存储的就是当前rpc服务发现模式,通过这个Scheme在m中获取到对应的Builder,在使用etcd作为注册中心时获取到的就是etcdBuilder
  2. 然后就是调用newCCBalancerWrapper()初始化创建负载均衡器,拿到包装器ccResolverWrapper
  3. 执行newCCResolverWrapper(),在该函数中通过前面拿到的Builder,在使用etcd时也就是etcdBuilder,执行他的Builder()方法,获取resolverWrapper, resolverWrapper实现了resolver.ClientConn,通过resolver.ClientConn实现服务地址的更新, 重点就是etcdBuilder的Builder()方法中
  1. 在etcdBuilder的Builder()方法内部封装了一个update()更新函数,查看封装的update()更新函数,内部会调用ccResolverWrapper上的UpdateState()方法,通过该方法,最终会调用到baseBalancer的UpdateClientConnState()方法,该方法中根据目标服务key,在注册中心获取目标服务的所有节点信息,封装SubConn,存储到负载均衡器上,以etcd作为注册中心时基于watch机制,在etcd上保存的目标服务KV发生变动时都会触发到这个函数更新本地的目标服务列表信息封装为SubConn,然后将目标服务节点信息SubConn保存到负载均衡器中,当前也就是p2cPicker的conns中,(在服务启动时也是执行baseBalancer的UpdateClientConnState()获取目标服务节点信息,封装到p2cPicker的conns中)
  2. go-zero中负载均衡到请求发出,在提供go-zero zrpc服务消费方时,首先要通过zrpc的MustNewClient()函数创建客户端,调用Client的Conn()方法获取到连接,在这个Conn连接结构体变量中存储了一些服务配置等信息,然后调用哪个目标服务,创建对应服务的Client,通过对应服务的客户端,对目标服务中提供的接口发起调用,查看通过目标服务Client调用目标服务接口的代码,发每个对目标服务接口发起调用的函数中都会执行一个 ClientConn 下的Invoke()方法,该方法内
  1. 调用newClientStream()创建http流
  2. 调用SendMsg()发送请求
  3. 调用RecvMsg()接收响应
  4. 当前只关注newClientStream()
  1. 查看newClientStream()源码,内部调用newClientStreamWithParams(),函数中会封装一个op函数,在op函数内部封装了一个csAttempt上的getTransport()方法,最终会调用到ClientConn下的getTransport(),调用到负载均衡相关的pickerWrapper下的pick()–>通过pickerWrapper获取到对应的负载均衡器默认就是p2cPicker,执行负载均衡器的Pick()负载均衡,拿到指定的节点,然后发起调用

你可能感兴趣的:(#,十四.,负载均衡,golang,java)