首发于负载均衡

DPVS——爱奇艺的开源四层负载均衡

DPVS的整体思路是在DPDK中抽象一个设备层和一个IPVS层,IPVS层完成LVS中完成的功能,设备层用于抽象各种网卡设备。这个设备层是DPVS相比其他方案会多做的事情。如果是一个简单的负载均衡的实现,是不会做这个设备层的,这个设备层对应于内核中对设备的管理,同时还对应的实现了邻居表(ARP)和路由表。有了这个设备层,ARP和路由的实现才有了可能。因为设备层提供了一个设备的概念和一个IP的概念。

Ip命令是Linux下网络管理的非常强大和最常用的命令,DPVS在DPDK中完整的实现了对ip命令的支持,所以使用ip命令可以管理网络的方方面面,link,ip,arp,route等。这就对应的要求DPVS中有link,ip,arp,route这些概念。而这一切的前提是网卡设备的抽象。所以可以看到netif模块是DPVS中最庞大的模块,而这一整个模块都是为了完备性而产生的,而不是为了负载均衡这个功能本身而产生的(虽然也有点意义)。

DPVS不是为了满足企业单一使用环境下的功能设计,而是为了做一个完备的协议栈式的大而全的四层均衡系统,这个系统的继续发展,将会给行业带来革命性的改善。但是对于爱奇艺本身,领导者想必要顶着巨大的压力才能够推进这些与主线业务不相关的功能设计。DPVS将成为全球范围内四层负载均衡的标杆软体,这个毋庸置疑。

DPVS对网卡,IP,ARP和路由表的实现与内核有一些不同,最大的区别在于DPVS是一个DPDK程序,DPDK程序的特点是所有的实现都要是每个核一份。有一些硬件的相关的高性能需求,DPVS也必须要在架构上向硬件让步。例如在实现IP地址管理的时候,inet_addr是每一个设备可以有多个的,这个模型是符合内核的思想的,但是在选择lip的时候(就是fnat情况下的本地ip的选择),DPVS采用的是sa_pool机制,这个机制依赖网卡的Flow Director功能,整体的思想是根据Flow Director的用法来的。Flow Director的功能是通过设置不同的过滤条件,使得不同的流导入到不同的队列,它的用途在于FNAT的回包上,可以让从哪个核出去的数据包就回到哪个核。

DPVS非常巧妙的从架构的层面依赖并且利用了这个硬件机制。他将DPVS的LIP的不同端口分成多个不同的组,组的个数取决于数据核的个数。每个LIP都会有一个数据核个数的哈希表,这个哈希表是端口的空间划分。例如有40个数据核,那么就会有40个哈希表,每个哈希表代表了端口中的一部分,以最低位作为mask的一个空间划分。例如第一个哈希表中的端口是1,41,81,121。。。,第二个哈希表中的端口是2,42,82,122。。。,以此类推。从端口的组织上,将端口空间划分(DPVS默认从1024端口到65535端口进行划分),然后LIP要选择哪一个端口的时候,是直接在当前核对应的哈希表中直接选择一个没有人用过的端口。

只要这样简单的一个操作,回包就可以保证回到这个发送核,这一步的设计是非常的精巧的。因为Flow Director正好提供的是IP和端口和mask三个维度进行的收包队列的映射。如此一配置,在启动阶段就可以配置好FDIR,而使用的时候仅仅是一个查表,这是DPVS中将硬件机制与软件架构结合设计的一个典范。

DPVS的网卡设备的抽象程度已经比较高,IP地址的管理上由于是一个负载均衡项目,设备的IP和IPVS的IP两个概念已经在努力进行区分,但是仍然没有比较清晰的边界。按照DPVS的设计思路,应该是设备抽象出来后,设备可以自由的拥有IP,有了这个IP之后,这个IP对应的ARP表,路由表等就可以正常的产生和使用。但是实际中,DPVS没有忘记自己是一个IPVS程序,在ipvsadm工具下,是可以直接屏蔽DPVS中的网卡的概念的。Ipvsadm设置一个lip是可以同时设置到IPVS子系统和网卡子系统中。也就是说当设置了一个LIP,网卡同时拥有了这个LIP作为自己的地址,同时IPVS子系统有了这个LIP作为FNAT的源IP选择的范围。LIP的选择和网卡设备的IP之间的桥梁是sa_pool,ip_vs_laddr.c是IPVS子系统对LIP的视角,选择哪一个LIP是由IPVS子系统决定的,但是选择哪一个LPORT(源端口),就是由刚刚提到的sa_pool和inet_addr之间的FDIR机制来决定的。

