今天试试用go实现四种常见的负载均衡,分别是随机负载均衡,轮询负载均衡,加权负载均衡,一致性hash负载均衡。
随机负载均衡
功能和名字一样,随机从一堆服务器中选择一个服务器,那么实现也很简单,不做过多说明。使用了rand方法随机取一个服务器。
package main
import (
"errors"
"fmt"
"math/rand"
)
type RandomBalance struct {
curIndex int
res []string
}
func (r *RandomBalance)Add(params ...string) error{
if len(params) == 0{
return errors.New("params need more than 0")
}
addr := params[0]
r.res = append(r.res,addr)
return nil
}
func (r *RandomBalance)Next() string{
if len(r.res) == 0{
return ""
}
r.curIndex = rand.Intn(len(r.res))
return r.res[r.curIndex]
}
func (r *RandomBalance)Get() (string,error){
return r.Next(),nil
}
func main() {
r := new(RandomBalance)
r.Add("localhost:8080")
r.Add("localhost:8081")
r.Add("localhost:8082")
r.Add("localhost:8083")
r.Add("localhost:8084")
r.Add("localhost:8085")
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
}
轮询负载均衡
这个就要注意轮询到了结尾(0-1-2-3),如果后面没有服务器要重新轮询回到0。这个就直接用取模就可以完成了。
package main
import (
"errors"
"fmt"
)
type RoundRobinBalance struct {
curIndex int
res []string
}
func (r *RoundRobinBalance)Add(params ...string) error{
if len(params) == 0{
return errors.New("params need more than 0")
}
addr := params[0]
r.res = append(r.res,addr)
return nil
}
func (r *RoundRobinBalance)Next() string{
if len(r.res) == 0{
return ""
}
res := r.res[r.curIndex]
r.curIndex = (r.curIndex + 1) % len(r.res)
return res
}
func (r *RoundRobinBalance)Get() (string,error){
return r.Next(),nil
}
func main() {
r := new(RoundRobinBalance)
r.Add("localhost:8080")
r.Add("localhost:8081")
r.Add("localhost:8082")
r.Add("localhost:8083")
r.Add("localhost:8084")
r.Add("localhost:8085")
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
}
加权负载均衡
这个加权就很有意思,每一个服务器有它当前的权值,我们选择权值最高的服务器作为访问服务器。
当然权值是会变化的,如何变化?先来看一张图。
我们默认选择currentWeight作为选择的标准,每次都选择最大的currentWeight,每轮选择结束后被选择的currentWeight也会对应的改变,就这么一轮一轮的选择。
package main
import (
"errors"
"fmt"
"strconv"
)
type WeightNode struct {
Addr string
Weight int64
EffectiveWeight int64
CurrentWeight int64
}
type WeightRoundLoadBalance struct {
list []*WeightNode
}
func (r *WeightRoundLoadBalance)Add(params ...string) error{
if len(params) != 2{
return errors.New("params need more 2")
}
parseInt, err := strconv.ParseInt(params[1], 10, 64)
if err != nil{
return errors.New("convert to int error")
}
newNode := &WeightNode{
Addr: params[0],
Weight: parseInt,
EffectiveWeight: parseInt,
CurrentWeight: parseInt,
}
r.list = append(r.list,newNode)
return nil
}
func (r *WeightRoundLoadBalance)Next() string{
var total int64 = 0
var best *WeightNode
for index,v := range r.list{
total += v.CurrentWeight
r.list[index].CurrentWeight += r.list[index].EffectiveWeight
if v.EffectiveWeight < v.CurrentWeight{
v.EffectiveWeight++
}
if best == nil || best.CurrentWeight < v.CurrentWeight{
best = v
}
}
if best == nil{
return ""
}
best.CurrentWeight -= total
return best.Addr
}
func (r *WeightRoundLoadBalance)Get() (string,error){
return r.Next(),nil
}
func main() {
r := new(WeightRoundLoadBalance)
r.Add("A","4")
r.Add("B","3")
r.Add("C","2")
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
fmt.Println(r.Get())
}
一致性hash
先说说一致性hash相对于传统取模hash的优点吧。
对于传统的取模hash我们知道使用这样一个公式来计算从哪个服务器取出缓存的,即:hash(资源) % 服务器数量 .但是这样会有一个缺点,即增加了服务器后,取余数的数量会变化,会导致大量缓存失效。
所以这个时候一致性hash就出现了,hash是将服务器先hash到一个hash环,通过顺时针决定自己的缓存存在哪个服务器,那么看看增加了一个服务器的情况,此时假如增加一个E,可能会导致一部分选取C为服务器的缓存失效,但是其他缓存是可以继续使用的。这样就优于取模hash。
但是一致性hash会发生一个问题,hash偏斜。即hash缓存不均匀,即由于hash情况过于极端,所有的缓存选择了一个服务器放缓存。所以需要让服务器“数量尽可能多”——————创建虚拟节点。
即我们一开始是A一个服务器,但是我们可以将一个A分解成多个A(虚拟节点),即下图:
那么我们读取缓存的时候,可以先找到虚拟节点,再找到缓存存储的真实节点,读出缓存。
了解了一些一致性hash的原理,我们可以开始编写一致性hash的代码。
package main
import (
"errors"
"fmt"
"hash/crc32"
"sort"
"strconv"
"sync"
)
type Hash func([]byte)uint32
type Uint32Slice []uint32
func (s Uint32Slice)Len() int{
return len(s)
}
func (s Uint32Slice)Less(i,j int)bool{
return s[i] < s[j]
}
func (s Uint32Slice)Swap(i,j int){
s[i],s[j] = s[j],s[i]
}
type ConsistentHashBalance struct {
mux sync.RWMutex
hash Hash //hash函数
replicas int //复制因子
keys Uint32Slice //已排序的hash节点切片
hashmap map[uint32]string //key为hash值val为节点
}
func NewConsistenceHashBalance(replicas int,hash Hash)*ConsistentHashBalance{
c := &ConsistentHashBalance{
hash: hash,
replicas: replicas,
hashmap: make(map[uint32]string),
keys: make([]uint32,0,100),
}
if c.hash == nil{
//保证是一个2^32 - 1的一个环
c.hash = crc32.ChecksumIEEE
}
return c
}
func (c *ConsistentHashBalance)Add(params ...string)error{
if len(params) == 0{
return errors.New("param need more than one")
}
addr := params[0]
//计算虚拟节点hash值
for i := 0;i < c.replicas;i++{
hash := c.hash([]byte(strconv.Itoa(i) + addr))
//实现了排序接口
c.keys = append(c.keys,hash)
c.hashmap[hash] = addr
}
sort.Sort(c.keys)
return nil
}
//得到取缓存的服务器
func (c *ConsistentHashBalance)Next(key string)(string,error){
if len(c.keys) == 0{
return "",nil
}
hash := c.hash([]byte(key))
//通过二分查询到最优节点(第一个hash大于资源hash的服务器)
idx := sort.Search(len(c.keys), func(i int) bool {
return c.keys[i] > hash
})
if idx == len(c.keys){
//没有找到服务器,说明此时处于环的尾部,那么直接用第0台服务器
idx = 0
}
c.mux.Lock()
defer c.mux.Unlock()
val := c.hashmap[c.keys[idx]]
return val,nil
}
func main() {
r := NewConsistenceHashBalance(3,nil)
r.Add("localhost:8001")
r.Add("localhost:8002")
r.Add("localhost:8003")
fmt.Println(r.Next("localhost:8000/hello"))
fmt.Println(r.Next("localhost:8000/hello1"))
fmt.Println(r.Next("localhost:8000/hello2"))
}
由于楼主水平有限,可能对于hash一致性的描述并不是很清楚,强烈推荐大家看B站的讲解,讲的特别好,时长不长,但是可以对一致性hash会有一个最基本的了解。
下面贴出地址:https://www.bilibili.com/video/BV1Hs411j73wfrom=search&seid=10071461099033302500&spm_id_from=333.337.0.0