负载均衡算法 — 轮询

负载均衡算法 — 轮询


目录

  1. 概述
  2. 简单轮询
  3. 加权轮询
  4. 平滑加权轮询

1. 概述

  1. 在分布式系统中,为了实现负载均衡,必然会涉及到负载调度算法,如 Nginx 和 RPC 服务发现等场景。常见的负载均衡算法有 轮询、源地址 Hash、最少连接数,而 轮询 是最简单且应用最广的算法。
  2. 3 种常见的轮询调度算法,分别为 简单轮询、加权轮询、平滑加权轮询。下面将用如下 4 个服务,来详细说明轮询调度过程。
服务实例权重值
192.168.10.1:22021
192.168.10.2:22022
192.168.10.3:22023
192.168.10.4:22024

2. 简单轮询

  1. 简单轮询是轮询算法中最简单的一种,但由于它不支持配置负载,所以应用较少。
1. 算法描述
  1. 假设有 N 台实例 S = {S1, S2, …, Sn},指示变量 currentPos 表示当前选择的实例 ID,初始化为 -1。算法可以描述为:
    1. 调度到下一个实例;
    2. 若所有实例已被 调度 过一次,则从头开始调度;
    3. 每次调度重复步骤 1、2;
请求currentPos选中的实例
10192.168.10.1:2202
21192.168.10.2:2202
32192.168.10.3:2202
43192.168.10.4:2202
50192.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. 优缺点
  1. 在实际应用中,同一个服务会部署到不同的硬件环境,会出现性能不同的情况。若直接使用简单轮询调度算法,给每个服务实例相同的负载,那么,必然会出现资源浪费的情况。因此为了避免这种情况,一些人就提出了下面的 加权轮询 算法。

2. 加权轮询

  1. 加权轮询算法引入了“权”值,改进了简单轮询算法,可以根据硬件性能配置实例负载的权重,从而达到资源的合理利用。
1. 算法描述
  1. 假设有 N 台实例 S = {S1, S2, …, Sn},权重 W = {W1, W2, …, Wn},指示变量 currentPos 表示当前选择的实例 ID,初始化为 -1;变量 currentWeight 表示当前权重,初始值为 max(S);max(S) 表示 N 台实例的最大权重值,gcd(S) 表示 N 台实例权重的最大公约数。

  2. 算法可以描述为:

    1. 从上一次调度实例起,遍历后面的每个实例;
    2. 若所有实例已被遍历过一次,则减小 currentWeight 为 currentWeight - gcd(S),并从头开始遍历;若 currentWeight 小于等于 0,则重置为 max(S);
    3. 直到 遍历的实例的权重大于等于 currentWeight 时结束,此时实例为需调度的实例;
    4. 每次调度重复步骤 1、2、3;
  3. 例如,上述 4 个服务,最大权重 max(S) 为 4,最大公约数 gcd(S) 为 1。其调度过程如下:

请求currentPoscurrentWeight选中的实例
134192.168.10.4:2202
223192.168.10.3:2202
333192.168.10.4:2202
412192.168.10.2:2202
….
921192.168.10.3:2202
1034192.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. 优缺点
  1. 加权轮询 算法虽然通过配置实例权重,解决了 简单轮询 的资源利用问题,但是它还是存在一个比较明显的 缺陷。
  2. 例如:服务实例 S = {a, b, c},权重 W = {5, 1, 1},使用加权轮询调度生成的实例序列为 {a, a, a, a, a, b, c},那么就会存在连续 5 个请求都被调度到实例 a。而实际中,这种不均匀的负载是不被允许的,因为连续请求会突然加重实例 a 的负载,可能会导致严重的事故。
  3. 为了解决加权轮询调度不均匀的缺陷,提出了 平滑加权轮询 调度算法,它会生成的更均匀的调度序列 {a, a, b, a, c, a, a}。

4. 平滑加权轮询

1. 算法描述
  1. 假设有 N 台实例 S = {S1, S2, …, Sn},配置权重 W = {W1, W2, …, Wn},有效权重 CW = {CW1, CW2, …, CWn}。每个实例 i 除了存在一个配置权重 Wi 外,还存在一个当前有效权重 CWi,且 CWi 初始化为 Wi;指示变量 currentPos 表示当前选择的实例 ID,初始化为 -1;所有实例的配置权重和为 weightSum;

  2. 那么,调度算法可以描述为:

    1. 初始每个实例 i 的 当前有效权重 CWi 为 配置权重 Wi,并求得配置权重和 weightSum;
    2. 选出 当前有效权重 最大 的实例,将 当前有效权重 CWi 减去所有实例的 权重和 weightSum,且变量 currentPos 指向此位置;
    3. 将每个实例 i 的 当前有效权重 CWi 都加上 配置权重 Wi;
    4. 取到变量 currentPos 指向的实例;
    5. 每次调度重复上述步骤 2、3、4;
  3. 上述 3 个服务,配置权重和 weightSum 为 7,其调度过程如下:

请求选中前的当前权重currentPos选中的实例选中后的当前权重
1{5, 1, 1}0192.168.10.1:2202{-2, 1, 1}
2{3, 2, 2}0192.168.10.1:2202{-4, 2, 2}
3{1, 3, 3}1192.168.10.2:2202{1, -4, 3}
4{6, -3, 4}0192.168.10.1:2202{-1, -3, 4}
5{4, -2, 5}2192.168.10.3:2202{4, -2, -2}
6{9, -1, -1}0192.168.10.1:2202{2, -1, -1}
7{7, 0, 0}0192.168.10.1:2202{0, 0, 0}
8{5, 1, 1}0192.168.10.1:2202{-2, 1, 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. 小结
  1. 尽管,平滑加权轮询算法改善了加权轮询算法调度的缺陷,即调度序列分散的不均匀,避免了实例负载突然加重的可能,但是仍然不能动态感知每个实例的负载。
  2. 若由于实例权重配置不合理,或者一些其他原因加重系统负载的情况,平滑加权轮询都无法实现每个实例的负载均衡,这时就需要 有状态 的调度算法来完成。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值