百度开源网关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
}
代码分解如下:
- 加载
server_data_conf
目录下的配置文件 - 基于上一步的配置文件创建对应的
Transport
, 看过net/http
的代码的人应该不默认,这个对象代表的传输层,相当于一个收发数据的抽象 - 加载
cluster_conf
目录下的配置文件 - 配置
balTable
, 一个后续用来路由的路由表对象,设置server_data_conf
里设置的参数
LoadServerDataConf
这个目录下的配置文件主要配置Hostname
到HostTag
的映射,hostTag
到Product的映射,Product
到ClusterName
的映射,以及一些负载均衡相关的配置,比如重试次数,负载均衡算法之类的。
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
}
代码分解如下:
- 加载
conf/server_data_conf/host_rule.data
,conf/server_data_conf/vip_rule.data
,conf/server_data_conf/route_rule.data
三个配置文件的配置 - 加载
conf/server_data_conf/cluster_conf.data
配置文件 - 检查配置是否合法
- 加载
host_rule.data
- 加载
vip_rule.data
- 加载
route_rule.data
- 将三者的数据整合起来
- 第2步的具体实现
要理解这些代码得对比着配置文件来看
host_rule.data
配置示例
{
"Version": "init version",
"DefaultProduct": null,
"Hosts": {
"exampleTag":[
"example.org",
]
},
"HostTags": {
"example_product":[
"exampleTag"
]
}
}
通过这个配置文件我们能得到Host -> HostTag
和HostTag ->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
吧
总的来说这三个配置文件让我们得到了三个映射关系
- Host -> HostTag
- HostTag ->Product
- VIP -> Product
- Product -> ClusterName
基于这第1,2,4的映射关系,我们可以构造一颗前缀树, 当请求进来之后,通过查看Host: exmaple.org
可以很快的找到对应的Product
和HostTag
, 之所以构造一颗前缀树是因为, 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
}
代码分解如下:
-
构造
clusterTable
,通过后面的变量名可以知道,其实只是一个Map,就是简单的映射关系 -
加载配置文件
-
初始化配置
-
依次遍历各个集群定义的配置
-
创建
BfeCluster
对象 -
初始化配置,内部逻辑就是复制配置文件里面的参数
-
加入定义好的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
之后,那么后面的路由示意图如下
总结
BFE
与其他负载均衡,比如Nginx,有两点比较大的区别,一是Hostname
不会直接映射到集群,而是增加了HostTag
和Product
的抽象,二是,集群到后端之间还有GSLB
的路由规则,也就是说集群下面还有一层子集群,子集群下面才是后端列表。