前言
上一篇文章我们分析了BFE的转发模型原理, 其中不同的负载均衡算法是转发的核心构成, 本篇我们基于第一种算法: 普通加权轮询算法进行源码分析。
前期回顾: BFE转发模型
源码实现
加权轮询算法实现在bfe_balance/bal_slb/bal_rr.go:simpleBalance()
方法, 如方法名, 是一种简单的轮询算法。
func (brr *BalanceRR) simpleBalance() (*backend.BfeBackend, error) {
/*** 省略 ***/
backends := brr.backends // 获取后端服务列表
allBackendDown := true // 是否需要重置backend权重
next := brr.next // 默认从 0 index 开始遍历
for {
backendRR = backends[next] // 此变量表示当前被选择后端
backend = backendRR.backend
avail := backend.Avail()
// 如果当前后端被租户设置为提供服务并且在此轮轮询中权重>0, 则是一个有效的后端
if avail && backendRR.current > 0 {
break
}
/*** 省略 ***/
// 进行到这里表示此轮轮询中已经存在因为权重比例小而不会被选择的后端
if avail && backendRR.weight != 0 {
allBackendDown = false
}
// 移动到下一个后端, 如果当前 next 已经是末尾, 则moveToNext()计算为从index=0开始
next = moveToNext(next, backends)
// 如果经过后移选择的后端等于当前轮询的后端
if next == brr.next {
// all backends have been check
if allBackendDown {
/*** 省略 ***/
return backend, fmt.Errorf("rr_bal:all backend is down")
} else {
/*** 省略 ***/
brr.initWeight()
brr.next = 0
next = 0
}
}
}
// 将此次选择的后端当前权重值减一
backendRR.current--
// next指向当前被选择的下一个backend节点
next = moveToNext(next, backends)
brr.next = next
/*** 省略 ***/
return backend, nil
}
backend
后端结构有两个字段:
weight
:表示此后端的权重值current
:表示当前轮次的轮询中的权重值, 动态变化, 被选中一次后会减一, 初始值和weight相等 代码相对比较简单, 如果只看实现可能理解不够深刻, 基于以上代码结构我们做一个例子分析, 假设我们有三个后端b1, b2, b3
, 并且设置的初始权重如下:b1
: weight=3, current=3b2
: weight=2, current=2b3
: weight=1, current=1 我们发起7个请求, 分别为r1..r7
,那么每个请求的负载均衡选择过程如下:
请求 | 当前next | 选择的后端 | 选择后b1,b2,b3分别对应的current值 | 选择后next值 |
---|---|---|---|---|
r1 | 0 | b1 | 2 2 1 | 1 |
r2 | 1 | b2 | 2 1 1 | 2 |
r3 | 2 | b3 | 2 1 0 | 0 |
r4 | 0 | b1 | 1 1 0 | 1 |
r5 | 1 | b2 | 1 0 0 | 2 |
r6 | 2,0 | b1 | 0 0 0 | 1 |
r7 | 1,2,0,1 | b1 | 2 2 1 | 1 |
其中r6
,r7
中当前next值有多个表示经历了多次循环赋值, 当执行到r6
请求时, 只有b1
的current
大于0, 表示还可以被选中, 执行到r7
请求时, 循环检查了所有的后端列表, 由于currnet
全为0, 表示完成了一个轮次的权重轮询, 然后重置所有后端的current
值为对应的weight
值, 重新开始下一个轮次的循环。下面自己实现一次这个过程.
模拟实现
package main
import "fmt"
type Backend struct {
Name string
Addr string
Weight int
Current int
}
type Wrr struct {
Next int
BackendList []Backend
}
func (p *Wrr) Balance() Backend {
var curBackend Backend
next := p.Next
for {
curBackend = p.BackendList[next]
if curBackend.Current > 0 {
break
}
next = moveNext(next, len(p.BackendList))
if next == p.Next {
p.ResetCurrent()
p.Next = 0
next = 0
}
}
p.BackendList[next].Current--
p.Next = moveNext(next, len(p.BackendList))
return curBackend
}
func (p *Wrr) ResetCurrent() {
for i, _ := range p.BackendList {
p.BackendList[i].Current = p.BackendList[i].Weight
}
}
func mockBackendList() []Backend {
return []Backend{
{"b1", "127.0.0.1:80", 3, 3},
{"b2", "127.0.0.1:81", 2, 2},
{"b3", "127.0.0.1:82", 1, 1},
}
}
func moveNext(curNext int, backendTotal int) int {
next := curNext + 1
if next > (backendTotal - 1) {
next = 0
}
return next
}
func main() {
wrr := Wrr{
Next: 0,
BackendList: mockBackendList(),
}
// 7次请求
for r := 0; r < 7; r++ {
b := wrr.Balance()
fmt.Printf("当前选择: %s, 当前权重b1: %d, b2: %d, b3: %d \n", b.Name, wrr.BackendList[0].Current,
wrr.BackendList[1].Current, wrr.BackendList[2].Current)
}
}
执行结果如下:
与上述表格分析结果一致。
思考及总结
在应用方面, 加权轮询算法在各种微服务的组件如nginx, lvs服务发现场景等有广泛应用。在业务场景中, 电商和游戏的抽奖算法也可以使用此算法。
在实用性方面, 从宏观角度看算法可以满足针对不同权重的实例调度, 但是从微观角度看, 在某些特殊的权重配置下, 可能存在瞬时的对于某个实例的集中调度, 这种不平滑的负载可能会使某些实例出现瞬时高负载的现象,导致系统存在宕机的风险。比如后端有11个实例, 记为b1...b11
, 其中b1
权重为10, b2...b11
的权重都为1, 则在调度的后期流量全部集中于b1
, 而我们期待的结果是类似于:
请求 | 实例 |
---|---|
r1 | b1 |
r2 | b2 |
r3 | b1 |
r4 | b3 |
r5 | b1 |
r6 | b4 |
这种均衡的调度, 所以引出了平滑加权轮询算法, 我们下期讨论 :)