在选择负载均衡算法时,我们希望满足以下要求:
基于这些考虑,go-zero 选择了 p2c+EWMA 算法来实现。
go-zero
p2c+EWMA
p2c (Pick Of 2 Choices) 二选一: 在多个节点中随机选择两个节点。
p2c (Pick Of 2 Choices)
go-zero 中的会随机的选择3次,如果其中一次选择的节点的健康条件满足要求,就中断选择,采用这两个节点。
EWMA (Exponentially Weighted Moving-Average) 指数移动加权平均法: 是指各数值的加权系数随时间呈指数递减,越靠近当前时刻的数值加权系数就越大,体现了最近一段时间内的平均值。
EWMA (Exponentially Weighted Moving-Average)
公式:
变量解释:
Vt
t
EWMA值
Vt-1
t-1
β
EWMA
β值
go-zero 采用的是牛顿冷却定律中的衰减函数模型计算 EWMA 算法中的 β 值:
其中 Δt 为两次请求的间隔,e,k 为常数
Δt
e
k
首先我们需要实现 google.golang.org/grpc/balancer/base/base.go/PickerBuilder 接口, 这个接口是有服务节点更新的时候会调用接口里的Build方法
google.golang.org/grpc/balancer/base/base.go/PickerBuilder
Build
type PickerBuilder interface { // Build returns a picker that will be used by gRPC to pick a SubConn. Build(info PickerBuildInfo) balancer.Picker}
还要实现 google.golang.org/grpc/balancer/balancer.go/Picker 接口。这个接口主要实现负载均衡,挑选一个节点供请求使用
google.golang.org/grpc/balancer/balancer.go/Picker
type Picker interface { Pick(info PickInfo) (PickResult, error)}
最后向负载均衡 map 中注册我们实现的负载均衡器
map
gRPC
Pick
p2c
PickResult.Done
保存服务的所有节点信息
我们需要保存节点处理本次请求的耗时、EWMA 等信息,go-zero 给每个节点设计了如下结构:
type subConn struct { addr resolver.Address conn balancer.SubConn lag uint64 // 用来保存 ewma 值 inflight int64 // 用在保存当前节点正在处理的请求总数 success uint64 // 用来标识一段时间内此连接的健康状态 requests int64 // 用来保存请求总数 last int64 // 用来保存上一次请求耗时, 用于计算 ewma 值 pick int64 // 保存上一次被选中的时间点}
p2cPicker 实现了 balancer.Picker 接口,conns 保存了服务的所有节点信息
p2cPicker
balancer.Picker
conns
type p2cPicker struct { conns []*subConn // 保存所有节点的信息 r *rand.Rand stamp *syncx.AtomicDuration lock sync.Mutex}
gRPC 在节点有更新的时候会调用 Build 方法,传入所有节点信息,我们在这里把每个节点信息用 subConn 结构保存起来。并归并到一起用 p2cPicker 结构保存起来
subConn
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(), }}
随机挑选节点信息,在这里分了三种情况:
主要实现代码如下:
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) }
load计算节点的负载情况
load
上面的 choose 方法会调用 load 方法来计算节点负载。
choose
计算负载的公式是: load = ewma * inflight
load = ewma * inflight
在这里简单解释下: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}
请求结束,更新节点的 EWMA 等信息
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() } } }}
lag
success
新书推荐:用 Gin 框架构建分布式应用
我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。
坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio