同程旅游四层负载实战
0x01 背景
随着公司的业务高速发展,急需一款高性能、高可靠的四层负载均衡设备。面对昂贵的商业设备及解决方案,我们决定着手做一款自己的四层负载设备(TVS)。在选型过程中,调研了开源的LVS项目,其新建性能及可用性并未达到预期的效果。针对LVS的短板并结合公司的需求,TVS进行大量优化,极大提升了新建性能、平台的稳定性、安全性及易用性。
TVS支持FullNat及Tunnel两种转发模型,以应对不同的业务场景。本文针对FullNat场景进行介绍。
0x02 负载均衡介绍
在互联网初期,企业业务流量不大,而且可靠性要求并不高,因此只需一台服务器对外服务即可。
后续随着负载和可靠性的要求,通过DNS给不同的客户解析不同的服务器IP。但是当服务器出现故障时,由于DNS各级都会有缓存,即使权威DNS域名解析改变时并不会在客户端及时生效,会影响部分客户的访问。而且DNS负载算法比较简单,不能满足业务需求。
那么在不使用DNS解析轮询算法的情况下,如何实现后端多台服务器的负载呢?此时就需要负载均衡设备,负载均衡设备后面挂载多台服务器,通过加权轮询算法将流量分发到不同的服务器处理,同时配合健康检查,当后端服务器出现故障时,负载均衡设备会将其自动剔除,保证业务高可用。
此处的Load Balance是四层负载均衡设备,当此设备收到流量后,通过轮询算法和会话表,将流量转发到不同的服务器进行处理。业内负载均衡有多种转发模型,以下是各种模型的优缺点比较:
因为TVS主要部署模式为FullNAT,在此详细介绍下FullNat流程,图例如下。
1. Client发送请求到Vip
2. 负载通过轮询算法选取Real Server和LocalIP
3. 新建会话,记录ClientIP,VIP,RSIP,LocalIP
4. 将报文的ClientIP改成LocalIP,VIP改成RSIP
5. Real Server收到报文后处理并回包
6. 负载收到回包后,查找会话
7. 将报文RSIP改成VIP,LocalIP改成ClientIP
8. 发送回包报文
负载均衡设备在整个业务流程中扮演者非常重要的角色。为此我们设计了一套高可靠、高性能的TVS转发平台。
0x03 总体架构
TVS使用同机房双集群互为主备的方案。TVS分为集群A及集群B,两个集群分别工作。集群内4台机器负载且互备,同时也实现集群间互备。拓扑如下:
TVS主要特性:
1. 支持每核会话表
2. 支持establish及长连接会话同步
3. 支持集群内TVS服务器动态上下线
4. 支持上层业务TVS集群间热切换
0x04 集群间高可用
TVS通过BGP与上层交换机互联。两个集群同时宣告同一个VIP1,集群B宣告时,使用route-policy调低优先级,集群A保持默认优先级。此时流量全部在集群A处理,当集群A整个故障时,流量会自动切换到B集群进行处理。TVS在集群间不进行会话同步。
0x05 集群内高可用
在TVS集群内,通过路由ECMP、长短掩码路由宣告和会话同步,实现集群内高可用。任意一台或多台服务器异常,也能保证业务不受影响。
1. ECMP负载高可用
TVS集群内4台机器,以相同优先级同时宣告VIP,在交换机侧形成ECMP等价路由,交换机根据SIP+DIP进行分流。当一台服务器故障时,ECMP等价路由会减少,但是所有流量还是会全部送到集群内剩余机器处理,保证流量不丢失。
当TVS1故障时,ECMP路由条目发生变化,ECMP重新Hash。即使TVS2没有故障,原先在TVS2上的会话流量也会重新调度到另外的机器上处理,这种是不必要的抖动。
最理想的是故障TVS的流量被均分到剩余三台TVS上,正常TVS上原先的流量保持不变。因此需要在TVS上联交换机配置一致性Hash。
2. 长短掩码路由负载高可用
Real Server收到TVS1转换后的报文,处理并回应相应的报文。在正常情况下,回应报文必须回到TVS1,保证来回路径一致。为此TVS做了如下处理:
Ø 每台TVS各分配一段LocalIP
Ø 通过BGP将各自的LocalIP宣告
当TVS1故障时,TVS1宣告的LocalIP路由会消失,Real Server的回应报文将无法转发,即使做了会话同步也无济于事。
根据路由表最长掩码匹配原则。为此TVS在宣告自己长掩码的LocalIP时,还需要宣告短掩码的LocalIP。当TVS1正常时,从RS到TVS的流量就会匹配长掩码的路由回到TVS1,当TVS1故障时,Real Server的回应报文会匹配到短掩码的路由,到达其他TVS上。此时集群又做了会话同步,因此原来在TVS1上的流量也能正常转发。
3. 会话同步模型
集群内会话同步,在Core1上新建的会话同步到其他TVS的Core1上,以此类推。
集群内TVS上的RSS Hash Key必须保持一致。如果不一致,当TVS1故障时,假如流量进入TVS3,RSS分流没将报文送入指定的CPU,则找不到会话,从而转发失败。因此我们修改了Ixgbe驱动,将驱动中RSS Hask Key随机生成函数进行替换。
0x06 高性能实现
1. 减少因调度引起的上下文切换
在正常情况下,Linux会接管所有CPU资源。当某个任务比较繁忙时,会寻找其他的Core进行分担任务。这样会导致Linux的任务直接影响数据包转发及会话新建性能,而且调度器调度时的上下文切换会消耗大量的资源。
因此将TVS相关的Core进行隔离,不受Linux调度器控制,这样会最大限度的减少Linux任务对TVS的影响。同时TVS的处理线程通过线程亲和度绑定到指定的核上,不会产生相互干扰。
2. 加快内存访问速度
内存访问的速度直接决定了程序的性能。TVS通过CPU就近节点访问,充分利用预取功能,降低Cache miss,提高访存效率。
数据结构Numa化:TVS使用的是两路服务器架构,有两颗CPU,每颗CPU就近访问内存是最快的,如果跨QPI访问对端内存,效率会明显降低。在网络收发包场景,频繁跨QPI访问数据,将有15%-20%的损耗。
使用Hugepage:从Hugepage上分配的数据结构,物理内存保持连续。这样在数据遍历及查找时,可充分使用CPU预取功能,同时可以减少MMU的查询次数,降低访存损耗,降低Cache miss。
数据结构Cache Line对齐:PerCore的数据结构必须进行Cache Line对齐,CPU在读写内存是会通过MESI协议保证Cache一致性,当Core1和Core2共用一个Cache Line大小的内存,那么Core1和Core2是存在竞争关系的,从而降低预取效率,增加访存时间。
3. 绕过内核
通过Linux内核进行收发包数据,存在比较严重的性能瓶颈。通过中断收包、CPU拷贝数据包、Linux Netfilter框架及协议栈冗长等直接影响转发性能,而且在内核态调试比较麻烦。
因此TVS使用了Linux的UIO机制,直接在用户态和内核态共享内存空间。通过DMA将数据包直接拷贝到共享内存空间中。用户态转发程序直接可读/写共享内存中的数据,减少了收发包的步骤,提高了效率。同时通过轮询方式收包及DMA进行拷贝数据包,减少了中断及CPU拷贝的耗时。
根据以上需求,TVS首选Intel DPDK作为高效稳定的网络收发包平台。
4. 解决锁竞争
TVS采用的是PerCore Session的设计架构,每个核有各自的会话表,相互不竞争,充分利用所有核的性能。全局会话则有局限性,核越多竞争越激烈,性能越差。
一个会话只存储在一张会话表上,在FullNat模式下, 如果只使用RSS进行分流的话,就会导致ClientIP->VIP的流量和RSIP->LocalIP的流量分到不同的Core上,从而导致会话找不到而转发异常。
为了保证同一个会话下,请求和回应的流量落在同一个Core上,TVS使用了Intel 82599网卡的FDIR特性。给每个核分配各自的LocalIP,创建会话时,只从本核的LocalIP池中选取LocalIP。同时设置网卡FDIR配置,指定LocalIP送往之前分配的核上。从而来实现请求的流量和回应的流量落在同一个Core上的需求。
FDIR的优先级高于RSS,如果匹配FDIR规则,则直接送往对应的收包队列,不过RSS模块,如果不匹配FDIR,则直接进入RSS模块进行分流。这样既可以保证请求时流量均匀分布到所有Core上,同时也能保证回应流量回到指定的Core,一举两得。
0x07 总结
TVS现有架构充分结合了网络架构及业务要求,完全满足同程现有需求。但是TVS还有一些可以提升的地方。后续还可以改进,比如:ALG模块支持更多的协议,更优的调度算法,更低的转发延迟(硬件转发方向),恶意流量初步清洗等等。
参考资料:
1. http://dpdk.org/
2. https://github.com/alibaba/LVS/
3. https://www.intel.com/content/www/us/en/embedded/products/networking/82599-10-gbe-controller-datasheet.html
往期文章
Eat.Hack.Sleep.Repeat - YSRC 第二期夏日安全之旅
Django的两个url跳转漏洞分析:CVE-2017-7233&7234
A BLACK PATH TOWARD THE SUN - HTTP Tunnel 工具简介