BFE负载均衡源码--加权轮询算法及实现

1,145 阅读4分钟

前言

上一篇文章我们分析了BFE的转发模型原理, 其中不同的负载均衡算法是转发的核心构成, 本篇我们基于第一种算法: 普通加权轮询算法进行源码分析。

前期回顾: BFE转发模型

源码实现

加权轮询算法实现在bfe_balance/bal_slb/bal_rr.go:simpleBalance()方法, 如方法名, 是一种简单的轮询算法。

func (brr *BalanceRR) simpleBalance() (*backend.BfeBackend, error) {
   /*** 省略 ***/
   backends := brr.backends // 获取后端服务列表
   allBackendDown := true // 是否需要重置backend权重

   next := brr.next // 默认从 0 index 开始遍历
   for {
      backendRR = backends[next] // 此变量表示当前被选择后端
      backend = backendRR.backend

      avail := backend.Avail()
      // 如果当前后端被租户设置为提供服务并且在此轮轮询中权重>0, 则是一个有效的后端
      if avail && backendRR.current > 0 { 
         break
      }
      /*** 省略 ***/
      // 进行到这里表示此轮轮询中已经存在因为权重比例小而不会被选择的后端
      if avail && backendRR.weight != 0 {
         allBackendDown = false
      }

      // 移动到下一个后端, 如果当前 next 已经是末尾, 则moveToNext()计算为从index=0开始
      next = moveToNext(next, backends)
      
      // 如果经过后移选择的后端等于当前轮询的后端
      if next == brr.next {
         // all backends have been check
         if allBackendDown {
            /*** 省略 ***/
            return backend, fmt.Errorf("rr_bal:all backend is down")
         } else {
            /*** 省略 ***/
            brr.initWeight()
            brr.next = 0
            next = 0
         }
      }
   }
   // 将此次选择的后端当前权重值减一
   backendRR.current--
   // next指向当前被选择的下一个backend节点
   next = moveToNext(next, backends)
   brr.next = next

   /*** 省略 ***/
   return backend, nil
}

backend后端结构有两个字段:

  • weight:表示此后端的权重值
  • current:表示当前轮次的轮询中的权重值, 动态变化, 被选中一次后会减一, 初始值和weight相等 代码相对比较简单, 如果只看实现可能理解不够深刻, 基于以上代码结构我们做一个例子分析, 假设我们有三个后端b1, b2, b3, 并且设置的初始权重如下:
  • b1: weight=3, current=3
  • b2: weight=2, current=2
  • b3: weight=1, current=1 我们发起7个请求, 分别为 r1..r7,那么每个请求的负载均衡选择过程如下:
请求当前next选择的后端选择后b1,b2,b3分别对应的current值选择后next值
r10b12 2 11
r21b22 1 12
r32b32 1 00
r40b11 1 01
r51b21 0 02
r62,0b10 0 01
r71,2,0,1b12 2 11

其中r6,r7中当前next值有多个表示经历了多次循环赋值, 当执行到r6请求时, 只有b1current大于0, 表示还可以被选中, 执行到r7请求时, 循环检查了所有的后端列表, 由于currnet全为0, 表示完成了一个轮次的权重轮询, 然后重置所有后端的current值为对应的weight值, 重新开始下一个轮次的循环。下面自己实现一次这个过程.

模拟实现

package main

import "fmt"

type Backend struct {
   Name    string
   Addr    string
   Weight  int
   Current int
}

type Wrr struct {
   Next        int
   BackendList []Backend
}

func (p *Wrr) Balance() Backend {
   var curBackend Backend

   next := p.Next
   for {
      curBackend = p.BackendList[next]
      if curBackend.Current > 0 {
         break
      }

      next = moveNext(next, len(p.BackendList))
      if next == p.Next {
         p.ResetCurrent()
         p.Next = 0
         next = 0
      }
   }
   p.BackendList[next].Current--
   p.Next = moveNext(next, len(p.BackendList))
   return curBackend
}

func (p *Wrr) ResetCurrent() {
   for i, _ := range p.BackendList {
      p.BackendList[i].Current = p.BackendList[i].Weight
   }
}

func mockBackendList() []Backend {
   return []Backend{
      {"b1", "127.0.0.1:80", 3, 3},
      {"b2", "127.0.0.1:81", 2, 2},
      {"b3", "127.0.0.1:82", 1, 1},
   }
}

func moveNext(curNext int, backendTotal int) int {
   next := curNext + 1
   if next > (backendTotal - 1) {
      next = 0
   }
   return next
}

func main() {
   wrr := Wrr{
      Next:        0,
      BackendList: mockBackendList(),
   }

   // 7次请求
   for r := 0; r < 7; r++ {
      b := wrr.Balance()
      fmt.Printf("当前选择: %s, 当前权重b1: %d, b2: %d, b3: %d \n", b.Name, wrr.BackendList[0].Current,
         wrr.BackendList[1].Current, wrr.BackendList[2].Current)
   }

}

执行结果如下:

image.png与上述表格分析结果一致。

思考及总结

在应用方面, 加权轮询算法在各种微服务的组件如nginx, lvs服务发现场景等有广泛应用。在业务场景中, 电商和游戏的抽奖算法也可以使用此算法。 在实用性方面, 从宏观角度看算法可以满足针对不同权重的实例调度, 但是从微观角度看, 在某些特殊的权重配置下, 可能存在瞬时的对于某个实例的集中调度, 这种不平滑的负载可能会使某些实例出现瞬时高负载的现象,导致系统存在宕机的风险。比如后端有11个实例, 记为b1...b11, 其中b1权重为10, b2...b11的权重都为1, 则在调度的后期流量全部集中于b1, 而我们期待的结果是类似于:

请求实例
r1b1
r2b2
r3b1
r4b3
r5b1
r6b4

这种均衡的调度, 所以引出了平滑加权轮询算法, 我们下期讨论 :)