- 随机
- 轮询: 通过取模算法举例:假设有n台机器,n取模上次调用的第几台机器,得出的就是当前需要调用的
- 加权: 考虑到不同机器负载能力不同,给不同机器设置不同的权重,让性能更高的承载更多的请求
- P2C: go-zero中默认采用的负载均衡算法
- P2C: 随机从所有可用节点中选择两个节点,然后计算这两个节点的负载情况,选择负载较低的一个节点来服务本次请求。为了避免某些节点一直得不到选择导致不平衡,会在超过一定的时间后强制选择一次
- EWMA指数移动加权平均的算法,表示是一段时间内的均值。该算法相对于算数平均来说对于突然的网络抖动没有那么敏感,突然的抖动不会体现在请求的lag中,从而可以让算法更加均衡
- gprc中针对负载均衡提供了Picker负载均衡器接口,该接口内部有一个Pick()方法,这个就是负载均衡算法
- grpc中针对负载均衡提供了用于创建Picker负载均衡器的接口PickerBuilder,该接口内部有一个Build方法,通过这个Bulid()方法获取服务节点的地址列表,负载均衡策略类型创建指定的Picker负载均衡器并返回
- grpc针对负载均衡提供了一个Builder接口在grpc的balancer包下,用于封装注册保存各种负载均衡器,
- 项目启动时会先创建PickerBuilder的Bulid,将负载均衡器封装为Builder保存到一个map中,在负载均衡器初始化时会执行PickerBuilder的Build()方法,创建指定的负载均衡器,后续例如发起调用时会根据指定标识,例如负载均衡器名称获取到指定负载均衡器,执行Picker中的Pick()方法进行负载均衡拿到指定的服务地址进行调用,所以想要实现负载均衡,要实现Picker接口,实现Picker接口中的Pick()负载均衡算法,要实现PickerBuilder接口,实现PickerBuilder接口中封创建指定负载均衡器的Bulid()方法
- 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)
}
- 这里内部调用了grpc下的函数,可能要结合grpc去看,
- 会调用grpc下的NewBalancerBuilder()函数,封装对应grpc负载均衡器的结构变量balancer.Builder,将这个Bulider存储到map中,实现负载均衡器的注册
- 默认情况下会以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,
}
}
- 先封装了一个grpc下的ClientConn结构体变量,该结构体表示一个客户端和服务端之间的连接,封装了连接的创建,管理,状态,配置,拦截器等功能
- 执行chainUnaryClientInterceptors() 与 chainStreamClientInterceptors()函数处理拦截器,将拦截器串联起来
- 调用parseTargetAndFindResolver()会再次解析target协议,将target解析成一个Target 结构体变量,该结构体中存在一个Scheme属性,内部存储的就是当前rpc服务发现模式,通过这个Scheme在m中获取到对应的Builder,在使用etcd作为注册中心时获取到的就是etcdBuilder
- 执行newCCBalancerWrapper()创建负载均衡器包装器ccResolverWrapper
- 执行newCCResolverWrapper(),在该函数中通过前面拿到的Builder,在使用etcd时也就是etcdBuilder,执行他的Builder()方法,获取resolverWrapper, resolverWrapper实现了resolver.ClientConn,通过resolver.ClientConn实现服务地址的更新
- …
- 创建了一个ccBalancerWrapper
- 启动协程调用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
}
- 当前服务消费方设置了负载均衡策略时,例如设置p2c负载均衡器,updateCh通道会返回switchToUpdate
- 当前服务消费方没有指定负载均衡策略,但是有多个可用的子连接,这时会创建一个默认的负载均衡器p2c,updateCh通道会返回switchToUpdate
- 当前服务消费方配置发生了变化,例如服务名,负载均衡策略,服务发现方式等,updateCh通道也会返回switchToUpdate
- 在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
}
}
}
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()
}
//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
}
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中有一个conns属性,是一个subConn类型数组,表示可用的子连接列表,每个子连接中包含了地址,权重,状态等信息也就是每个节点的详细信息
- 这里又来到了服务发现时的逻辑
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
}
- 先封装了一个grpc下的ClientConn结构体变量,该结构体表示一个客户端和服务端之间的连接,封装了连接的创建,管理,状态,配置,拦截器等功能
- 执行chainUnaryClientInterceptors() 与 chainStreamClientInterceptors()函数处理拦截器,将拦截器串联起来
- 调用parseTargetAndFindResolver()会再次解析target协议,将target解析成一个Target 结构体变量,该结构体中存在一个Scheme属性,内部存储的就是当前rpc服务发现模式,通过这个Scheme在m中获取到对应的Builder,在使用etcd作为注册中心时获取到的就是etcdBuilder
- 执行newCCBalancerWrapper()创建负载均衡器包装器ccResolverWrapper
- 执行newCCResolverWrapper(),在该函数中通过前面拿到的Builder,在使用etcd时也就是etcdBuilder,执行他的Builder()方法,获取resolverWrapper, resolverWrapper实现了resolver.ClientConn,通过resolver.ClientConn实现服务地址的更新
- …
- 项目启动时会先创建PickerBuilder的Bulid,将负载均衡器封装为Builder保存到一个map中,在负载均衡器初始化时会执行PickerBuilder的Build()方法,创建指定的负载均衡器,获取到所有节点信息,封装为 subConn结构,保存到p2cPicker的conns属性中,后续例如发起调用时会根据指定标识,例如负载均衡器名称获取到指定负载均衡器,执行Picker中的Pick()方法进行负载均衡拿到指定的服务地址进行调用
- 服务发起rpc调用时,需要获取目标服务的客户端,通过目标服务客户端调用指定接口,最终会执行到proto生成的invoke()
- 执行到ClientConn的invoke(),在invoke()中会调用getTransport()方法,获取Transport
- 在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, "; "))
}
计算负载的公式是: 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
}
参考博客
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)
}
- 配置连接的注册中心地址,比如Etcd.Hosta,目标服务key等,调用MustLoad()函数读取配置文件到一个Config结构体变量上,需要调用zrpc下的MustNewClient()函数创建zrpc客户端
- 调用客户端的Conn()方法获取连接
- 在go-zero基于proto实现rpc时,当前服务中针对每个目标服务都会生成一个对应目标服务的Client客户端,调用对应的NewXxxxClient()函数创建目标服务客户端,例如当前的UserClient
- 目标服务中提供的接口都绑定在这个目标服务客户端上,通过目标服务客户端,调用对应的接口例如当前的Auth()接口
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
}
//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 // 客户端连接最后一次错误信息,用于记录拨号失败或断开原因
}
- 调用newClientStream()创建http流
- 调用SendMsg()发送请求
- 调用RecvMsg()接收响应
- 当前只关注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)
}
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() {})
}
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
}
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
}
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,
})
}
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)
//....省略...
}
}
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())
}
- P2C: 随机从所有可用节点中选择两个节点,然后计算这两个节点的负载情况,选择负载较低的一个节点来服务本次请求。为了避免某些节点一直得不到选择导致不平衡,会在超过一定的时间后强制选择一次
- EWMA指数移动加权平均的算法,表示是一段时间内的均值。该算法相对于算数平均来说对于突然的网络抖动没有那么敏感,突然的抖动不会体现在请求的lag中,从而可以让算法更加均衡
- gprc中针对负载均衡提供了Picker负载均衡器接口,该接口内部有一个Pick()方法,这个就是负载均衡算法
- grpc中针对负载均衡提供了用于创建Picker负载均衡器的接口PickerBuilder,该接口内部有一个Build方法,通过这个Bulid()方法获取服务节点的地址列表,负载均衡策略类型创建指定的Picker负载均衡器并返回
- grpc针对负载均衡提供了一个Builder接口在grpc的balancer包下,用于封装注册保存各种负载均衡器,
- go-zero中目前支持roundrobin轮询、random随机、consistenthash一致性哈)和p2c最小负载四种策略,默认使用p2c,针对p2c提供了p2cPickerBuilder与p2cPicker
- 当前服务消费方设置了负载均衡策略时,例如设置p2c负载均衡器,updateCh通道会返回switchToUpdate
- 当前服务消费方没有指定负载均衡策略,但是有多个可用的子连接,这时会创建一个默认的负载均衡器p2c,updateCh通道会返回switchToUpdate
- 当前服务消费方配置发生了变化,例如服务名,负载均衡策略,服务发现方式等,updateCh通道也会返回switchToUpdate
- 在updateCh通道会返回switchToUpdate时,会执行ccBalancerWrapper上的handleSwitchTo()方法
- 通过name也就是"p2c_ewma"在一个map中获取到指定的负载均衡器,也就是前面通过一个init()初始化包装了p2cPickerBuilder的baseBuilder
- 如果获取到了执行SwitchTo()方法,该方法中会执行baseBuilder的Bulid()方法封装baseBalancer,自此负载均衡器创建初始化完毕
- 调用parseTargetAndFindResolver()会再次解析target协议,将target解析成一个Target 结构体变量,该结构体中存在一个Scheme属性,内部存储的就是当前rpc服务发现模式,通过这个Scheme在m中获取到对应的Builder,在使用etcd作为注册中心时获取到的就是etcdBuilder
- 然后就是调用newCCBalancerWrapper()初始化创建负载均衡器,拿到包装器ccResolverWrapper
- 执行newCCResolverWrapper(),在该函数中通过前面拿到的Builder,在使用etcd时也就是etcdBuilder,执行他的Builder()方法,获取resolverWrapper, resolverWrapper实现了resolver.ClientConn,通过resolver.ClientConn实现服务地址的更新, 重点就是etcdBuilder的Builder()方法中
- 调用newClientStream()创建http流
- 调用SendMsg()发送请求
- 调用RecvMsg()接收响应
- 当前只关注newClientStream()