作者:陈俊浩
名词解释
pps:一种单位,表示每秒报文数。
核:本文中说到的核,是指processor。
ring:DPDK实现的核间通讯用的高速环形缓冲区。
RSS特性:根据ip、tcp或者udp元组信息计算hash,将报文分发给hash值对应编号的核的一种网卡特性。
mbuf结构:DPDK用来管理报文的结构体。
sk_buff结构:内核协议栈用来管理报文的结构体。
ospf协议:一种动态路由协议,当前主要用于TGW的容灾功能上。
numa:非统一内存访问的简称,是一种消除CPU访问内存时对前端总线的竞争的架构。
物理核:物理上的 processor。
逻辑核:超线程模拟的 processor。
socket:本文特指CPU socket,而非网络socket。
BPF:柏克莱报文过滤器,一种通过指定的规则快速匹配过滤报文的接口。
perf:linux自带的一种性能分析工具。
背景
TGW是一套实现多网接入的负载均衡系统,为腾讯业务提供着外网接入服务。随着TGW影响力的提升,越来越多的业务接入TGW,对于TGW的整体负载能力要求也越来越高,性能问题也逐渐成为TGW的痛点。
其中,最突出的问题,就是单台机器转发性能只有140万pps,跑不满10Gb流量,造成机器资源浪费。另外,一些pps高、流量大、又无法扩容的集群,要经常在较大压力下运行,也给业务带来不稳定因素。
所以,提升单机的转发性能,充分利用CPU、内存与网卡,成为TGW性能优化的关键。
瓶颈
请输入标题 abcdefg
做性能优化,首先要分析瓶颈:
1.规则表、连接表等都是多核间的共享资源,读写都加锁,容易造成较大cache-misses。
2.页面小,当前只有4KB,而TGW的连接池需要占用30GB左右的内存,就容易造成大量的TLB miss。
解决方案
做完瓶颈分析,就来思考解决方案:
1.要消除共享资源加锁,首先想到的方案是无锁化,每个处理报文的核都能自己维护一份资源,尽量减少cache-misses。
2.要消除TLB-misses,则可以采用hugepage,使用2M甚至1G的页面。
综上两点,我们选择了基于DPDK的开源解决方案来改造TGW,原因如下:
(1)DPDK实现了多线程/多进程报文处理框架,为TGW资源per-cpu化提供便利。
(2)DPDK实现了基于hugepage的内存池管理,为TGW连接池、规则表等访问优化提供了便利。
(3)DPDK实现了高效的ring接口,为报文零拷贝操作提供了便利。
(4)DPDK实现了网卡队列映射到用户态,TGW可以改造成为应用程序,在用户态处理报文,少走了内核网络协议栈的部分逻辑,降低与内核的耦合。
当然,业界也有其他的解决方案,比如netmap,为啥就选择DPDK呢?原因主要有2点:
(1)netmap仍然采用中断,当pps高时,中断容易打断本来正在处理报文的CPU工作,影响吞吐;而DPDK默认采用轮询,CPU自己判断网卡队列是否有报文了,不打断CPU工作。
(2)netmap仍避免不了使用系统调用,而系统调用时需要切换上下文,势必造成CPU cache-misses,无法发挥CPU极致性能。而DPDK都在用户态实现,消除了系统调用的开销。
设计
做设计过程中,我们遇到了各种各样的问题:
1.使用哪种报文处理模型?
答:使用DPDK改造网络转发程序,需要确定每个核负责的工作以及核与核之间的交互,设计好报文处理模型。
DPDK的example程序中,提供了run-to-completion以及pipeline两种模型。
run-to-completion是指从开始处理报文起,到报文发出去,都是由某个核负责。这种模型让编码变得简单,每个核跑同样的逻辑,可以灵活地做平行扩展。
pipeline是指将报文处理逻辑拆分成多个段,每个逻辑段跑在独立的核上,当报文跑完一个逻辑段,就通过核间的ring,将报文丢给另一个核,跑另一个逻辑段。这种模型有利于充分利用CPU cache的局部性原理,避免频繁刷新cache。
对于TGW而言,run-to-completion模型无法满足功能需求,因为TGW采用tunnel模式,需要解析ipip报文,将外层ip头部剥离,取出内层ip地址计算hash并进行分发,以保证出入方向的报文都可以跑到同一个业务逻辑处理核上。而完全的pipeline模型实现起来比较复杂,代码改动量大(利用DPDK改造之前,TGW更接近run-to-completion模型),容易出bug,影响稳定性。
最终,采用了两者结合的一种模型:
(1)报文分发核,从网卡接收队列收取报文,根据源目标ip地址计算hash(若是收到ipip报文,则剥离掉外层ip头部,利用内层ip地址计算hash),然后通过ring,将其分发给对应的业务逻辑处理核。
(2)业务逻辑处理核,对报文进行查找规则、连接、封装解封装ipip报文等处理,然后将报文塞入网卡发送队列,发送出去。
我们做了以下模拟测试:
根据测试结果,得出以下结论:
(1)跨socket的组合性能最低。
(2)纯物理核的组合比物理核跟逻辑核混搭的组合性能高。
(3)封装转发的逻辑比较重,可以通过增加核来提高性能。
所以,尽量使用同个socket的物理核,就可以有更高性能。
但是,理想总是美好的,现实却是如此残忍。
经统计,TGW总共需要使用35GB内存(主要是业务逻辑处理用到)。
TGW主流的机器只有64GB内存,2个socket,假设取其中56GB挂载hugepage(留6GB左右内存给系统使用),如果采用1G大小的hugepage,则每个socket最多可以使用28GB内存(linux做了限制,必须均分),那么业务逻辑处理核需要跨socket。如果采用2M大小的hugepage,可以调整每个socket使用内存的比例,但是需要配置好numa策略,增加了与操作系统的耦合,并且TLB-misses概率会相对大一些。
权衡利弊,最终选择了1G大小的hugepage,用一些跨socket导致的性能消耗,换来与操作系统的解耦以及TLB-misses概率的降低。
1.选择多线程还是多进程?
答:多线程与多进程区别主要是地址空间独立与否。另外,多进程挂了一个进程,还有其他进程可以继续服务;多线程一旦挂了,就全部线程都会退出。
TGW是通过ospf协议来实现集群容灾的,一台机器挂了,上联交换机一旦探测到这台机器没有响应,则会将报文发往集群中的其他机器,不会再发往这台挂掉的机器了。
如果TGW采用多进程,某个进程挂了,其他进程仍然继续工作,此时上联交换机的探测报文很可能依然可以探测成功(活着的进程处理了探测报文),交换机依然会把业务报文发往这台机器。此时,TGW需要将死掉的进程排除在外,不将业务报文给它处理,否则业务报文会丢失。这样,TGW就要再做一层进程间的容灾,增加了系统复杂性,且带来的收益不大。
因此,TGW采用了多线程。
2.DPDK采用是轮询报文的方式,CPU会长期100%,如何确定机器负载以及是否已经到达性能极限了呢?
答:在业务报文处理的路径上,报文分发核跟业务逻辑处理核是主要的参与者。若报文分发核负载高,则网卡接收队列的占用率会随之升高。而业务逻辑处理核负载高,则它与报文分发核之间的ring占用率也会随之升高。所以,对于机器负载的确定,TGW采用监控网卡接收队列以及两种核之间的ring的占用率,替代监控CPU占用率。
3.脱离了内核,需要自己实现arp学习、动态路由、ssh登录等基础功能吗?
答:TGW没有完全脱离内核,仅仅是让业务报文在用户态程序中处理,非业务报文都采用DPDK提供的kni功能,丢给内核处理。所以,arp学习、动态路由、ssh登录的非业务报文都会被扔给内核处理。
4.kni是什么?
答:kni是DPDK实现的与内核协议栈做报文交互的接口,其中包括一个ko模块与相应的通讯接口。
ko模块主要做以下两件事:
(1)启动一个内核线程,内核线程负责接收从用户态发来的报文,并将其从mbuf结构转换成sk_buff结构,再调用netif_rx来让该报文跑内核协议栈。
(2)在内核注册一个虚拟网络接口,若应用程序通过socket发报文,在内核准备通过虚拟网络接口发出去时,会调用kni注册的发送函数,报文将被转换成mbuf结构,并被丢到与用户态程序通讯的ring中。
kni工作原理如下图:
1.kni创建的是虚拟网络接口,那真实的网络接口怎么处理,如eth0、eth1?
答:TGW把eth0、eth1都干掉了,而kni创建的虚拟网络接口名称就改为eth0、eth1。这样可以保持对一些依赖于网络接口名称的脚本或者程序的兼容性。
2.kni会不会影响业务流量统计功能?
答:会!由于业务报文是不走kni接口的,所以ifconfig统计的流量已经不准确了。好在DPDK提供了获取网卡流量的接口,所以TGW依然可以获取到网卡流量。
3.怎么实现类似tcpdump功能?
答:tcpdump是将过滤条件转换成BPF的规则,下发给内核,内核利用这些规则过滤报文,再将匹配条件的报文上传到用户态。
但是,BPF比较复杂,移植到TGW的难度较大,所以TGW采用另一种方案:
(1)实现一个工具,该工具将过滤条件传到TGW报文处理模块。然后,该工具再执行tcpdump,将指定的过滤条件,转换成BPF规则,下发到内核。
(2)在TGW报文处理模块这边,从网卡收取到报文后,以及将报文转发出去之前,利用工具传过来的简单过滤条件(只匹配ip、端口、传输层协议),进行匹配。
(3)对于符合简单过滤条件的报文,则clone一份,将clone结果通过kni接口,发往内核。这里的clone,只是申请一个新的mbuf结构体,引用原始报文,并不会做内容拷贝。而在封装ipip报文的时候,则会做类似于内核copy-on-write策略的操作。
(4)内核协议栈收到报文,根据之前tcpdump下发的BPF规则,过滤报文,将报文送往用户态,最终由tcpdump打印出来。
4.怎么打日志?
答:打日志需要写文件,如果直接在业务逻辑处理核打印日志,那么会影响报文处理。于是,TGW采用了以下方案,解决业务逻辑核打印日志的问题:
(1)维护专用的日志内存池,内存池中每个节点,都是一块日志缓冲区。
(2)调用日志接口时,会从内存池申请一个节点,日志信息直接写到该节点上,并将该节点塞入ring中(这里的ring是专门用于传送日志的,与传输报文用的ring是互相独立的)。
(3)控制面线程从ring中读取日志信息,并写入文件。
调优
完成了基于DPDK的前期改造,经过测试,TGW的极限性能只有320万 pps,仅仅比原来版本提高一倍。于是,我们在当前基础上,对TGW进行了调优。
1.多核扩展
测试发现,当跑到320万 pps时,TGW有大量丢包,丢包原因在于网卡接收队列满了,说明是报文分发核性能不足。
当前,TGW采用的是2个报文分发核与8个业务逻辑处理核的组合,每个网口仅对应着1个报文分发核。
由此看来,1个网口只由1个报文分发核来收取分发报文,显然是不够的。根据之前选核测试得出的结论:增加核数,可以提高业务处理性能,我们尝试调整了报文分发核的核数,并做了以下极限性能测试:
根据测试结果,可以得出,8个报文分发核与8个业务逻辑核的组合是性能最好的,但是,由于机器只有24个核,除去kni、同步、控制面线程独占的核外,只剩17个核。如果采用性能最好的方案,则系统只剩下1个核用了,整个系统会长期处于CPU高负荷状态。所以,经过评估,我们采用了4个报文分发核与8个业务逻辑核的组合。既保留给系统足够的CPU资源,又可以提升TGW性能到600万 pps。
2.新机型
尽管经过多核扩展后,TGW仍然只可以跑到600万 pps。后来,新机型出来了,CPU是intel E5 (48核),128GB内存,40Gb网卡。
于是,又做了以下极限性能测试:
3.单核优化
从之前的测试结果来看,有2个问题:
(1)当业务逻辑核数增加到14个之后,成功收取报文数下降了,说明是报文分发核的性能不足了。
(2)当业务逻辑核数增加到12个之后,成功转发报文数下降了,说明业务逻辑核的性能不足了。
那有没有办法继续提高性能呢?
根据perf结果,分析代码,发现有3个问题:
a.报文分发核会将一些TGW的自定义数据存在mbuf结构的第2条cache line,该条cache line并没有提前预取,在写数据时,就引起了cache-misses。
b.接近极限性能的时候,mbuf占用率很高,怀疑是否mbuf的内存池太小了(当时只有32768)。
c.之前做多核扩展的时候,为了图方便,没有将报文分发核与业务逻辑核之间的ring两两独立开,而是每个网口对应的报文分发核共享与业务逻辑核数相当的ring,这样报文分发核对ring的访问就需要做互斥同步了,也会产生cache-misses。
针对上述的问题,分别做了以下优化:
(1)裁减TGW的自定义数据,把没必要的字段去掉,并将其位置改到第0条cache line中。
(2)将mbuf内存池大小扩大为131072。
(3)每个报文分发核跟每个业务逻辑核都有一一对应的ring,保证对ring的操作只有单写单读。
加上上述优化后,极限性能测试结果如下:
从测试结果来看,8个报文分发核与16个业务逻辑核的组合的性能最高。另外,综合该组合的测试结果看,单核优化前后对比,报文分发核的极限处理性能可以提高700万pps,业务逻辑核的极限处理性能可以提高350万pps。
踩过的坑
开发过程中,我们也遇到一些坑:
1.诡异的丢包
TGW上线后,我们遇到了一个问题,就是网卡的统计计数中,imissed一项会增加,这意味着报文分发核的性能不足。但是,当时TGW负载不高,出入报文量远远没到达性能极限。
刚开始,怀疑是报文分发核之间共享ring,产生竞争导致的。
于是,将每个网口对应的报文分发核数临时改成1个,消除报文分发核之间的资源竞争。测试结果发现,现象有所缓解,丢包率峰值从4.8%降到0.2%。
继续排查,通过pidstat查看TGW各个线程的运行情况,发现报文分发核的任务被动调度次数较多,并且不定时会有突发。然后,观察任务调度次数突发与报文丢弃的关系,发现一旦出现突发,丢弃的报文数就升上去了。所以,可以确定,报文丢弃给任务被动调度有关系,怀疑是任务被调度出去了,然后报文处理不过来,就给丢了。
于是,我们通过尝试设置实时进程的方式来解决这个问题。设置实时进程,提高TGW线程的优先级,避免TGW的线程任务被调度出去。设置实时进程后,报文丢弃的问题确实得到了解决。
但是,跑了一段时间后,却发现了一个新的问题:系统上出现了大量D状态的进程。查看进入D状态的调用栈发现,卡在了flush_work上(如下图所示)。出现D状态进程的原因是TGW被设置为SCHED_FIFO的实时进程,且其线程是不会主动退出的或者产生主动调度的,而实时进程的优先级本来就大于kworker的优先级,导致内核进程kworker一直得不到调度,进而其他进程的I/O相关操作得不到处理,进入了D状态。
由此看来,设置实时进程的方式还是太暴力了,不能采用。
网上搜索资料,发现内核参数isolcpus 中断亲和性设置可以实现CPU独占,任务不会被调度出去。马上测试一下,发现报文丢弃现象有所好转,但未完全根治。在另一个机型的机器上测试,却没有发现报文丢弃现象。
难道报文丢弃跟机器硬件有关系?
查看dmesg,发现有这种日志:
观察发现,打印日志的时候,就会出现报文丢弃现象。
再次网上搜索资料,发现有人遇过类似的问题,并给出了解决方案:
https://jasonlinux.wordpress.com/2013/12/30/performance-regression-and-power-limit-notification-on-dell-poweredge/
这个是linux kernel不能很好地兼容dell服务器电源管理特性(测试用的机器,恰好就是dell R620),可以通过设置内核参数(clearcpuid=229)来解决。采用该方案再次测试,已经没有出现报文丢弃现象了。终于完整地解决这个报文丢弃问题了。
2.DEBUG下的core dump
由于使用了kni接口,若程序直接退出,怕会引用的一些资源没有释放而导致问题。所以在停止TGW之前,加入了rte_eth_dev_stop来停止网卡。
但是,也由此发现了一个DPDK的代码BUG:
若网卡采用向量收报文模式,并且开启了CONFIG_RTE_LIBRTE_MBUF_DEBUG,调用rte_eth_dev_stop,则一定概率上会出现core dump。
分析代码,发现原因如下:
(1)向量收报文模式下,mbuf结构转交给报文分发核处理后,其指针仍然留在网卡接收队列中,并没有清掉。报文转发出去后,mbuf结构会被网卡驱动给释放掉。
(2)调用了rte_eth_dev_stop时,会遍历网卡接收队列,将其中所有mbuf结构给释放掉,结果将之前已经转发出去的报文对应的mbuf结构再次释放一遍,造成二次释放。
(3)开启CONFIG_RTE_LIBRTE_MBUF_DEBUG时,释放mbuf结构的代码中会判断,是否已经释放过了,如果已经释放过,则产生panic,从而产生core dump。
最终,这个问题报给了intel的工程师。而我们采用了去掉TGW停止网卡的代码,并关闭CONFIG_RTE_LIBRTE_MBUF_DEBUG选项的方法来规避解决问题。
落地
优化后的TGW,已经上线了一年多了。从线上机器运行情况来看,优化效果还是相当明显的。以前需要4台机器来抗住压力的集群,现在用2台就可以了,节省了机器资源,也解决了高负载集群的问题。
文章来自:腾讯架构师