每日一书|立于山巅,能抗住万亿级流量冲击的BFE

杜甫诗云:“读书破万卷,下笔如有神”。开发者多读书、读好书,能打好基础、掌握实践、答疑解惑、拓展视野。正基于此,2021年11月1日起,CSDN、《新程序员》推出“每日一书”栏目,为你推荐精选好书,助力你的开发工作如行云流水。

在云计算时代浪潮下,大规模、高并发的技术架构已成为主流。云计算的高速发展,离不开底层基础设施的创新与改进,传统七层负载均衡架构已无法满足复杂的网络集群。

在云时代巨量请求背景下,在网民数量和互联网流量井喷的时点,2012年,百度技术团队推出BFE平台

网上有一个著名的段子:百度一下,测试网络通不通。BFE 就是保障百度可用性口碑的关键支柱之一。

1 BFE是什么

  • 百度统一七层流量转发平台

  • 每日转发流量请求超万亿次

  • 为企业场景设计的现代七层负载均衡开源软件

  • 国内首个被CNCF接受的网络方向开源项目

BFE最初是Baidu Front End(百度统一前端)的缩写。BFE平台是百度统一的七层负载均衡接入转发平台,平台从2012年开始建设,截至2020年年底,平台每日转发的请求超过1万亿次,日峰值请求超过每秒1000万次查询。

 2014年,BFE平台的核心转发引擎基于Go语言重构,并于2015年1月在百度全量上线。BFE平台是全球第一个将Go语言用于负载均衡场景及大规模使用的项目。   

 2019年年初,BFE平台成功地支持了百度春晚红包项目。在本次项目中,BFE平台提供了亿级别的转发能力,在海量的流量下支持了HTTPS卸载,以及精确限流等关键能力,保证了活动的顺利进行。

扫描二维码关注公众号,回复: 13334988 查看本文章

2019年7月,BFE平台的转发引擎对外开源。英文全称更名为Beyond Front End(中文意为“超越前端”)。

2020年6月,BFE被CNCF(Cloud Native Computing Foundation,云原生计算基金会)接受为“沙盒项目”(Sandbox Project)。

BFE开源项目定位于现代的七层负载均衡系统,为工业级的使用场景而设计。

2 BFE的特点

  • 全球首个将Go语言用于大规模网络接入转发的项目

  • 为企业级场景设计:

图片

(1)源于Go语言的特性,BFE可以提供更高的安全性和稳定。

(2)配合良好的插件化设计,BFE支持功能的快速开发。

(3)内置的多租户设计,强大的路由转发模型支持,对于多数据中心和多容器云集群间的流量调度支持,这些特性使得BFE可以支持复杂的应用场景。

(4)内置大量的状态探针,可以更好掌控BFE转发引擎的运行状态。

3 BFE转发引擎重构的缘起

百度BFE平台最早于2012年年初上线并使用,那时的转发引擎主要基于一个名为Transmit的内部系统,Transmit基于C语言实现,是“多进程+Libevent”的模型。到了2013年年底,百度产生了重构转发引擎的想法,主要原因有如下几方面。

(1)平台化的需要。在BFE平台上线初期,只有十多个业务线使用它。而到2013年年底,已经扩展至几十个业务线了。原有的系统中没有多租户机制,不易做多业务的配置管理。另外,配置的格式是非结构化的,不易使用程序来生成和处理;原来系统的配置热加载机制也比较复杂。

(2)降低网络协议栈的维护成本。Transmit中的HTTP协议栈是百度自研的,我们在使用过程中发现了一些协议一致性方面的细节问题,维护成本较高。另外,在2013年年底,百度已经启动了对HTTPS的调研,需要在转发引擎上增加对HTTPS的支持。网络协议栈是反向代理系统的重要模块。从长期来看,维护完全自研的网络协议栈的成本很高。

(3)状态监控能力欠缺。一个工业级水平的转发系统需要有很强的状态监控能力,Transmit原有的监控信息较少,而且增加新的监控状态也比较困难。

(4)转发配置的维护难度较高。Transmit的转发配置主要使用正则表达式来描述。在实践中,我们发现正则表达式存在可维护性方面的问题。