而VIP在DPVS中也是一个同时存在于netif和IPVS两个部分的概念,VIP是负载均衡的概念,但是在DPVS中,他与LIP一样,也同时对应一个inet_addr(就是netif的IP地址),只是VIP不像LIP是一个ipvsadm命令就同时设置了两部分,而是需要手工的分别设置。这也反映了架构上的权衡。实际上设置了VIP确实是没有必要一定要设置netif的IP的,因为按照DPVS的思想,设置了IP的价值在于可路由和可ARP寻址。如果使用ECMP模型,交换机的数据包VIP并不依赖VIP本身的寻址,而是KNI的IP地址的OSPF的路由宣告,所以VIP本身是不需要ARP的。至于路由,直接强制添加一个路由即可,因为路由并不一定要检查设备的IP地址。

设备的IP地址与路由表的关系也是参考了Linux内核的实现,在给设备添加了一个IP地址之后,同时会添加两个路由条目,一个是这个局域网的链路路由,也就是到同一个网段的其他IP的访问,会自动的走ARP协议去查找MAC地址。这个很容易理解,因为当给一个网口添加了一个IP地址的时候,DPVS可以预测同一个网段的其他IP也位于和DPVS网口的同一个二层,既然是同一个二层,那么使用ARP协议就是直接可达的。所以首先添加的是链路层的路由。另外一个是到内部的路由,多个网卡之间,源IP是自己刚配置的IP的时候,需要路由通过。所以scope是host的,代表着在本机内部的路由条目。也就是说,在本机内部这个源IP是可路由的。

ARP表和路由表都是依托于netif做的网卡抽象的层面实现的。而IPVS是另外一个核心的功能,就是LVS所能实现的功能本身。IPVS的核心是流表,这个流表在DPVS中是一个非常庞大的流表条目,它包含了各种各样的与其他系统之间的关系信息,例如统计,定时器,内存分配器,加速等其他功能。所以这个流表的条目非常的大,为了防止攻击,DPVS实现了Syn Proxy功能。

流表中,DPVS实现了调度层的一个封装,这一点的设计非常好,有效的规避了下层不同的调度算法,在实际的使用中,不同种类的调度算法一般是很少使用的,实际上一般都会使用一致性哈希。所以实现多种调度算法和一个封装,从软件技术上看,是非常的好的,但是从工程上看,很多也是没有太大必要的。当然,DPVS有今天的这个地位,不但是因为他大,还是因为他全。全本身就是一个竞争力,是一个满足各种需求场合的统一解决方案。一招吃遍天下,是很多人很乐意接受的解决方案,也是DPVS能在四层负载均衡市场成功的唯一前提。Gitlab的GLB也是开源的,方案不能说不精巧,但是只提供了一种使用场景,满足了Gitlab企业的需要,但是在DPVS面前,没有人会选择使用GLB。这就是全的力量,Linux内核能够成功也是因为如此。

IPVS子系统在实现的时候同时考虑支持DR,Tunnel,FNAT,DNAT,SNAT,这本身就是一件非常难做的架构设计。好在有LVS的设计可以供参考。IPVS在实现各个子功能的时候,能够有效的暴露dpip命令和ipvsadm命令需要的数据,对于架构设计又是一个比较大的考验,这一切,DPVS虽然不能说已经完备了,但是已经是非常的难得。例如Syn Proxy的逻辑与正常的逻辑不是严格的模块划分,只是功能上做到了可以停掉Syn Proxy而已。还有很多细节的功能点还有很多值得完善的地方,但是IPVS这个子系统,已经基本具备了替代LVS的能力。

值得一提的是DPVS的配置子系统,采用注册的方式来约定配置的层级和架构,这在底层的项目中是很少见的。使用了这个配置引擎,可以做到比较复杂的配置解析,但是引擎的实现比较麻烦。但是一旦有了这套引擎,新配置的定义将会是非常的方便。比起json,在规范性上好很多,比起INI,在功能上强大很多。并不是说这样是最好的,配置没有最好的方案,很多时候使用多个INI配置文件进行配合反而会更好,使用Nginx式的配置也用的很方便。所以DPVS的配置系统可以说有特色,很强大,但是合理性不能做出评价。

