前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从 10 Gb 到 40 Gb,从百万级到千万级转发,打造高性能 TGW

从 10 Gb 到 40 Gb,从百万级到千万级转发,打造高性能 TGW

原创
作者头像
腾讯架构师
修改2017-11-14 11:09:40
5.6K1
修改2017-11-14 11:09:40
举报

作者:陈俊浩

名词解释

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。

[1504749437252_9009_1504749437527.jpg]
[1504749437252_9009_1504749437527.jpg]

2.页面小,当前只有4KB,而TGW的连接池需要占用30GB左右的内存,就容易造成大量的TLB miss。

[1504749452882_4885_1504749453145.jpg]
[1504749452882_4885_1504749453145.jpg]

解决方案

做完瓶颈分析,就来思考解决方案:

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,影响稳定性。

最终,采用了两者结合的一种模型:

[1504749487325_772_1504749487614.jpg]
[1504749487325_772_1504749487614.jpg]

(1)报文分发核,从网卡接收队列收取报文,根据源目标ip地址计算hash(若是收到ipip报文,则剥离掉外层ip头部,利用内层ip地址计算hash),然后通过ring,将其分发给对应的业务逻辑处理核。

(2)业务逻辑处理核,对报文进行查找规则、连接、封装解封装ipip报文等处理,然后将报文塞入网卡发送队列,发送出去。

我们做了以下模拟测试:

[1504749506806_8757_1504749507114.jpg]
[1504749506806_8757_1504749507114.jpg]

根据测试结果,得出以下结论:

(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工作原理如下图:

[1504749534519_8894_1504749534820.jpg]
[1504749534519_8894_1504749534820.jpg]

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个报文分发核来收取分发报文,显然是不够的。根据之前选核测试得出的结论:增加核数,可以提高业务处理性能,我们尝试调整了报文分发核的核数,并做了以下极限性能测试:

[1504749635860_8728_1504749636175.jpg]
[1504749635860_8728_1504749636175.jpg]

根据测试结果,可以得出,8个报文分发核与8个业务逻辑核的组合是性能最好的,但是,由于机器只有24个核,除去kni、同步、控制面线程独占的核外,只剩17个核。如果采用性能最好的方案,则系统只剩下1个核用了,整个系统会长期处于CPU高负荷状态。所以,经过评估,我们采用了4个报文分发核与8个业务逻辑核的组合。既保留给系统足够的CPU资源,又可以提升TGW性能到600万 pps。

2.新机型

尽管经过多核扩展后,TGW仍然只可以跑到600万 pps。后来,新机型出来了,CPU是intel E5 (48核),128GB内存,40Gb网卡。

于是,又做了以下极限性能测试:

[1504749662442_7203_1504749662753.jpg]
[1504749662442_7203_1504749662753.jpg]

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中。

[1504749695569_8534_1504749695899.jpg]
[1504749695569_8534_1504749695899.jpg]

(2)将mbuf内存池大小扩大为131072。

(3)每个报文分发核跟每个业务逻辑核都有一一对应的ring,保证对ring的操作只有单写单读。

加上上述优化后,极限性能测试结果如下:

[1504749723773_8040_1504749724123.jpg]
[1504749723773_8040_1504749724123.jpg]

从测试结果来看,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状态。

[1504749772404_2341_1504749772723.jpg]
[1504749772404_2341_1504749772723.jpg]

由此看来,设置实时进程的方式还是太暴力了,不能采用。

网上搜索资料,发现内核参数isolcpus+中断亲和性设置可以实现CPU独占,任务不会被调度出去。马上测试一下,发现报文丢弃现象有所好转,但未完全根治。在另一个机型的机器上测试,却没有发现报文丢弃现象。

难道报文丢弃跟机器硬件有关系?

查看dmesg,发现有这种日志:

[1504749790819_2125_1504749791221.jpg]
[1504749790819_2125_1504749791221.jpg]

观察发现,打印日志的时候,就会出现报文丢弃现象。

再次网上搜索资料,发现有人遇过类似的问题,并给出了解决方案:

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台就可以了,节省了机器资源,也解决了高负载集群的问题。

文章来自:腾讯架构师

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 名词解释
    • 背景
      • 瓶颈
        • 设计
          • 调优
            • 踩过的坑
              • 落地
              相关产品与服务
              负载均衡
              负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档