百度开源网关BFE源代码阅读2之路由

文章目录

前文了解了BFE的启动流程,本文深入一下BFE的路由部分,当我们了解了BFE路由机制,就可以理解BFE的配置文件,也就可以使用BFE了。但是说实话,BFE的路由规则相对于其他产品有点反直觉,因为我使用的还不多,姑且这么说吧。

路由配置

配置路由的目录有两个

  • server_data_conf
  • cluster_conf

直接看配置文件还是比较难懂的,因为既没有注释,而且文件比较散,所以在了解BFE的路由概念之前想通过配置文件了解还是很难的,所以先看看代码吧

func main() {
    bfe_server.StartUp(config, version, *confRoot)
}

func StartUp() error {
	var err error
    bfeServer.InitDataLoad()
}

func (srv *BfeServer) InitDataLoad() error {
    // 1.
	serverConf, err := bfe_route.LoadServerDataConf(srv.Config.Server.HostRuleConf,
		srv.Config.Server.VipRuleConf, srv.Config.Server.RouteRuleConf,
		srv.Config.Server.ClusterConf)

    // 2.
	srv.ServerConf = serverConf
	srv.ReverseProxy.setTransports(srv.ServerConf.ClusterTable.ClusterMap())
	log.Logger.Info("init serverDataConf success")

    // 3.
	if err := srv.balTable.Init(srv.Config.Server.GslbConf,
		srv.Config.Server.ClusterTableConf); err != nil {
		return fmt.Errorf("InitDataLoad():balTableInit Error %s", err)
	}

    // 4.
	if srv.ServerConf != nil {
		ct := srv.ServerConf.ClusterTable
		srv.balTable.SetGslbBasic(ct)
		srv.balTable.SetSlowStart(ct)
	}
    
    // nameConf的加载

	return nil
}

代码分解如下:

  1. 加载server_data_conf目录下的配置文件
  2. 基于上一步的配置文件创建对应的Transport, 看过net/http的代码的人应该不默认,这个对象代表的传输层,相当于一个收发数据的抽象
  3. 加载cluster_conf目录下的配置文件
  4. 配置balTable, 一个后续用来路由的路由表对象,设置server_data_conf里设置的参数

LoadServerDataConf

这个目录下的配置文件主要配置HostnameHostTag的映射,hostTag到Product的映射,ProductClusterName的映射,以及一些负载均衡相关的配置,比如重试次数,负载均衡算法之类的。

func LoadServerDataConf(hostFile, vipFile, routeFile, clusterConfFile string) (*ServerDataConf, error) {
	s := newServerDataConf()

    // 1.
	if err := s.hostTableLoad(hostFile, vipFile, routeFile); err != nil {
		return nil, fmt.Errorf("hostTableLoad Error %s", err)
	}

    // 2.
	if err := s.clusterTableLoad(clusterConfFile); err != nil {
		return nil, fmt.Errorf("clusterTableLoad Error %s", err)
	}

    // 3.
	if err := s.check(); err != nil {
		return nil, fmt.Errorf("ServerDataConf.check Error %s", err)
	}

	return s, nil
}

func (s *ServerDataConf) hostTableLoad(hostFile, vipFile, routeFile string) error {
    // 4.
	hostConf, err := host_rule_conf.HostRuleConfLoad(hostFile)

    // 5.
	vipConf, err := vip_rule_conf.VipRuleConfLoad(vipFile)

    // 6.
	routeConf, err := route_rule_conf.RouteConfLoad(routeFile)

	// 7.
	s.HostTable.Update(hostConf, vipConf, routeConf)
	return nil
}

func (s *ServerDataConf) clusterTableLoad(clusterConf string) error {
    // 8.
	err := s.ClusterTable.Init(clusterConf)
	return nil
}

代码分解如下:

  1. 加载conf/server_data_conf/host_rule.data,conf/server_data_conf/vip_rule.data, conf/server_data_conf/route_rule.data三个配置文件的配置
  2. 加载conf/server_data_conf/cluster_conf.data配置文件
  3. 检查配置是否合法
  4. 加载host_rule.data
  5. 加载vip_rule.data
  6. 加载route_rule.data
  7. 将三者的数据整合起来
  8. 第2步的具体实现

要理解这些代码得对比着配置文件来看

host_rule.data

配置示例

