作者:vivo 互联网运维团队- Duan Chengping
在大规模业务场景中,已经不可能通过单机提供业务,这就衍生出了负载均衡的需求。为了满足合适可靠的负载,本文将从简单的基础需求出发,一步步推进并解释如何建立负载均衡平台。
想一个问题:假设你有10台服务器对外提供相同的服务,你如何保证这10台服务器能稳定处理外部请求?
这里可能有很多种解决方案,但本质上都是处理下述两个问题:
① 客户端的请求应该分配去哪一台服务器比较好?
② 万一其中某些服务器故障了,如何隔离掉故障服务器?
问题① 处理不好,可能会导致10台服务器中的一部分服务器处于饥饿状态,没有被分配客户端请求或者是分配得很少;而另一部分则一直在处理大量的请求,导致不堪负重。
问题② 处理不好,则CAP原则中的可用性(A)可能就没法保证,除非系统不需要A。
要解决上述问题,你必须实现一套控制器,能调度业务请求和管理业务服务器。很不幸的是,大多数情况下这个控制器往往就是整个系统的瓶颈。因为控制系统如果不深入到客户端上,就必须依赖一个集中式的决策机构,这个机构必然要承载所有客户端的请求。这时候你又得去考虑这个控制器的冗余和故障隔离的问题,这就变得无休无止。
那么,如何解决上述问题?
那就是专业的事情交给专业平台去做,即,我们需要独立的负载均衡提供上述2点的解决方案。
对于客户端来说,每次请求一个站点,最终都会转变成对某个IP发起请求。所以只要能控制客户端访问的IP地址,我们就能控制请求应该落到哪个后端服务器上,从而达到调度效果,这是DNS在做的事情。或者,劫持客户端所有请求流量,对流量重新分配请求到后端服务器上。这个是Nginx、LVS等的处理方式。
图1、通过DNS实现负载均衡的效果示意图
图2、通过LVS/Nginx实现负载均衡的效果示意图
这两个方式都能达到负载均衡的效果。但这里面有个严重的问题,DNS、Nginx、LVS等服务在互联网时代不可能单机就能提供业务,都是集群式(也就是有N台服务器组成),那这些集群的可靠性和稳定性又该如何保证呢?
DNS主要负责域名解析,有一定的负载均衡效果,但往往负载效果很差,不作为主要考虑手段。Nginx提供7层负载均衡,主要靠域名来做业务区分和负载。LVS是4层负载均衡,主要靠TCP/UDP协议、IP地址、TCP/UDP端口来区分服务和负载。
为了解决Nginx、LVS这些负载均衡器集群的负载均衡及可靠性,我们可以做下述简单的方案:
业务服务器的负载和可靠性由Nginx保障;
Nginx的负载和可靠性由LVS保障。
上述方案是遵循了业务 <-- 7层负载 <-- 4层负载的逻辑,实际上是在网络分层模型中的应用层 <-- 传输层的做两级负载。可以看出,其实这个方案是用另一层负载均衡来解决当前层级的负载和可靠性的问题。但这个方案还是有问题,业务和Nginx集群这两层的负载和可靠性是有保障了,但LVS集群这一层的可靠性怎么办?
既然我们在网络分层模型中应用层 <-- 传输层做了两级负载,那有没有可能做到应用层 <-- 传输层 <-- 网络层的三级负载?很幸运的是,基于IP路由的方式,网络设备(交换机、路由器)天然具备了网络层负载均衡功能。
到此,我们可以实现整个负载均衡链条:业务 <-- 7层负载(Nginx) <-- 4层负载(LVS) <-- 3层负载(NetworkDevices);
从这里可以看出,要确保整个负载均衡体系是有效可靠的,必须从网络层开始构筑。处于高层级的业务,可以为低层级的业务提供负载。相对于低层级,高层级的业务都可以认为是低层级业务的控制面,可以交给专业团队去实现和管理,低层级业务侧只需要关注业务本身实现即可。
图3、网络7层模型和LVS、Nginx之间的对应关系
网络7层分层模型说明:
7、应用层: 支持网络应用,应用协议仅仅是网络应用的一个组成部分,运行在不同主机上的进程则使用应用层协议进行通信。主要的协议有:HTTP、FTP、Telnet、SMTP、POP3等。
6、表示层: 数据的表示、安全、压缩。(实际运用中该层已经合并到了应用层)
5、会话层: 建立、管理、终止会话。(实际运用中该层已经合并到了应用层)
4、传输层: 负责为信源和信宿提供应用程序进程间的数据传输服务,这一层上主要定义了两个传输协议,传输控制协议即TCP和用户数据报协议UDP。
3、网络层: 负责将数据报独立地从信源发送到信宿,主要解决路由选择、拥塞控制和网络互联等问题。
2、数据链路层: 负责将IP数据报封装成合适在物理网络上传输的帧格式并传输,或将从物理网络接收到的帧解封,取出IP数据报交给网络层。
1、物理层: 负责将比特流在结点间传输,即负责物理传输。该层的协议既与链路有关也与传输介质有关。
上面说过,3层负载由网络设备天然提供,但实际使用中是和4层负载紧耦合的,一般不独立提供服务。4层负载可以直接为业务层提供服务,而不依赖7层负载(7层负载主要面向HTTP/HTTPS等业务),所以我们这里主要针对4层负载来讲。
实现负载均衡,言外之意就是实现流量重定向,那么,首要解决的问题是如何转发流量。
4个问题要解决:
① 如何把客户端的流量吸引到负载均衡器上?
② 负载均衡器如何选择合适的后端服务器?
③ 负载均衡器如何将请求数据发送到后端服务器上?
④ 后端服务器如何响应请求数据?
对于①,
解决方案很简单,给一批后端服务器提供一个独立的IP地址,我们称之为Virtual IP(也即VIP)。所有客户端不用直接访问后端IP地址,转而访问VIP。对于客户端来说,相当于屏蔽了后端的情况。
对于②,
考虑到处于低层级的负载均衡通用性,一般不做复杂的负载策略,类似RR(轮询)、WRR(带权重轮询)的方案更合适,可以满足绝大多数场景要求。
对于③,
这里的选择会往往会影响这④的选择。理想情况下,我们期望是客户端的请求数据应该原封不动的发送到后端,这样能避免数据包被修改。前面提到的网络七层分层模型中,数据链路层的转发可以做到不影响更上层的数据包内容,所以可以满足不修改客户端请求数据的情况下转发。这就是网络常说的二层转发(数据链路层,处在七层网络模型中的第二层),依靠的是网卡的MAC地址寻址来转发数据。
那么,假设把客户端的请求数据包当成一份应用数据打包送到后端服务器是不是可行?该方式相当于负载均衡器和后端建立了一个隧道,在隧道中间传输客户端的请求数据,所以也可以满足需求。
上述两种解决方案中,依赖数据链路层转发的方案称之为直接路由方式(Direct Route),即DR模式;另一种需要隧道的方案称之为隧道(Tunnel)模式。DR模式有一个缺点,因为依赖MAC地址转发,后端服务器和负载均衡器必须在同一个子网中(可以不严谨认为是同一个网段内),这就导致了只有和负载均衡服务器在同子网的服务器能接入,不在同一个子网的这部分就没有使用负载均衡的机会了,这显然不可能满足当前大规模的业务。Tunnel模式也有缺点:既然数据转发依赖隧道,那就必须在后端服务器和负载均衡器之间建立隧道。如何确保不同业务人员能正确在服务器上配置隧道,并且能监控隧道正常运行,难度都很大,需要一套完成的管理平台,言外之意即管理成本过高。
图4、DR模式的转发示意图,响应流量不会经过负载均衡器
图5、Tunnel模式转发示意图,和DR一样,响应流量不会经过负载均衡器
既然都不是很理想,那还有没有其他方案?
我们期望的是后端服务器不感知前端有负载均衡存在,服务可以放在任何地方,且不做任何过多配置。既然做不到原封不动传送客户端的数据包,那就代理客户端的请求。也就是对客户端发出来的数据包的源IP和目的IP分别做一次IP地址转换。
详细来说,负载均衡器收到客户端A的请求后,自己以客户端的角色向后端服务器发起相同的请求,请求所带的payload来自于客户端A的请求payload,这样保证了请求数据一致。
此时,负载均衡器相当于发起了一个新连接(不同于客户端A发起的连接),新建的连接将会使用负载均衡器的IP地址(称之为LocalIP)作为源地址直接和后端服务器IP通信。
当后端返回数据给负载均衡器时,再用客户端A的连接将数据返回给客户端A。整个过程涉及了两个连接,对应两次IP地址转换,
请求时刻:CIP→VIP, 转换成了LocalIP→后端服务器IP,
数据返回时刻:后端服务器IP→ LocalIP, 转换成了VIP→ CIP。
客户端该过程完全基于IP地址转发数据,而不是MAC地址,只要网络可达,数据就可以顺畅在客户端和后端之间传输。
图6、FULLNAT转发模式
上述方案称之为FULLNAT转发模式,也就是实现了两次地址转换。显然,这种方式足够简单,不需要后端服务器做任何调整,也不限制后端部署在何处。但也有个明显问题,那就是后端看到的请求全部来自于负载均衡器,真实的客户端IP信息完全看不到了,这就相当于屏蔽了真实客户端的情况。幸运的是数据中心中绝大多数应用并不需要明确知道真实客户端的IP地址信息,即便是需要真实客户端IP信息,负载均衡器也可以将这部分信息加载在TCP/UDP协议数据中,通过按需安装一定的插件获取。
综合上述几种方案来说,FULLNAT模式对我们的业务场景(非虚拟化环境)适合度是最佳的。
既然我们打算使用FULLNAT模式, 则④的解决就没有任何困难了,因为负载均衡器间接充当了客户端角色,后端的数据必然要全部转发给负载均衡器,由负载均衡器再发给真正的客户端即可。
表1:各种模式之间的优劣势分析
负载均衡一般提供对后端的健康检查探测机制,以便能快速剔除异常的后端IP地址。理想情况下,基于语义的探测是最好的,能更有效的检测到后端是不是异常。但这种方式一来会带来大量的资源消耗,特别是后端庞大的情况下;这还不是特别严重的,最严重的是后端可能运行了HTTP、DNS、Mysql、Redis等等很多种协议,管理配置非常多样化,太复杂。两个问题点加起来导致健康检查太过于笨重,会大量占用本来用于转发数据的资源,管理成本过高。所以一个简单有效的方式是必要的,既然作为4层负载,我们都不去识别上游业务是什么,只关注TCP或者UDP端口是不是可达即可。识别上层业务是什么的工作交给Nginx这类型的7层负载去做。
所以只要定期检查所有后端的TCP/UDP端口是否开放,开放就认为后端服务是正常的,没有开放就认为是服务异常,后端列表中剔除,后续将不再往该异常后端转发任何数据。
那么如何探测?既然只是判断TCP端口是不是正常开放,我们只要尝试建立一个TCP连接即可,如果能建立成功,则表明端口是正常开放的。但是对于UDP来说,因为UDP是无连接的,没有新建连接的这种说法,但同样能够靠直接发送数据来达到探测的目的。即,直接发送一份数据,假设UDP端口是正常开放的,所以后端通常不会做响应。如果端口是没有开放的,操作系统会返回一个icmp port unreachable状态,可以借此判断端口不可达。但有个问题,如果UDP探测数据包里带了payload,可能会导致后端认为是业务数据导致收取到无关数据。
前面我们说过,4层负载均衡依赖于网络层衡来确保4层负载均衡器之间的负载是平衡的,那么负载均衡器的故障隔离就是依赖于网络层来做。
具体如何做?
实际上,我们是依赖于路由的方式来做网络层的负载均衡,负载均衡集群中每台服务器都把相同的VIP地址通过路由协议BGP通告给网络设备(交换机或者路由器),网络设备收到相同的一个VIP来自不同的服务器,就会形成一个等价路由(ECMP),言外之意就是形成负载均衡。所以如果我们想要隔离掉某台负载均衡器的话,只要在该服务器上把通过BGP路由协议发布的VIP撤销即可,这样,上游交换机就认为该服务器已经被隔离,从而不再转发数据到对应设备上。
图7、通过撤销VIP路由进行故障隔离
至此,我们已经实现了一个基于FULLNAT,主要依赖三层协议端口做健康检查,通过BGP和网络设备对接实现VIP收发的方式实现负通用载均衡架构模型。
基于上述4层负载的架构,我们建设了VGW(vivo Gateway),主要对内网和外网业务提供4层负载均衡服务。下面我们将从逻辑架构、物理架构、冗余保障、如何提高管理性转发性能等方面进行说明。
VGW核心功能是复杂均衡,同时兼具健康检查,业务引流等功能,所以组成VGW的组件主要就是核心的负载均衡转发模块、健康检查模块、路由控制模块。
负载均衡转发模块:主要负责负载计算和数据转发;
健康检查模块:主要负责检测后端(RealServer)的可用状态,并及时清除不可用后端,或者恢复可用后端;
路由控制模块:主要进行VIP发布引流和隔离异常VGW服务器。
为了方便理解,我们把客户端到VGW环节的部分称之为外部网络(External),VGW到后端(RealServer)之间的环节较内部网络(Internal)。从逻辑架构上将,VGW功能很简单,就是把External的业务请求均匀分发到Internal的RealServer上。
图8、VGW逻辑示意图
物理架构上,对于提供内网的VGW和外网的VGW会有一定差异。
外网VGW集群使用了至少2张网卡,分别接外网侧网络设备和内网侧网络设备。对于VGW服务器来说,两个网口延伸出两条链路,类似人的一双手臂,所以称这种模式为双臂模式,一个数据包只经过VGW服务器的单张网卡一次。
图9、外网VGW物理示意图
而内网VGW和外网不同,内网VGW则只用使用了1张网卡,直接内网侧网络设备。相对应的,该方式称为单臂模式,一个数据包需要需要先从仅有的一张网卡进,然后再从网卡出,最后转发到外部,总计穿过网卡2次。
图10、内网VGW物理示意图
前面我们说过,负载均衡可以为7层负载提供更上一层的负载均衡。
当前VGW最大的业务流量来自于7层接入接出平台(也就是Nginx)的流量,而Nginx基本承载了公司绝大部分核心业务。
当然Nginx并不能支持所有类型的业务,直接构建于TCP、UDP之上的非HTTP类型这一部分流量7层Nginx并不支持,这类业务直接有VGW将数据转发至业务服务器上,中间没有其他环节,比如kafka、Mysql等等。
还有一部分业务是用户自建了各种代理平台,类似于Nginx等等,但也由VGW为其提供4层负载均衡。
图11、VGW业务模型图
为了提高可用性,那么考虑哪些风险?大家自然而然的考虑到服务器故障、进程故障等场景。但VGW场景需要考虑更多,因为VGW整个系统包括了链路、网络设备、服务器、进程等。而且还不能只考虑设备宕机这种简单的场景,实际上,上述任何设备出现不宕机但转发异常的情况才是最麻烦的。
所以监控VGW服务转发是不是正常是第一步,我们通过布放在不同机房和地区的探测节点定期和VIP建立连接,通过建立连接的失败比列来作为衡量VGW是不是正常的标准。实际上,该监控相当于检测了整个涉及VGW的所有环节的链路和转发是不是良好的。
上面的监控覆盖到的是集群级别的探测,当然我们也建立了其他更细粒度的监控来发现具体问题点。
在有监控一手数据之后,我们就能提供服务器级别故障处理和集群级别的故障处理能力。
所有设备级别的直接宕机,VGW做到自动隔离;
所有链路级别的异常,一部分能自动隔离;
所有进程级别的异常,能达到自动隔离;
其他非完全故障的异常,人工介入隔离。
服务器级别故障隔离是将某一些VGW服务器通过路由调整将取消VIP的发布,从而达到隔离目的;
集群级别故障隔离是将整个VGW集群的VIP取消发布,让流量自动被备用集群牵引,由备用集群接管所有业务流量。
图12、服务器、链路级别故障隔离
图13、集群级别的故障隔离
随着业务量级越来越大,VGW单机要接收近百万的QPS请求,同时要达到500W/s以上的包处理能力。显然一般服务器根本无法达到这么大量的请求和包处理速度,主要原因在于Linux的网络处理机制。网络数据包都必须经过Linux内核,网卡收到数据包后都要发送中断给CPU,由CPU在内核处理,然后再拷贝一份副本给应用程序。发送数据也要经过内核进行处理一遍。频繁的中断、用户空间和内核空间之间不断的拷贝数据,导致CPU时长严重被消耗在了网络数据处理上,包速率越大,性能越差。当然还有其他诸如Cache Miss,跨CPU的数据拷贝消耗等等问题。
这里会很容易想到,能不能把上面CPU干的这些脏活累活扔网卡去干了,CPU就纯粹处理业务数据就行。目前很多方案就是根据这个想法来的,硬件方案有智能网卡,纯软件方案当前用的较多的是DPDK(Intel Data Plane Development Kit)。显然智能网卡的成本会偏高,而且还在发展阶段,应用有一定成本。而DPDK纯软件来说在成本和可控方面要好得多,我们选择了DPDK作为底层的包转发组件(实际上是基于爱奇艺开源的DPVS的二次开发)。
DPDK主要是拦截了内核的包处理流程,将用户数据包直接上送至应用程序中,而不是由内核进行处理。同时摈弃了依靠网卡中断的方式处理数据的行为,转而采用轮询的方式从网卡中读取数据,从达到降低CPU中断的目的。当然还利用了CPU亲和性,使用固定的CPU处理网卡数据,减少进程的切换消耗。另外还有很多关于Cache、内存等方面的优化技术。总体上能够将服务器网卡包处理速度达到千万PPS,极大提升网卡的包处理能力,进而能提升服务器的CPS(每秒新建连接数目)。当前我们在100G网卡下,能够达到 100w+的CPS和1200w+PPS的业务处理量(有限条件下的测试结果,非理论值)。
图14、VGW使用的底层工具DPVS(DPDK+LVS)对比几种现有的负载均衡方案性能
通过上述讲解,我们逐步从一个业务可靠性的需求推演出一套可行的负载均衡方案,同时结合vivo的实际需求,落地了我们的VGW负载均衡接入平台。当然,当前的负载均衡方案都是大量取舍后的结果,不可能做到完美。同时我们未来还面临着新的业务协议支持的问题,以及数据中心去中心化的业务模型对负载均衡的集中式控制之间冲突的问题。但技术一直在进步,但总会找到合适的方案的!
在大规模业务场景中,已经不可能通过单机提供业务,这就衍生出了负载均衡的需求。为了满足合适可靠的负载,本文将从简单的基础需求出发,一步步推进并解释如何建立负载均衡平台。
在我们设计软件的很多地方,都看到需要对表格数据进行导入和导出的操作,主要是方便客户进行快速的数据处理和分享的功能,本篇随笔介绍基于WPF实现DataGrid数据的导入和导出操作。