zoukankan      html  css  js  c++  java
  • 负载均衡算法

    前言


       之前写过一个多进程异步服务系统的小项目,里面对于多个进程之间使用的负载均衡算法。就是最简单的轮询办法,但是这种算法存在一个比较大的问题,如果其中一个服务器出现问题,那下一个服务器的负载会增大两倍。基于这个问题我这几天在看Go语言高级编程的书的时候,发现作者写了有关于负载均衡的章节,因此这里拿来学习一下。

      原书地址:https://github.com/chai2010/advanced-go-programming-book/blob/master/ch6-cloud/ch6-05-load-balance.md

    常见的负载均衡思路


     如果我们不考虑均衡的话,现在有n个服务节点,我们完成业务流程只需要从这n个中挑出其中的一个。有几种思路:

    1. 按顺序挑: 例如上次选了第一台,那么这次就选第二台,下次第三台,如果已经到了最后一台,那么下一次从第一台开始。这种情况下我们可以把服务节点信息都存储在数组中,每次请求完成下游之后,将一个索引后移即可。在移到尽头时再移回数组开头处。

    2. 随机挑一个: 每次都随机挑,真随机伪随机均可。假设选择第 x 台机器,那么x可描述为rand.Intn()%n

    3. 根据某种权重,对下游节点进行排序,选择权重最大/小的那一个。

    当然了,实际场景我们不可能无脑轮询或者无脑随机,如果对下游请求失败了,我们还需要某种机制来进行重试,如果纯粹的随机算法,存在一定的可能性使你在下一次仍然随机到这次的问题节点。

     基于洗牌算法的负载均衡


       考虑到我们需要随机选取每次发送请求的节点,同时在遇到下游返回错误时换其它节点重试。所以我们设计一个大小和节点数组大小一致的索引数组,每次来新的请求,我们对索引数组做洗牌,然后取第一个元素作为选中的服务节点,如果请求失败,那么选择下一个节点重试,以此类推:

    var endpoints = []string {
    	"100.69.62.1:3232",
    	"100.69.62.32:3232",
    	"100.69.62.42:3232",
    	"100.69.62.81:3232",
    	"100.69.62.11:3232",
    	"100.69.62.113:3232",
    	"100.69.62.101:3232",
    }
    
    // 重点在这个 shuffle
    func shuffle(slice []int) {
    	for i := 0; i < len(slice); i++ {
    		a := rand.Intn(len(slice))
    		b := rand.Intn(len(slice))
    		slice[a], slice[b] = slice[b], slice[a]
    	}
    }
    
    func request(params map[string]interface{}) error {
    	var indexes = []int {0,1,2,3,4,5,6}
    	var err error
    
    	shuffle(indexes)
    	maxRetryTimes := 3
    
    	idx := 0
    	for i := 0; i < maxRetryTimes; i++ {
    		err = apiRequest(params, indexes[idx])
    		if err == nil {
    			break
    		}
    		idx++
    	}
    
    	if err != nil {
    		// logging
    		return err
    	}
    
    	return nil
    }
    

      我们循环一遍slice,两两交换,这个和我们平常打牌时常用的洗牌方法类似。看起来没有什么问题。真的没有问题么?还是有问题的。这段简短的程序里有两个隐藏的隐患:

    1. 没有随机种子。在没有随机种子的情况下,rand.Intn()返回的伪随机数序列是固定的。

    2. 洗牌不均匀,会导致整个数组第一个节点有大概率被选中,并且多个节点的负载分布不均衡。

      第一点比较简单,应该不用在这里给出证明了。关于第二点,我们可以用概率知识来简单证明一下。假设每次挑选都是真随机,我们假设第一个位置的节点在len(slice)次交换中都不被选中的概率是((6/7)*(6/7))^7 ≈ 0.34。而分布均匀的情况下,我们肯定希望被第一个元素在任意位置上分布的概率均等,所以其被随机选到的概率应该约等于1/7≈0.14

      显然,这里给出的洗牌算法对于任意位置的元素来说,有30%的概率不对其进行交换操作。所以所有元素都倾向于留在原来的位置。因为我们每次对shuffle数组输入的都是同一个序列,所以第一个元素有更大的概率会被选中。在负载均衡的场景下,也就意味着节点数组中的第一台机器负载会比其它机器高不少(这里至少是3倍以上)。

     修正洗牌算法


     从数学上得到过证明的还是经典的fisher-yates算法,主要思路为每次随机挑选一个值,放在数组末尾。然后在n-1个元素的数组中再随机挑选一个值,放在数组末尾,以此类推。

    func shuffle(indexes []int) {
    	for i:=len(indexes); i>0; i-- {
    		lastIdx := i - 1
    		idx := rand.Int(i)
    		indexes[lastIdx], indexes[idx] = indexes[idx], indexes[lastIdx]
    	}
    }
    

       在Go的标准库中已经为我们内置了该算法:

    func shuffle(n int) []int {
    	b := rand.Perm(n)
    	return b
    }
    

      在当前的场景下,我们只要用rand.Perm就可以得到我们想要的索引数组了。

    负载均衡算法效果验证


       我们这里不考虑加权负载均衡的情况,既然名字是负载“均衡”。那么最重要的就是均衡。我们把开篇中的shuffle算法,和之后的fisher yates算法的结果进行简单地对比

    package main
    
    import (
    	"fmt"
    	"math/rand"
    	"time"
    )
    
    func init() {
    	rand.Seed(time.Now().UnixNano())
    }
    
    func shuffle1(slice []int) {
    	for i := 0; i < len(slice); i++ {
    		a := rand.Intn(len(slice))
    		b := rand.Intn(len(slice))
    		slice[a], slice[b] = slice[b], slice[a]
    	}
    }
    
    func shuffle2(indexes []int) {
    	for i := len(indexes); i > 0; i-- {
    		lastIdx := i - 1
    		idx := rand.Intn(i)
    		indexes[lastIdx], indexes[idx] = indexes[idx], indexes[lastIdx]
    	}
    }
    
    func main() {
    	var cnt1 = map[int]int{}
    	for i := 0; i < 1000000; i++ {
    		var sl = []int{0, 1, 2, 3, 4, 5, 6}
    		shuffle1(sl)
    		cnt1[sl[0]]++
    	}
    
    	var cnt2 = map[int]int{}
    	for i := 0; i < 1000000; i++ {
    		var sl = []int{0, 1, 2, 3, 4, 5, 6}
    		shuffle2(sl)
    		cnt2[sl[0]]++
    	}
    
    	fmt.Println(cnt1, "
    ", cnt2)
    }
    

     输出:

    map[0:224436 1:128780 5:129310 6:129194 2:129643 3:129384 4:129253]
    map[6:143275 5:143054 3:143584 2:143031 1:141898 0:142631 4:142527]
  • 相关阅读:
    梦断代码读后感02
    UML大战需求与分析--阅读笔记4
    UML大战需求分析--阅读笔记3
    软件需求与分析课堂讨论一
    UML大战需求分析--阅读笔记02
    UML大战需求分析--阅读笔记01
    学习进度--大三下
    问题账户需求分析
    2016年秋季阅读计划
    个人总结
  • 原文地址:https://www.cnblogs.com/GoodRnne/p/11342571.html
Copyright © 2011-2022 走看看