{
    "Version": "init version",
    "DefaultProduct": null,
    "Hosts": {
        "exampleTag":[
            "example.org",
        ]
    },
    "HostTags": {
        "example_product":[
            "exampleTag"
        ]
    }
}

通过这个配置文件我们能得到Host -> HostTagHostTag ->Product的映射

一个host只能对应一个tag, 重复会报错

一个hostTag只能对应一个product, 重复不会报错

vip_rule.data

{
    "Version": "init version",
    "Vips": {
        "example_product": [
            "111.111.111.111"
        ] 
    }
}

通过这个配置文件我们能得到VIP -> Product的映射。

这里的VIP是指Proxy Protocol协议下的目标地址

可重复 不会报错

route_rule.data

{
    "Version": "init version",
    "ProductRule": {
        "example_product": [
            {
                "Cond": "req_host_in(\"example.org\")",
                "ClusterName": "cluster_example"
            }
        ]
    }
}

通过这个配置文件我们能得到Product -> ClusterName的映射。

这里只有AdvancedRule没有BasicRule, 两者比较大的区别在于前者是线性匹配,后者会构造一个树,通过最长匹配规则匹配路由,两者各有优缺点,前者最大的优点是比较好维护,后者相比较而言就稍微难维护一点,如果你不知道BasicRule, 那就使用AdvancedRule

总的来说这三个配置文件让我们得到了三个映射关系

  1. Host -> HostTag
  2. HostTag ->Product
  3. VIP -> Product
  4. Product -> ClusterName

基于这第1,2,4的映射关系,我们可以构造一颗前缀树, 当请求进来之后,通过查看Host: exmaple.org可以很快的找到对应的ProductHostTag, 之所以构造一颗前缀树是因为, Hostname或者域名支持模糊查询,这对于同时代理多个子域名的流量还是很有用的。

而第三个映射关系,是在传输中启用Proxy Protocol的时候才生效,这里不做过多说明。

前面的配置文件主要是定义了Hostname或者说域名到Product之间的关系,通过Product可以进而的找到其对应的ClusterName, 很显然,ClusterName不足以说明集群的相关配置,所以BFE还需要通过cluster_conf.data配置文件定义了集群的相关配置,下面看看它的配置。

cluster_conf.data

{
    "Version": "init version",
    "Config": {
        "cluster_example": {
            "BackendConf": {
                "TimeoutConnSrv": 2000,
                "TimeoutResponseHeader": 50000,
                "MaxIdleConnsPerHost": 0,
                "RetryLevel": 0
            },
            "CheckConf": {
                "Schem": "http",
                "Uri": "/healthcheck",
                "Host": "example.org",
                "StatusCode": 200,
                "FailNum": 10,
                "CheckInterval": 1000
            },
            "GslbBasic": {
                "CrossRetry": 0,
                "RetryMax": 2,
                "HashConf": {
                    "HashStrategy": 0,
                    "HashHeader": "Cookie:UID",
                    "SessionSticky": false
                }
            },
            "ClusterBasic": {
                "TimeoutReadClient": 30000,
                "TimeoutWriteClient": 60000,
                "TimeoutReadClientAgain": 30000,
                "ReqWriteBufferSize": 512,
                "ReqFlushInterval": 0,
                "ResFlushInterval": -1,
                "CancelOnClientClose": false
            }
        }
    }
}

这些配置定义了集群整体的配置,比如超时,健康检查等,以及比较重要的GslbBasic(这个配置定义了子集群之间的负载均衡)

代码如下:

s.clusterTableLoad(clusterConfFile)

func (s *ServerDataConf) clusterTableLoad(clusterConf string) error {
	err := s.ClusterTable.Init(clusterConf)
	return nil
}

func (t *ClusterTable) Init(clusterConfFilename string) error {
    // 1.
	t.clusterTable = make(ClusterMap)
    // 2.
	clusterConf, err := cluster_conf.ClusterConfLoad(clusterConfFilename)
    // 3.
	t.BasicInit(clusterConf)
	return nil
}

func (t *ClusterTable) BasicInit(clusterConfs cluster_conf.BfeClusterConf) {
	t.clusterTable = make(ClusterMap)

    // 4.
	for clusterName, clusterConf := range *clusterConfs.Config {
		// 5.
		cluster := bfe_cluster.NewBfeCluster(clusterName)
		// 6.
		cluster.BasicInit(clusterConf)
		// 7.
		t.clusterTable[clusterName] = cluster
	}

	t.versions.ClusterConfVer = *clusterConfs.Version
}