2014年年初,百度确定基于Go语言来重构BFE转发引擎,并于2014年4月开始编写代码,2014年年底完成开发,2015年年初Go语言版本的转发引擎在百度完成全量上线。

4 BFE为什么要基于Go语言

在2014年年初对BFE重构做技术选型时,曾经考虑过以下两种技术路线。

(1)基于Nginx。这是业界普遍使用的方案,绝大多数企业的七层负载均衡是基于Nginx搭建的。

(2)基于Go语言。回到2014年,Go语言在国内的使用案例还比较少。这么多年来,不断有人问,你们为什么要选用Go语言。下面是当时的一些考虑。

a. 研发效率。使用过C和Python语言的人应该有这样的体会:Python语言的研发效率远高于C语言。我们的一个基本判断是,在未来很多年内,七层负载均衡仍然有很多功能需要开发。Go语言的研发效率接近Python语言,在快速交付功能上具有较大优势。

b. 稳定性。负载均衡对稳定性要求很高,如果负载均衡转发引擎崩溃了,无论数据中心内其他服务的稳定性有多高,用户都无法访问服务。对于使用C语言研发的系统来说,内存访问错误占比非常高,部分错误可以直接导致系统崩溃;C语言对错误缺乏保护机制。而对于Go语言来说,内存的回收是由系统负责的,开发者无须关注,这大大降低了问题发生的概率。另外,在Go语言中可以使用“异常捕捉恢复”机制来捕捉可能发现的Panic(Go语言中的异常)。

c. 安全性。从理论上讲,C语言编写的程序都具有缓冲区溢出的隐患,而这是很多恶意攻击可以成功的原因之一。Go语言的内存管理机制让缓冲区溢出方面的安全风险大大降低。

d. 代码可维护性。Go语言相对于C语言,以及Nginx中常用的Lua语言,代码的可读性和可维护性都更好。另外,在编写高并发程序方面,Go语言提供了协程机制,可以使用多线程模型来编写程序,不需要设计复杂的状态机,这降低了程序的编写难度。

e. 网络协议栈。对于一个负载均衡软件来说,网络协议栈是重要的考虑因素。BFE利用了Go系统库中成熟稳定的网络协议栈,其背靠Google在网络协议栈方面的强大实力。近年来,很多网络协议栈升级都由Google发起,如HTTP/2、QUIC。Go系统库中也很快提供了对新协议的支持。

从实践来看,我们在2014年所做的选择是非常正确的。基于Go语言重构的BFE引擎及时响应了百度内部业务对七层负载均衡的各种需求,并且长期保持稳定。自2015年年初全量上线以来,BFE引擎从来没有在线上环境中发生过崩溃。

当然,Go语言也有它的短板,和Nginx相比,基于Go语言实现的BFE引擎性能要差一些。这种性能方面的差距主要来自两方面。

(1)BFE没有在内存拷贝方面做极致优化。Nginx在内存拷贝方面做了端到端的极致优化,而内存拷贝是性能消耗的主要来源之一。出于对网络协议栈一致性方面的考虑,BFE尽量保持Go系统库网络协议栈实现的原貌,所以在内存拷贝方面多了一些消耗。

(2)BFE无法利用CPU亲和性(CPU Affinity)。Nginx可以通过“绑定CPU”的方式来减少进程切换代理的性能损耗。对于BFE来说,开发者只能控制Go协程,底层的线程是被系统所控制的,所以无法利用CPU亲和性来优化性能。

这里还有一点需要重点说明——Go语言的GC(Garbage Collection,垃圾回收)延迟对BFE研发的影响。在2014年,Go语言版本为1.3,GC延迟的问题非常严重,BFE的实测效果是:GC延迟达到了400ms,完全无法接受。为此,当时我们在BFE中引入了多进程轮转的机制,以降低GC延迟对于转发流量的影响(详情见第17章)。2017年年初在发布的Go 1.8版本中,GC延迟的问题有了较好的解决,大部分的GC延迟都降低到了1ms内,可以满足业务的要求。于是在2017年,我们从BFE中去掉了多进程轮转机制。

5 BFE转发引擎的主要设计思想

