BFE负载均衡源码--转发模型

727 阅读7分钟

前言

负载均衡技术在服务端是一个比较关键的架构环节, 可分为硬件负载均衡和软件负载均衡, 硬件负载均衡如F5, 软件负载均衡基于OSI网络模型又可分为四层负载均衡和七层负载均衡。硬件负载均衡器和四层负载均衡更多在于数据流量的转发与服务防护, 如负载均衡、应用交换、会话交换、状态监控, 拒绝服务(DoS)攻击和SYN Flood保护等。七层负载均衡可以通过解析数据包中的应用协议, 做更加灵活的转发处理。

BEF是百度内部基于golang开发, 经过了充分的生产环境实践, 被CNCF(云原生计算基金会)接受为“沙盒项目”(Sandbox Project), 相关学习文档总结如下:

  • git地址: github.com/bfenetworks…
  • 项目官网: www.bfe-networks.net/zh_cn/ 项目官网有详细的安装使用说明, github中有详细设计文档介绍, 其中并未对源码做介绍。我们可以基于源码来更好的了解其实现原理。同时也可以学习一下golang比较好的实践, 提升自己的编码能力。

工程结构

我下载的源码版本是: 1.3.0, 根目录下按照功能组织划分有如下工程组织结构:

image.png

转发模型

名词解释

在通过源码分析时我们需要先了解其设计中的一些概念。

  • 租户(Tenant): 在云原生时代以租户为标准进行业务和资源的隔离是非常重要的需求, 可以理解为在平台上的不同账号
  • 集群(Cluster): 具有同样功能的后端服务可以定义为一个集群, 一个租户可以有多个集群, 比如我们有一个User服务, 所有提供此服务的实例组成User服务集群
  • 子集群(Sub Cluster): 还是使用User服务距离, 所有User服务组成的集群可能被部署在不同的IDC中, 其中某一个IDC内的User服务称为User集群的自己群
  • 实例(Instance): 即服务的具体具体载体, 可以使容器, 虚拟机, 物理机等, 具有IP + PORT 标识

转发流程

基于以上理解, 我们使用一个案例来表示其转发流程, 加入我们有一个租户为用户中心中台业务, 命名为: UserCenterTenant,在此业务中维护了一个域名: user_center.com, 此域名下提供用户中心的服务, 我们使用3个IDC来承载用户中心的服务, 则这3个IDC提供的用户中心服务共同组成用户中心集群, 我们命名为: UserCenterCluster, 其中每一个IDC为UserCenterCluster集群下的子集群, 我们命名为: UserCenterSubClusterN, 其中N为IDC编号,由此确定:

  • 租户: UserCenterTenant
  • 集群: UserCenterCluster
  • 子集群: UserCenterSubCluster1, UserCenterSubCluster2, UserCenterSubCluster3 我们以一个图示来表示其转发流程:

image.png

源码分析

基于上图转发流程, 我们可以看到重点是如何找到这个域名对应的实例并且处理和返回。具体的处理流程实现在bfe_server/reverseproxy.go:ServeHTTP()方法中。

  1. 确定租户 调用findProduct->LookupHostTagAndProduct方法查找租户。由于历史原因, 租户在实现中称为Product, 即产品线。
func (t *HostTable) LookupHostTagAndProduct(req *bfe_basic.Request) error {
   hostName := req.HttpRequest.Host

   // 通过Host名称和租户映射关系查找
   hostRoute, err := t.findHostRoute(hostName)

   // 查找失败再尝试通过Vip和租户映射关系查找
   if err != nil {
      if vip := req.Session.Vip; vip != nil {
         hostRoute, err = t.findVipRoute(vip.String())
      }
   }

   // 如果查找失败, 则使用默认租户
   if err != nil && t.defaultProduct != "" {
      hostRoute, err = route{product: t.defaultProduct}, nil
   }

   // 将租户信息设定在req指针向后传递
   req.Route.HostTag = hostRoute.tag
   req.Route.Product = hostRoute.product
   req.Route.Error = err

   return err
}

// 通过Host查找租户
func (t *HostTable) findHostRoute(host string) (route, error) {
   if t.hostTrie == nil {
      return route{}, ErrNoProduct
   }

   host = strings.ToLower(host)
   // 查找时会将Host字符串反转, 如 www.baidu.com -> moc.udiab.www, 原因我们下面文字分析
   match, ok := t.hostTrie.Get(strings.Split(string_reverse.ReverseFqdnHost(hostnameStrip(host)), "."))
   if ok {
      // get route success, return
      return match.(route), nil
   }

   return route{}, ErrNoProduct
}

