共计 4234 个字符,预计需要花费 11 分钟才能阅读完成。
背景
在抉择负载平衡算法时,咱们心愿满足以下要求:
-
具备分区和机房调度亲和性
- 每次抉择的节点尽量是负载最低的
- 每次尽可能选择响应最快的节点
-
无需人工干预故障节点
- 当一个节点有故障时,负载平衡算法能够主动隔离该节点
- 当故障节点复原时,可能主动复原对该节点的流量散发
基于这些思考,go-zero
抉择了 p2c+EWMA
算法来实现。
算法的核心思想
p2c
p2c (Pick Of 2 Choices)
二选一: 在多个节点中随机抉择两个节点。
go-zero
中的会随机的抉择 3 次,如果其中一次抉择的节点的衰弱条件满足要求,就中断抉择,采纳这两个节点。
EWMA
EWMA (Exponentially Weighted Moving-Average)
指数挪动加权平均法: 是指各数值的加权系数随工夫呈指数递加,越凑近以后时刻的数值加权系数就越大,体现了最近一段时间内的平均值。
-
公式:
-
变量解释:
Vt
: 代表的是第t
次申请的EWMA 值
Vt-1
: 代表的是第t-1
次申请的EWMA 值
β
: 是一个常量
EWMA 算法的劣势
- 相较于一般的计算平均值算法,
EWMA
不须要保留过来所有的数值,计算量显著缩小,同时也减小了存储资源。 -
传统的计算平均值算法对网络耗时不敏感, 而
EWMA
能够通过申请频繁来调节β
,进而迅速监控到网络毛刺或更多的体现整体平均值。- 当申请较为频繁时, 阐明节点网络负载升高了, 咱们想监测到此时节点解决申请的耗时 (侧面反映了节点的负载状况), 咱们就相应的调小
β
。β
越小,EWMA 值
就越靠近本次耗时,进而迅速监测到网络毛刺; - 当申请较为不频繁时, 咱们就绝对的调大
β 值
。这样计算出来的EWMA 值
越靠近平均值
- 当申请较为频繁时, 阐明节点网络负载升高了, 咱们想监测到此时节点解决申请的耗时 (侧面反映了节点的负载状况), 咱们就相应的调小
β 计算
go-zero
采纳的是牛顿冷却定律中的衰减函数模型计算 EWMA
算法中的 β
值:
其中 Δt
为两次申请的距离,e
,k
为常数
gRPC 中实现自定义负载均衡器
-
首先咱们须要实现
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
接口。这个接口次要实现负载平衡,筛选一个节点供申请应用type Picker interface {Pick(info PickInfo) (PickResult, error) }
- 最初向负载平衡
map
中注册咱们实现的负载均衡器
go-zero 实现负载平衡的次要逻辑
- 在每次节点更新,
gRPC
会调用Build
办法,此时在Build
里实现保留所有的节点信息。 gRPC
在获取节点解决申请时,会调用Pick
办法以获取节点。go-zero
在Pick
办法里实现了p2c
算法,筛选节点,并通过节点的EWMA 值
计算负载状况,返回负载低的节点供gRPC
应用。- 在申请完结的时候
gRPC
会调用PickResult.Done
办法,go-zero
在这个办法里实现了本次申请耗时等信息的存储,并计算出了EWMA 值
保留了起来,供下次申请时计算负载等状况的应用。
负载平衡代码剖析
-
保留服务的所有节点信息
咱们须要保留节点解决本次申请的耗时、
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
保留了服务的所有节点信息type p2cPicker struct {conns []*subConn // 保留所有节点的信息 r *rand.Rand stamp *syncx.AtomicDuration lock sync.Mutex }
-
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(),} }
-
随机筛选节点信息,在这里分了三种状况:
- 只有一个服务节点,此时间接返回供
gRPC
应用即可 - 有两个服务节点,通过
EWMA 值
计算负载,并返回负载低的节点返回供gRPC
应用 - 有多个服务节点,此时通过
p2c
算法选出两个节点,比拟负载状况,返回负载低的节点供gRPC
应用
次要实现代码如下:
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
计算节点的负载状况下面的
choose
办法会调用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 }
-
申请完结,更新节点的
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() } } } }
我的项目地址
https://github.com/tal-tech/go-zero
欢送应用 go-zero
并 star 反对咱们!
微信交换群
关注『微服务实际 』公众号并点击 交换群 获取社区群二维码。