基于Go语言重写BFE转发引擎,绝不是仅换了一种编程语言。

在新版BFE中,团队从以下几方面考虑。

(1)对转发模型做了较大的修改,在引擎中明确引入了租户概念,可以基于主机名(Host Name)来区分租户(在各功能模块的配置中,也引入了对租户的区分)。另外,基于之前所发现的正则表达式的问题,为尽量减少对正则表达式的使用,我们设计了条件表达式机制。

(2)降低动态配置加载的难度。配置的动态加载是负载均衡软件的一个重要需求。除了升级可执行程序的场景,软件应可以持续运行,以保证流量转发的持续性。新版的BFE将配置分为“常规配置”和“动态配置”:常规配置仅在程序启动时生效;动态配置可在程序执行过程中动态加载。动态配置统一使用JSON格式,兼顾了程序读取和人工阅读的需求。另外,系统提供了统一的动态加载机制,在实现新的模块时可以直接使用。

(3)增强服务状态监控能力。在重构BFE时,我们同时编写了前端监控平台(Web-Monitor)框架:每个BFE运行实例可以通过独立的HTTP服务向外展现内部的执行状态;同时,增加新的内部状态非常简单,只需要一行代码(详情见7.1节)。

(4)将大存储功能转移到外部。在原有的实现中,类似“词典查找”这样的功能也包含在BFE内部。这样的模块在启动时,需要使用较长的时间来加载词典数据,不利于BFE程序快速启动。而BFE程序的快速启动能力,对于系统的稳定性至关重要。在发生故障的时候,一个需要几分钟才能启动的程序,其故障的恢复时间要长得多。为此,在重构BFE时,将词典查找功能改写为独立的词典服务,由BFE远程调用,这保证了BFE可以在数秒内完成重启,如图1所示。

图片
图1  BFE设计中将大存储功能转移到外部

以上这些内容将在《万亿级流量转发:BFE核心技术与实现》一书中给出详细说明。

6 BFE和相关开源项目的对比

对比角度包括以下几方面。

(1)开源项目定位。

(2)系统所提供的功能。

(3)系统所提供的扩展开发能力。

(4)系统的可运维性。

需要说明的是,由于这些项目都在活跃开发中,信息可能过期或有误,读者可通过这些开源项目的官方网站查看最新信息。

1.开源项目定位

在各开源项目的官网上对它们的定位描述如下。

(1)BFE: BFE是一个开源的七层负载均衡系统。

(2)Nginx: Nginx是HTTP服务、反向代理服务、邮件代理服务和通用TCP/UDP代理服务。

(3)Envoy: Envoy是开源的边缘和服务代理,为云原生应用而设计。

(4)Traefik: Traefik是先进的HTTP反向代理和负载均衡。

2.功能对比

下面从系统功能的角度对几个开源项目进行对比。

(1)协议支持。这四个系统都支持HTTPS和HTTP/2,并计划或正在实现对HTTP/3的支持。

(2)健康检查。

a.BFE和Nginx只支持“被动”模式的健康检查(Nginx商业版支持“主动”模式的健康检查)。

b.Envoy支持主动、被动和混合模式的健康检查。

c.Traefik只支持“主动”模式的健康检查。

(3)实例级别负载均衡。这四个系统都支持实例级别负载均衡。

(4)集群级别负载均衡。

a.BFE、Envoy、Traefik都支持集群级别负载均衡。

b.Nginx不支持集群级别负载均衡。

注意,Envoy基于全局及分布式负载均衡策略。

(5)对于转发规则的描述方式。

a.BFE基于条件表达式。

b.Nginx基于正则表达式。

c.Envoy支持基于域名、Path及Header的转发规则。

d.Traefik支持基于请求内容的分流。

3.扩展开发能力

七层负载均衡有较多的定制扩展开发需求。下面从系统扩展开发能力的角度对几个开源项目进行对比。

(1)使用的编程语言。

a.BFE和Traefik都基于Go语言开发。

b.Nginx使用C语言和Lua语言开发。

c.Envoy使用C++语言开发。

(2)可插拔架构。这四个系统都使用了可插拔架构。