代码分解如下:

  1. 构造clusterTable,通过后面的变量名可以知道,其实只是一个Map,就是简单的映射关系

  2. 加载配置文件

  3. 初始化配置

  4. 依次遍历各个集群定义的配置

  5. 创建BfeCluster对象

  6. 初始化配置,内部逻辑就是复制配置文件里面的参数

  7. 加入定义好的Map里面

小结

至此,server_data_conf目录下的配置文件全部加载完成,我们得到了Hostname(域名)到产品(Product), 产品到集群的映射,以及集群的相关配置,下面看看集群内部是如何路由的。

srv.balTable.Init

前面的配置让我们可以通过Hostname找到集群,这一小节看看集群怎么将流量分发到后端去的。

// 1.
srv.balTable.Init(srv.Config.Server.GslbConf, srv.Config.Server.ClusterTableConf)

func (t *BalTable) Init(gslbConfFilename, clusterTableFilename string) error {
    // 1. 加载cluster_table.data, gslb.data配置文件
	gslbConf, backendConf, err := t.BalTableConfLoad(gslbConfFilename, clusterTableFilename)

    // 2. 初始化 gslb配置
	if err := t.gslbInit(gslbConf); err != nil {
		log.Logger.Error("clusterTable gslb init err [%s]", err)
		return err
	}

    // 3. 初始化后端配置
	if err := t.backendInit(backendConf); err != nil {
		log.Logger.Error("clusterTable backend init err [%s]", err)
		return err
	}
	return nil
}

代码这里就不过多深入了,配置加载的代码大部分的逻辑在于加载参数,校验参数,设置默认值。当然了,这里处理加载参数还有一个比较重要的对象要初始化,那就是BalTable,但是单纯看代码不容易看懂,所以下面继续看配置文件。

gslb.data

{
    "Clusters": {
        "cluster_example": {
            "GSLB_BLACKHOLE": 0,
            "example.bfe.bj": 100
        }
    },
    "Hostname": "",
    "Ts": "0"
}

上面的配置意思是, 集群cluster_example配置了两个子集群,两个子集群分别的权重是0和100. GSLB_BLACKHOLE是一个特殊的子集群名,代表黑洞,从名字也能看出来,就是流量直接丢弃。因为集群cluster_example 只有子集群example.bfe.bj有权重,所以流量全部会转发给它。让我们继续看看另一个配置文件。

cluster_table.data

{
    "Config": {
        "cluster_example": {
            "example.bfe.bj": [
                {
                    "Addr": "127.0.0.1",
                    "Name": "example_hostname",
                    "Port": 8181,
                    "Weight": 10
                }
            ]
        }
    }, 
    "Version": "init version"
}

上面的配置意思是,集群cluster_example关联了一个子集群列表,其中有子集群example.bfe.bj, 而子集群example.bfe.bj包含了一个后端列表,每个后端都需要定义四个字段, Addr, Name, Port, Weight, 值得注意的是后端的配置中也有权重

本文关于路由的部分,大致可以通过下面的yaml文件表达

products:
  example_product:
    exampleTag:
    - example.com
    clusters:
      - cond: "req_host_in(\"example.org\")"
        name: cluster_example

clusters:
  cluster_example:
    gslb:
    - GSLB_BLACKHOLE: 0
    - example.bfe.bj: 100
    backends:
      example.bfe.bj:
      - addr: 127.0.0.1
        name: example_hostname
        port: 8181
        weight: 10

一图胜千言,假设我们通过Hostname找到产品Product之后,那么后面的路由示意图如下

flowchart TD %% 产品到集群 A[Product] --> B1(ClusterA) A[Product] --> BB2(ClusterB) %% 集群通过GSLB在子集群间路由 B1 --> C1{GSLB} C1 --> D1[子集群1] C1 --> D2[子集群2] %% 子集群通过负载均衡算法在后端之间路由 D1 -->D11{WRR} D11 --> D111[后端1] D11 --> D112[后端2] D2 -->D21{WRR} D21 --> D211[后端1] D21 --> D212[后端2]

总结

BFE与其他负载均衡,比如Nginx,有两点比较大的区别,一是Hostname不会直接映射到集群,而是增加了HostTagProduct的抽象,二是,集群到后端之间还有GSLB的路由规则,也就是说集群下面还有一层子集群,子集群下面才是后端列表。

参考链接