最近项目需要一个REST API应用网关,因此用GO写了一个,功能是在不断完善的,感觉最终功能跟nginx反向代理差不多的,除了业务处理。现在已经实现通过配置来控制转发行为。请求前端定义的地址A,通过认证后,转到后端指定地址B,并把结果返回。
现在增加支持多backend,因此要实现负载均衡。
大概网上看了下nginx源代码的算法,及其它网友的,基本上都是实时计算,最终落到backend的顺序,都是顺序的。
例如网友的例子:
{ a, b, c }三个服务器,weight值是{ 5, 1, 2 }
加权轮询的选择的服务器顺序是:a,c,a,a,b,a,c,a
当轮询一遍后,又重新一遍,不断重复。
这样看来,每次计算权重获取backend是没有必要了,不如把获取backend的顺序计算好了,放到队列里,按顺序取就好了。
对上面这个例子,我的实现基本思路是:
1.在启动时读取配置文件并初始化,把所有的backend放到队列MasterQ里,
例如三个backend:MasterQ=[0:backend1,1:backend2,2:backend3]
如果是使用加权轮循算法,初始化加权轮循算法对象
2.初始化加权轮循算法对象。
把权重值累加,weight_sum=5+1+2=8(可除最大公约后再累加)
初始队列RoundRobinQ[weight_sum]
填充RoundRobinQ=[0,0,0,0,0,1,2,2],值是MasterQ的索引
打乱RoundRobinQ顺序,算法自定:RoundRobinQ=[1 0 1 0 2 0 1 0 2 0]
3.在每次请求到来,要获取一个backend时,执行next方法:
直接从根据当前RoundRobinQ的索引值,获取MasterQ中可用的backend:
backend = MasterQ[RoundRobinQ[index]]
获取后index++
以上。这样的话,每次请求获取一个backend的计算量就比较少。
当然,我是想着换一个思路并实现,而且貌似也比较简单。
下面是第一版代码片断,可以参考一下,已经支持把不可用的backend剔除掉:
//负载均衡的配置信息
type LBConf struct {
BaseUrl string
Weight int
Cookie string
MaxFails int //连接几次后标记为该连接不可用
FailTimeout int //连接超过多少秒时间后标记该连接不可用
Backup int //是否备份服务器 0:no, 1:yes
}
//负载均衡的backend信息
type LBNode struct {
Conf LBConf
Down bool
Fails int //连续失败的次数,成功后置为0
LastDownTime int64
}
func (this *LBNode) SetFail() {
this.Fails++
if this.Fails > this.Conf.MaxFails {
this.Down = true
this.LastDownTime = time.Now().Unix()
}
}
//负载均衡算法接口,不同的算法实现这个接口即可
type LBInterface interface {
Init()
Next(req *http.Request) *LBNode
}
//负载均衡运行时
//保存公共的配置信息
type LBRuntime struct {
LB int //load balance 类型
MasterQ []*LBNode //可用的节点
BackupQ []*LBNode
}
func (this *LBRuntime) Next(Req *http.Request) *LBNode {
return nil
}
func (this *LBRuntime) Init() {
}
//负载均衡实现方式:加权轮循
//具体可以参考ngnix的实现。
//这里实现方式有点区别,没那么复杂
//实现LBInterface接口,继承LBRuntime
type LBRountRobin struct {
LBRuntime
RoundRobinQ []int //轮循队列
RBIndex int //当前RoundRobinQ的下标
MQLen int //MasterQ队列长度
RQLen int //RoundRobinQ队列长度
}
//获取下一个节点
//根据初始化的加权轮循队列,按顺序循环获取后端节点
func (this *LBRountRobin) Next(req *http.Request) *LBNode {
if this.RQLen == 1 {
c := this.MasterQ[this.RoundRobinQ[0]]
if c.Down {
this.RQLen = 0
return nil
}
return c
} else if this.RQLen > 1 {
//多线程这里会不安全(当其它线程调用init时),不过接受
//通过复制来防止并发修改,防止下标超标
index := this.RBIndex
this.RBIndex++
if this.RBIndex >= this.RQLen {
this.RBIndex = 0
}
if index >= this.RQLen {
index = 0
}
c := this.MasterQ[this.RoundRobinQ[index]]
if c.Down {
//如果down了,重新生成可用的队列,取下一个节点
this.Init()
return this.Next(req)
}
return c
}
//TODO 没有主节点,选择备用节点
return nil
}
//初始化Round-Robin负载均衡
//根据加权轮循得出的结果是周期循环的,这里初始化时简单地实现一个周期循环队列
//而不像NGINX在每个请求到来时再计算
func (this *LBRountRobin) Init() {
weight_sum := 0
numQ := 0
if len(this.MasterQ) < 1 {
//如果没有backend节点,直接返回
return
}
for _, node := range this.MasterQ {
if node.Down {
//过滤掉挂掉的节点
continue
}
numQ++
weight_sum += node.Conf.Weight
}
if numQ == 0 {
this.RBIndex = 0
this.RQLen = 0
this.MQLen = len(this.MasterQ)
this.RoundRobinQ = nil
log.Info("Round Robin Q: [ ]")
return
}
//把每次执行的下标预先放到队列里,执行的时候按顺序就行了
if numQ == 1 {
this.RoundRobinQ = make([]int, 1)
} else {
this.RoundRobinQ = make([]int, weight_sum)
}
w := 0
for key, node := range this.MasterQ {
if node.Down {
//过滤掉挂掉的节点,只选择没挂的节点
continue
}
if numQ == 1 {
this.RoundRobinQ[0] = key
break
} else {
for i := 0; i < node.Conf.Weight; i++ {
this.RoundRobinQ[w+i] = key
}
}
w = w + node.Conf.Weight
}
if numQ > 1 {
//打乱RoundRobinQ的顺序 TODO 改进
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < weight_sum; i++ {
x := r.Intn(weight_sum)
temp := this.RoundRobinQ[x]
other := weight_sum % (x + 1)
this.RoundRobinQ[x] = this.RoundRobinQ[other]
this.RoundRobinQ[other] = temp
}
}
this.RBIndex = 0
this.RQLen = len(this.RoundRobinQ)
this.MQLen = len(this.MasterQ)
log.Info("Round Robin Q: ", this.RoundRobinQ)
}