负载均衡算法 — 轮询
目录
- 概述
- 简单轮询
- 加权轮询
- 平滑加权轮询
1. 概述
- 在分布式系统中,为了实现负载均衡,必然会涉及到负载调度算法,如 Nginx 和 RPC 服务发现等场景。常见的负载均衡算法有 轮询、源地址 Hash、最少连接数,而 轮询 是最简单且应用最广的算法。
- 3 种常见的轮询调度算法,分别为 简单轮询、加权轮询、平滑加权轮询。下面将用如下 4 个服务,来详细说明轮询调度过程。
服务实例 | 权重值 |
---|---|
192.168.10.1:2202 | 1 |
192.168.10.2:2202 | 2 |
192.168.10.3:2202 | 3 |
192.168.10.4:2202 | 4 |
2. 简单轮询
- 简单轮询是轮询算法中最简单的一种,但由于它不支持配置负载,所以应用较少。
1. 算法描述
- 假设有 N 台实例 S = {S1, S2, …, Sn},指示变量 currentPos 表示当前选择的实例 ID,初始化为 -1。算法可以描述为:
- 调度到下一个实例;
- 若所有实例已被 调度 过一次,则从头开始调度;
- 每次调度重复步骤 1、2;
请求 | currentPos | 选中的实例 |
---|---|---|
1 | 0 | 192.168.10.1:2202 |
2 | 1 | 192.168.10.2:2202 |
3 | 2 | 192.168.10.3:2202 |
4 | 3 | 192.168.10.4:2202 |
5 | 0 | 192.168.10.1:2202 |
2. 代码实现
type Round struct {
curIndex int
rss []string
}
func (r *Round) Add(params ...string) error {
if len(params) == 0 {
return errors.New("至少需要1个参数")
}
r.rss = append(r.rss, params...)
return nil
}
func (r *Round) Next() (string, error) {
if len(r.rss) == 0 {
return "", errors.New("不存在参数")
}
curElement := r.rss[r.curIndex]
r.curIndex = (r.curIndex + 1) % len(r.rss)
return curElement, nil
}
3. 优缺点
- 在实际应用中,同一个服务会部署到不同的硬件环境,会出现性能不同的情况。若直接使用简单轮询调度算法,给每个服务实例相同的负载,那么,必然会出现资源浪费的情况。因此为了避免这种情况,一些人就提出了下面的 加权轮询 算法。
2. 加权轮询
- 加权轮询算法引入了“权”值,改进了简单轮询算法,可以根据硬件性能配置实例负载的权重,从而达到资源的合理利用。
1. 算法描述
-
假设有 N 台实例 S = {S1, S2, …, Sn},权重 W = {W1, W2, …, Wn},指示变量 currentPos 表示当前选择的实例 ID,初始化为 -1;变量 currentWeight 表示当前权重,初始值为 max(S);max(S) 表示 N 台实例的最大权重值,gcd(S) 表示 N 台实例权重的最大公约数。
-
算法可以描述为:
- 从上一次调度实例起,遍历后面的每个实例;
- 若所有实例已被遍历过一次,则减小 currentWeight 为 currentWeight - gcd(S),并从头开始遍历;若 currentWeight 小于等于 0,则重置为 max(S);
- 直到 遍历的实例的权重大于等于 currentWeight 时结束,此时实例为需调度的实例;
- 每次调度重复步骤 1、2、3;
-
例如,上述 4 个服务,最大权重 max(S) 为 4,最大公约数 gcd(S) 为 1。其调度过程如下:
请求 | currentPos | currentWeight | 选中的实例 |
---|---|---|---|
1 | 3 | 4 | 192.168.10.4:2202 |
2 | 2 | 3 | 192.168.10.3:2202 |
3 | 3 | 3 | 192.168.10.4:2202 |
4 | 1 | 2 | 192.168.10.2:2202 |
… | … | … | …. |
9 | 2 | 1 | 192.168.10.3:2202 |
10 | 3 | 4 | 192.168.10.4:2202 |
2. 代码实现
var slaveDns = map[int]map[string]interface{}{
0: {"connectstring": "root@tcp(172.16.0.164:3306)/shiqu_tools?charset=utf8", "weight": 2},
1: {"connectstring": "root@tcp(172.16.0.165:3306)/shiqu_tools?charset=utf8", "weight": 4},
2: {"connectstring": "root@tcp(172.16.0.166:3306)/shiqu_tools?charset=utf8", "weight": 8},
}
var last int = -1 //表示上一次选择的服务器
var cw int = 0 //表示当前调度的权值
var gcd int = 2 //当前所有权重的最大公约数 比如 2,4,8 的最大公约数为:2
var devCount int = 2 //当前机器数
func getDns() string {
for {
last = (last + 1) % len(slaveDns)
if last == 0 {
cw = cw - gcd
if cw <= 0 {
cw = getMaxWeight()
if cw == 0 {
return ""
}
}
}
if weight, _ := slaveDns[last]["weight"].(int); weight >= cw {
return slaveDns[last]["connectstring"].(string)
}
}
}
func getMaxWeight() int {
max := 0
for _, v := range slaveDns {
if weight, _ := v["weight"].(int); weight >= max {
max = weight
}
}
return max
}
func Add(addr string, weight int) {
tmap := make(map[string]interface{})
tmap["connectstring"] = addr
tmap["weight"] = weight
slaveDns[devCount] = tmap
devCount = devCount + 1
if devCount == 0 {
gcd = weight
} else {
gcd = Gcd(gcd, weight)
}
}
func Gcd(gcd int, weight int) int {
for weight != 0 {
gcd, weight = weight, gcd%weight
}
return gcd
}
3. 优缺点
- 加权轮询 算法虽然通过配置实例权重,解决了 简单轮询 的资源利用问题,但是它还是存在一个比较明显的 缺陷。
- 例如:服务实例 S = {a, b, c},权重 W = {5, 1, 1},使用加权轮询调度生成的实例序列为 {a, a, a, a, a, b, c},那么就会存在连续 5 个请求都被调度到实例 a。而实际中,这种不均匀的负载是不被允许的,因为连续请求会突然加重实例 a 的负载,可能会导致严重的事故。
- 为了解决加权轮询调度不均匀的缺陷,提出了 平滑加权轮询 调度算法,它会生成的更均匀的调度序列 {a, a, b, a, c, a, a}。
4. 平滑加权轮询
1. 算法描述
-
假设有 N 台实例 S = {S1, S2, …, Sn},配置权重 W = {W1, W2, …, Wn},有效权重 CW = {CW1, CW2, …, CWn}。每个实例 i 除了存在一个配置权重 Wi 外,还存在一个当前有效权重 CWi,且 CWi 初始化为 Wi;指示变量 currentPos 表示当前选择的实例 ID,初始化为 -1;所有实例的配置权重和为 weightSum;
-
那么,调度算法可以描述为:
- 初始每个实例 i 的 当前有效权重 CWi 为 配置权重 Wi,并求得配置权重和 weightSum;
- 选出 当前有效权重 最大 的实例,将 当前有效权重 CWi 减去所有实例的 权重和 weightSum,且变量 currentPos 指向此位置;
- 将每个实例 i 的 当前有效权重 CWi 都加上 配置权重 Wi;
- 取到变量 currentPos 指向的实例;
- 每次调度重复上述步骤 2、3、4;
-
上述 3 个服务,配置权重和 weightSum 为 7,其调度过程如下:
请求 | 选中前的当前权重 | currentPos | 选中的实例 | 选中后的当前权重 |
---|---|---|---|---|
1 | {5, 1, 1} | 0 | 192.168.10.1:2202 | {-2, 1, 1} |
2 | {3, 2, 2} | 0 | 192.168.10.1:2202 | {-4, 2, 2} |
3 | {1, 3, 3} | 1 | 192.168.10.2:2202 | {1, -4, 3} |
4 | {6, -3, 4} | 0 | 192.168.10.1:2202 | {-1, -3, 4} |
5 | {4, -2, 5} | 2 | 192.168.10.3:2202 | {4, -2, -2} |
6 | {9, -1, -1} | 0 | 192.168.10.1:2202 | {2, -1, -1} |
7 | {7, 0, 0} | 0 | 192.168.10.1:2202 | {0, 0, 0} |
8 | {5, 1, 1} | 0 | 192.168.10.1:2202 | {-2, 1, 1} |
- 此轮询调度算法思路首先被 Nginx 开发者提出
2. 代码实现
type LoadBalance interface {
//选择一个后端Server
//参数remove是需要排除选择的后端Server
Select(remove []string) *Server
//更新可用Server列表
UpdateServers(servers []*Server)
}
type Server struct {
//主机地址
Host string
//主机名
Name string
Weight int
//主机是否在线
Online bool
}
type Weighted struct {
Server *Server
Weight int
CurrentWeight int //当前机器权重
EffectiveWeight int //机器权重
}
func (this *Weighted) String() string {
return fmt.Sprintf("[%s][%d]", this.Server.Host, this.Weight)
}
type LoadBalanceWeightedRoundRobin struct {
servers []*Server
weighted []*Weighted
}
func NewLoadBalanceWeightedRoundRobin(servers []*Server) *LoadBalanceWeightedRoundRobin {
new := &LoadBalanceWeightedRoundRobin{}
new.UpdateServers(servers)
return new
}
func (this *LoadBalanceWeightedRoundRobin) UpdateServers(servers []*Server) {
if len(this.servers) == len(servers) {
for _, new := range servers {
isEqual := false
for _, old := range this.servers {
if new.Host == old.Host && new.Weight == old.Weight && new.Online == old.Online {
isEqual = true
break
}
}
if isEqual == false {
goto build
}
}
return
}
build:
log.Println("clients change")
log.Println(this.servers)
log.Println(servers)
weighted := make([]*Weighted, 0)
for _, v := range servers {
if v.Online == true {
w := &Weighted{
Server: v,
Weight: v.Weight,
CurrentWeight: 0,
EffectiveWeight: v.Weight,
}
weighted = append(weighted, w)
}
}
this.weighted = weighted
this.servers = servers
log.Printf("weighted[%v]", this.weighted)
}
func (this *LoadBalanceWeightedRoundRobin) Select(remove []string) *Server {
if len(this.weighted) == 0 {
return nil
}
w := this.nextWeighted(this.weighted, remove)
if w == nil {
return nil
}
return w.Server
}
func (this *LoadBalanceWeightedRoundRobin) nextWeighted(servers []*Weighted, remove []string) (best *Weighted) {
total := 0
for i := 0; i < len(servers); i++ {
w := servers[i]
if w == nil {
continue
}
isFind := false
for _, v := range remove {
if v == w.Server.Host {
isFind = true
}
}
if isFind == true {
continue
}
w.CurrentWeight += w.EffectiveWeight
total += w.EffectiveWeight
if w.EffectiveWeight < w.Weight {
w.EffectiveWeight++
}
if best == nil || w.CurrentWeight > best.CurrentWeight {
best = w
}
}
if best == nil {
return nil
}
best.CurrentWeight -= total
return best
}
func (this *LoadBalanceWeightedRoundRobin) String() string {
return "WeightedRoundRobin"
}
3. 小结
- 尽管,平滑加权轮询算法改善了加权轮询算法调度的缺陷,即调度序列分散的不均匀,避免了实例负载突然加重的可能,但是仍然不能动态感知每个实例的负载。
- 若由于实例权重配置不合理,或者一些其他原因加重系统负载的情况,平滑加权轮询都无法实现每个实例的负载均衡,这时就需要 有状态 的调度算法来完成。