DPVS的一个最大的短板恐怕是在统计数据上,程序执行的情况缺少很多数据进行反应,也缺少数据收集的维度进行数据的分析,更缺少现代的抓包框架。例如sFlow和meter这种分析包的架构和分析数据的架构。更不用说流日志之类的超大规模数据的产出。

DPVS设计了一个非常有意思的独立接收核的框架。这个独立接收核的框架同时工作在专用的接收处理核上,鉴于收包是一个开销比较大的事情,所以DPVS设计了收包可以由一个单独的核进行收包,处理由专用的核来处理,当专用的核还有空余的计算能力的时候,也可以用来收包。这个思想是为重逻辑设计的。因为如果是一般的逻辑强度,收包和处理跑满整个10G的网卡,CPU也不会满,即使是25G的网卡,大部分情况下,单核单队列也是能搞得定的。

DPVS从配置到架构实现了收包和Lcore分配上的灵活性,使得DPVS从一开始就是一个通用的方案,是一个可以承载重度逻辑的方案。当我需要让CPU承载更多的逻辑的时候,只需要让多个核同时去负责一个队列,当我需要精简的逻辑,例如DR模式,我可以只配置一个核来做处理,但是同时配置多个核用来接收。这种灵活的调度能力是DPVS的配置架构提供的,架构即技术,在DPVS中有一个比较好的体现。但是Lcore和网卡队列这些关系的映射管理,目前DPVS做的还比较乱,只能说可以用。当然配置多核的时候,需要考虑NUMA,这是配置的时候需要关注的事情。

还有一个比较有意思的子系统是定时器子系统。DPDK原本的定时器子系统就是一个skiplist,skiplist是一个高效的查找链表,所以DPDK本身的定时器的性能就不会太差。那DPVS费了非常大的篇幅来实现一个定时器子系统的原因在哪里?首先,DPVS封装了获得精确时间的函数,像Linux应用层的gettimeofday,在DPVS的定时器子系统中被封装实现了,不是通过直接读取HPET或者TSC,而是直接从DPVS模块中由tick计算而来。我们知道HPET的特点是高并发下必然出现性能问题,TSC的特点是不如HPET稳,精度也不够高,最要命的是,每个核可能还不一样。但是大部分情况下,TSC的缺点并不构成缺点,很少有如此高精度的需求。DPVS的定时器制造了一个调度器的概念,每个核一个。全局的调度器在master核上。每一个调度器只对应一个DPDK的定时器,这样整个DPVS就只使用了几十个DPDK的定时器,这显然是看不起DPDK定时器的超大规模情况下的性能了。事实上,DPVS是对的,skiplist再强大也是一个list,千万级的流表使用的定时器,会很快的把这个list拖垮。DPVS的定时器子系统实现了一个“时分秒”的概念,只是不是通常的时分秒的间隔。同个这个时分秒的概念,将定时器在时间空间进行了划分,每个时间空间的定时器都对应一个哈希表的链表。整个单位时间内的圆盘就是一个哈希表,每个点是一个链表。如此将时间在空间展开。这个方案的设计可谓精巧绝伦。有了这个定时器子系统,DPVS可以处理几乎无限大的定时器规模。所以再也不用担心流表的定时器爆炸的问题了。一般情况下,DPDK的流表都会用专用的老化线程来做,但是由于DPVS的通用设计,使得在数据通道直接进行老化成为可能。

在每个核的任务安排上,DPVS出色的设计了job子系统,每个核要做的周期性的任务都是一个一个的job,job的执行可以设置频率,整个轮询逻辑的核心就是不同job的执行,不同的模块在添加自己的逻辑到主逻辑的时候,都是简单的使用job接口。这些通用的设计都使得DPVS成为一个标杆式的实现,DPVS的野心,充斥在每一个点的设计上。

安全方面除了Syn Proxy,还做了黑名单和限速,限速部分起名叫TC,可见野心不小。Linux下的tc子系统是非常的强大的,DPVS能否复制这一成功,值得继续观察。

另外,KNI,VLAN,跨核的msg消息通信等辅助功能DPVS也有支持。

编辑于 2018-09-04

文章被以下专栏收录