BFE中Host与租户的对应关系是通过字典树结构来保存,同时在增加和查找字典树时会将Host字符串逆转, 这是为什么呢? 我们知道字典树通过存储公共节点来压缩数据, 在Host场景中, 经常会有二级, 三级域名出现, 例如: zhidao.baidu.com, news.baidu.com等, 所以在构建字典树时需要逆序构建, 以顶级域名: com, cn类似字符串做根节点。BFE中实现了一个简单的字典树结构, 通过递归进行元素的增加和查找, 如: www.baidu.com, news.baidu.com, global.didi.com三个域名在Trie中的存储方式如图:

image.png 每个节点对应的entry元素结构便是租户信息:

type route struct {
   product string // 租户名称
   tag     string // 标签
}
  1. 确定集群 在查找集群时首先使用路由查找, 查找失败再尝试使用基于协议的规则条件进行匹配, 具体如下:
func (t *HostTable) LookupCluster(req *bfe_basic.Request) error {
   var clusterName string

   // 基于路由查找
   basicRules, ok := t.productBasicRouteTree[req.Route.Product]
   if ok {
      host := strings.SplitN(req.HttpRequest.Host, ":", 2)[0]
      path := ""
      /*** 省略 ***/
      clusterName, found := basicRules.Get(host, path)               /*** 省略 ***/
   }

   // 基于规则查找
   rules, ok := t.productAdvancedRouteTable[req.Route.Product]
   /*** 省略 ***/
   for _, rule := range rules {
      if rule.Cond.Match(req) {
         clusterName = rule.ClusterName
         break
      }
   }
   /*** 省略 ***/
}

首先是基于路由查找, 和gin, echo等web框架类似都使用了radix tree作为路由查找的算法, 优先查找精确匹配规则, 否则按照最长前缀原则匹配路由集群, 源码如下:

func (pt *pathTrees) get(path string) (string, bool) {
   // 精确匹配
   if value, found := pt[treeMatchExact].Get(path); found {
      return value.(string), true
   }
   /*** 省略 ***/
   // 模糊匹配
   if _, value, found := pt[treeMatchWildcard].LongestPrefix(path); found {
      return value.(string), true
   }

   return "", false
}
  1. 确定子集群 根据集群名称获取到集群对应的子集群列表, 然后根据加权轮询算法计算将要咋混发的子集群, 核心编码为:
for i := 0; i < len(bal.subClusters); i++ {
   subCluster = bal.subClusters[i]
   if subCluster.weight <= 0 {
      continue
   }
   w -= subCluster.weight
   // got it
   if w < 0 {
      break
   }
}
  1. 确定转发实例 通过子集群可以查找到真正的后端服务的实例列表, 然后通过负载均衡算法确定转发实例, BFE支持五种负载均衡算法:
  • WrrSimple: 普通加权轮询
  • WrrSmooth: 平滑加权轮询
  • stickyBalance: 会话保持算法
  • WlcSimple: 普通最小连接数算法
  • WlcSmooth: 平滑最小连接数算法 这里也是BFE作为负载均衡最重要的核心逻辑, 也是业界比较成熟的算法, 下期开始我们单独学习这五种负载均衡算法。
func (brr *BalanceRR) Balance(algor int, key []byte) (*backend.BfeBackend, error) {
   /*** 省略 ***/
   switch algor {
   case WrrSimple:
      return brr.simpleBalance()
   case WrrSmooth:
      return brr.smoothBalance()
   case WrrSticky:
      return brr.stickyBalance(key)
   case WlcSimple:
      return brr.leastConnsSimpleBalance()
   case WlcSmooth:
      return brr.leastConnsSmoothBalance()
   default:
      return brr.smoothBalance()
   }
}

思考

通过学习BFE转发模型, 可以学习到非常多和我们工作中相关的知识, 比如加权轮询算法在游戏的抽奖场景中经常使用, 条件表达式在一些高度规则化的业务中也经常被使用到, 比如在运营配置活动的场景中可以根据不同的用户状态展示不同的业务弹窗, 通过规则配置避免重复的开发工作和冗余的编码结构等。

总结

要理解BFE转发模型实现首先要理解其设计理念, 通过租户机制实现配置和资源的隔离, 可以非常方便的在云生时代落地。在数据存储及查找方面:

  • Host与租户关系使用Trie字典树实现, 通过递归进行查找
  • 租户与集群的关系使用map做映射, 首先查找是否精确匹配, 查找失败再通过条件表达式匹配集群
  • 查找子集群会使用WRR加权轮训算法选取子集群
  • 核心负载均衡算法 其中五种核心的负载均衡算法我们做单篇的详细解读和学习, 刷到的小伙伴可以点个赞支持一下 :)