(3)新功能开发成本。由于各编程语言的差异,BFE和Traefik的开发成本较低,Nginx和Envoy的开发成本较高。

(4)异常处理能力。由于各编程语言的差异,BFE和Traefik可以对异常(在Go语言中被称为Panic)进行捕获处理,从而避免程序的异常结束; 而Nginx和Envoy无法对内存等方面的错误进行捕获,这些错误很容易导致程序崩溃。

4.可运维性

可运维性对于系统在正式生产环境中的使用非常重要,下面从系统可运维性的角度对几个开源项目进行对比。

(1)内部状态展示。

a.BFE对程序内部状态提供了丰富的展示。

b.Nginx和Traefik提供的内部状态信息较少。

c.Envoy也提供了丰富的内部状态展示。

(2)配置热加载。

a.四个系统都提供了配置热加载功能。

b.Nginx配置生效须重启进程,并中断活跃长连接。

注意,Nginx商业版支持动态配置,在不重启进程的情况下热加载的配置可以生效。

7 内网流量调度机制

内网流量调度是BFE的重要功能,非常适用于多数据中心的复杂场景。

(一) 内网流量调度背景介绍

1.全局流量调度解决方案

经过多年建设,对于由IDC服务的业务流量,百度形成了两层的全局流量调度系统,如图2所示。

图片
图2 百度全局流量调度系统

(1)GTC,负责外网流量调度。基于DNS生效,将各运营商分布在各省的用户流量引导到合适的网络入口。在调度计算中,GTC要考虑外网带宽(容量和使用情况)、BFE平台转发资源(容量和使用情况)、用户到各带宽出口的接入质量(连通性和访问延迟)等因素。

(2)GSLB,负责内网流量调度。基于BFE生效,将到达各BFE集群的流量,按照权重转发到位于各数据中心的子集群。

2.外网流量调度

GTC负责在各网络入口间进行流量调度,其工作原理如图3所示。GTC包括以下3个主要步骤。

图片
  图3 GTC工作原理

(1)实时监控。由位于各地的监控节点持续向各外网接入点发送探测信号,并对各地与接入点之间的连通性和质量进行监控。如果发现异常,分布式的实时监控系统会在1min内将故障信号上报给调度系统。

(2)调度计算。实时流量采集系统从路由器获取实时的带宽使用情况,从七层负载均衡系统获取实时的每秒请求情况。调度系统根据实时流量数据和实时监控情况,配合全局网络模型,在1min内计算出新的调度方案。

(3)下发执行。调度系统将调度方案下发给DNS和HTTPDNS执行。由于DNS缓存的因素,客户端的生效需要一定的时间。在百度,大部分域名的DNS TTL(Time To Live,生存时间)被设置为300s(即5min)。一般在下发后,要经过8~10min才能完成90%以上用户的生效。

和前一代外网调度系统相比,GTC有以下两方面的提升。

(1)加快了外网故障处理速度。通过“实时监控+自动调度计算”,从故障发生到启动DNS下发,时间压缩至2min以内。

(2)降低了配置维护成本。不需要针对域名维护复杂的预案。

业内很多类似系统均采用预案机制,例如,如果存在A和B两个备选的外网IP,预案会这样写:如果A出问题,就把流量切换到B;如果B出问题,就把流量切换到A。对于每个直接分配了IP地址的域名(也就是写为A记录的域名),都需要准备这样一个预案。

预案机制的最大问题是维护成本很高。首先,维护成本和外网出口的数量成指数关系。如果有2个出口,预案则非常简单;如果有5个甚至10个出口,预案则是非常不好写的,需要考虑各种可能性。另外,维护成本和域名的数量呈线性关系。假设有几千个域名,这时如果要对带宽出口进行调整(增加或删除一个出口),那么工作量会很惊人。

外网流量调度主要适用于以下场景。

(1)网络入口故障,是指由于网络入口本地或运营商网络的故障,导致用户无法访问网络入口。

(2)网络入口由于攻击导致拥塞。大规模的DDoS攻击可以达到数百G,甚至达到T级别,直接将网络入口的入向带宽打满。

(3)网络接入系统故障,如四层负载均衡系统或七层负载均衡系统的故障。

(4)分省连通性故障。这类故障是指,虽然从总体上看网络入口可以访问,但是某个运营商负责的某个地区的网络则无法访问。这部分是由于用户所在地区出现网络局部异常,也可能是由于服务所使用的IP在局部地区被误封。

3.为什么需要引入内网流量调度

在多数据中心的场景下,如果没有内网流量调度机制,当一个数据中心内的服务发生故障时,则只能通过改变域名对应的IP地址将流量调度到另一个数据中心,如图4所示。如上所述,在改变权威DNS的配置后,需要8~10min才能完成90%以上用户的生效。在完成切换之前,原来由故障IDC所服务的用户都无法使用服务,而且运营商的Local DNS数量很多,可能存在有故障或不遵循DNS TTL的Local DNS,从而导致对应的用户使用更长的时间完成切换,甚至一直都不切换。

图片
图4  使用外网DNS处理服务故障

在引入内网流量调度机制后,可以通过修改BFE的配置将流量从有故障的服务集群切走,如图5所示。在百度内部,配合自动的内网流量调度计算模块,在感知故障后,流量的调度可在30s内完成。和完全依赖外网流量调度机制相比,故障止损时间大幅缩短,从8~10min缩短至30s内。而且,由于执行调度的BFE集群都在内部,内网流量调度的可控性也比基于DNS的外网流量调度要好得多。

图片
图5  使用内网流量调度机制处理服务故障

(二)内网流量调度工作机制

本节介绍内网流量调度工作机制。首先,介绍内网流量调度的基本工作原理;然后介绍内网“自动”流量调度,这也是将自动化用于网络接入场景的一个案例;最后,通过一个示例场景来进一步说明内网流量调度。

1.基本工作原理

内网流量调度的工作原理如图6所示,其基本原理非常简单。在每个BFE集群中,针对一个服务集群的每个后端子集群分配一组权重。在流量转发时,BFE按照这个权重来决定请求的目标子集群。

图片
图6  内网流量调度的工作原理

另外,对每个服务集群,还包含一个虚拟的子集群,被称为Blackhole(黑洞)。在黑洞子集群对应的权重不为0的情况下,分给黑洞子集群的流量会被BFE主动丢弃。在到达BFE的流量超过服务集群总体容量的情况下,可以启用黑洞子集群来防止服务集群的整体过载。

内网流量调度适用于和内部服务相关的场景,具体场景分析如下。

(1)内部服务故障。在某些场景(如服务的灰度发布)下,单个服务子集群可能出现故障,从而导致服务容量下降甚至完全无法提供服务,这时可以通过内网流量调度快速完成止损处理。

(2)内部服务压力不均。具体包括以下两种可能场景。

场景1:如图7所示,某个地区的用户流量突增,导致单个数据中心内的子集群服务压力超过容量,这时可以将部分流量调度到其他子集群来服务。

图片
图7  场景1:某个地区的用户流量突增

场景2:如图8所示,由于外网故障处理,外网将部分流量从一个网络入口调度到另一个网络入口,导致相关子集群压力超过容量,这时也可以将部分流量调度到其他子集群来服务。

图片
图8  场景2:外网故障处理导致流量变化

2.内网“自动”流量调度

内网流量调度的权重可以手工设置,但实际上这个权重不应该是固定的,而应随着用户流量、服务集群的容量及机房间的连通性情况等因素的变化而调整。为此,在百度内实现了一个内网流量的调度器,用于对分流的权重比例进行计算。

以下是内网流量调度总体机制的具体介绍。

(1)流量采集。基于BFE的访问日志,实时获取到达各BFE集群的各服务的流量。

(2)权重计算。根据流量、各服务集群的容量、各数据中心网络连通性/距离等因素,计算各BFE集群向各服务集群的分流权重,如图9所示。

(3)下发执行。由各BFE集群按照分流权重执行转发。

图片

图9  GSLB调度器的输入和输出

目前在BFE开源项目中,仅支持内网流量调度权重的手工设置,未包含内网“自动”流量调度的相关模块。

3.示例场景

示例场景如图10所示,其中包含两个IDC(IDC 1和IDC 2)、两个BFE集群(BFE_1和BFE_2),同时后端集群有两个子集群(SubCluster_1和SubCluster_2)。另外,还有一个虚拟的黑洞集群,用于主动丢弃流量。

图片
图10  内网流量调度示例场景

针对BFE集群,可以设置子集群的分流比例,举例如下。

(1)BFE_1集群的分流配置为

{SubCluster_1: W11,SubCluster_2: W12, Blackhole: W1B}

(2)BFE_2集群的分流配置为

{SubCluster_1: W21,SubCluster_2: W22, Blackhole: W2B}

BFE实例根据上述配置做WRR调度,向子集群转发请求。例如,当BFE_1的分流配置{W11, W12, W1B}为{45, 45, 10}时,BFE_1转发给SubCluster_1、SubCluster_2、Blackhole的流量比例依次为45%、45%、10%。

通过修改上述配置,可以将流量在不同子集群之间切换,实现负载均衡、快速止损和过载保护等目的。

(三)内网转发的其他机制

1.失败重试机制

BFE在转发时支持以下两种失败重试机制,如图11所示。

图片
图11  BFE失败重试机制

(1)同子集群重试。在一次转发失败后,选择原目标子集群内的其他服务实例进行重试。同子集群内重试的最大次数可以通过配置集群的参数同子集群重试次数来控制。

(2)跨子集群重试。在转发失败后,在原目标子集群之外,使用另一个子集群进行重试。跨子集群重试的最大次数可以通过配置集群的参数跨子集群重试次数来控制。

在转发失败后,BFE会首先尝试同子集群重试(如果同子集群重试次数大于0),然后尝试跨子集群重试(如果跨子集群重试次数大于0)。

启用跨子集群重试功能时要非常小心,因为这个功能在某些场景下可能会将过量的流量转移到其他健康的集群中,从而导致这些集群的压力过大,甚至被压垮。和上面“内网‘自动’流量调度”中按照权重将流量转发到各子集群中的机制不同,跨子集群重试所引发的流量压力有一定的不可控性。

BFE并不会在所有请求失败的情况下都进行重试。如果BFE感知到下游实例已经读取了请求(即使没有完整读取),那么它也不会再去重试。在这种情况下,BFE无法确认下游实例是否已经处理了请求,如果再次发送则可能导致状态错误,所以采取了比较保守的策略。

2.连接池

BFE和下游实例的连接支持两种方式。

(1)短连接方式。BFE在每次向下游实例转发请求时,均需要建立新的TCP连接。

(2)连接池方式。BFE为每个下游实例维护一个连接池。

a.当BFE需要向某个下游实例转发请求时,如果连接池中有idle(空闲)连接,则复用这个连接;如果连接池中没有idle连接,则会建立一个新的TCP连接。

b.当BFE处理完一个请求时,如果连接池中的idle连接数量小于连接池的大小,则将当前使用的连接放入连接池;如果连接池中的idle连接数量大于或等于连接池的大小,则关闭当前使用的连接。

使用连接池的方式,可以避免新建TCP连接所导致的延迟,从而降低总转发延迟。由于BFE需要对每个下游实例都保持长连接,在某些情况(如BFE的实例数较大)下,可能导致下游实例的并发连接数较多。在使用连接池和设置连接池的参数时,需要结合以上因素综合考虑。

3.会话保持

BFE向下游转发请求时,支持将相同来源请求转发至固定的业务后端(某个子集群或某个实例),这个功能被称为会话保持。

在执行会话保持时,BFE可以基于以下请求来源进行标识。

(1)请求来源IP。

(2)请求特定头部,例如请求Cookie等。

BFE支持以下两种会话保持级别。

(1)子集群级别。相同来源的请求被转发至固定的业务子集群(注意,这里是指子集群中的任意实例)。

(2)实例级别。相同来源的请求被转发至固定的业务实例。

本文摘自《万亿级流量转发:BFE核心技术与实现》

图片

(声明:本文转载自“博文视点Broadview”微信公众号。)

猜你喜欢

转载自blog.csdn.net/programmer_editor/article/details/121423720