一、网络io的处境和趋势
从我们用户的使用就可以感受到网速一直在提升,而网络技术的发展也从1ge/10ge/25ge/40ge/100ge的演变,从中可以得出单机的网络io能力必须跟上时代的发展。
1.传统的电信领域
ip层及以下,例如路由器、交换机、防火墙、基站等设备都是采用硬件解决方案。基于专用网络处理器(np),有基于fpga,更有基于asic的。但是基于硬件的劣势非常明显,发生bug不易修复,不易调试维护,并且网络技术一直在发展,例如2g/3g/4g/5g等移动技术的革新,这些属于业务的逻辑基于硬件实现太痛苦,不能快速迭代。传统领域面临的挑战是急需一套软件架构的高性能网络io开发框架。
2.云的发展
私有云的出现通过网络功能虚拟化(nfv)共享硬件成为趋势,nfv的定义是通过标准的服务器、标准交换机实现各种传统的或新的网络功能。急需一套基于常用系统和标准服务器的高性能网络io开发框架。
3.单机性能的飙升
网卡从1g到100g的发展,cpu从单核到多核到多cpu的发展,服务器的单机能力通过横行扩展达到新的高点。但是软件开发却无法跟上节奏,单机处理能力没能和硬件门当户对,如何开发出与时并进高吞吐量的服务,单机百万千万并发能力。即使有业务对qps要求不高,主要是cpu密集型,但是现在大数据分析、人工智能等应用都需要在分布式服务器之间传输大量数据完成作业。这点应该是我们互联网后台开发最应关注,也最关联的。
二、linux + x86网络io瓶颈
在数年前曾经写过《网卡工作原理及高并发下的调优》一文,描述了linux的收发报文流程。根据经验,在c1(8核)上跑应用每1w包处理需要消耗1%软中断cpu,这意味着单机的上限是100万pps(packet per second)。从tgw(netfilter版)的性能100万pps,alilvs优化了也只到150万pps,并且他们使用的服务器的配置还是比较好的。假设,我们要跑满10ge网卡,每个包64字节,这就需要2000万pps(注:以太网万兆网卡速度上限是1488万pps,因为最小帧大小为84b《bandwidth, packets per second, and other network performance metrics》),100g是2亿pps,即每个包的处理耗时不能超过50纳秒。而一次cache miss,不管是tlb、数据cache、指令cache发生miss,回内存读取大约65纳秒,numa体系下跨node通讯大约40纳秒。所以,即使不加上业务逻辑,即使纯收发包都如此艰难。我们要控制cache的命中率,我们要了解计算机体系结构,不能发生跨node通讯。
从这些数据,我希望可以直接感受一下这里的挑战有多大,理想和现实,我们需要从中平衡。问题都有这些
1.传统的收发报文方式都必须采用硬中断来做通讯,每次硬中断大约消耗100微秒,这还不算因为终止上下文所带来的cache miss。
2.数据必须从内核态用户态之间切换拷贝带来大量cpu消耗,全局锁竞争。
3.收发包都有系统调用的开销。
4.内核工作在多核上,为可全局一致,即使采用lock free,也避免不了锁总线、内存屏障带来的性能损耗。
5.从网卡到业务进程,经过的路径太长,有些其实未必要的,例如netfilter框架,这些都带来一定的消耗,而且容易cache miss。
三、dpdk的基本原理
从前面的分析可以得知io实现的方式、内核的瓶颈,以及数据流过内核存在不可控因素,这些都是在内核中实现,内核是导致瓶颈的原因所在,要解决问题需要绕过内核。所以主流解决方案都是旁路网卡io,绕过内核直接在用户态收发包来解决内核的瓶颈。
linux社区也提供了旁路机制netmap,官方数据10g网卡1400万pps,但是netmap没广泛使用。其原因有几个:
1.netmap需要驱动的支持,即需要网卡厂商认可这个方案。
2.netmap仍然依赖中断通知机制,没完全解决瓶颈。
3.netmap更像是几个系统调用,实现用户态直接收发包,功能太过原始,没形成依赖的网络开发框架,社区不完善。
那么,我们来看看发展了十几年的dpdk,从intel主导开发,到华为、思科、aws等大厂商的加入,核心玩家都在该圈子里,拥有完善的社区,生态形成闭环。早期,主要是传统电信领域3层以下的应用,如华为、中国电信、中国移动都是其早期使用者,交换机、路由器、网关是主要应用场景。但是,随着上层业务的需求以及dpdk的完善,在更高的应用也在逐步出现。
dpdk旁路原理:
左边是原来的方式数据从 网卡 -> 驱动 -> 协议栈 -> socket接口 -> 业务
右边是dpdk的方式,基于uio(userspace i/o)旁路数据。数据从 网卡 -> dpdk轮询模式-> dpdk基础库 -> 业务
用户态的好处是易用开发和维护,灵活性好。并且crash也不影响内核运行,鲁棒性强。
dpdk支持的cpu体系架构:x86、arm、powerpc(ppc)
dpdk支持的网卡列表:https://core.dpdk.org/supported/,我们主流使用intel 82599(光口)、intel x540(电口)
四、dpdk的基石uio
为了让驱动运行在用户态,linux提供uio机制。使用uio可以通过read感知中断,通过mmap实现和网卡的通讯。
uio原理:
要开发用户态驱动有几个步骤:
1.开发运行在内核的uio模块,因为硬中断只能在内核处理
2.通过/dev/uiox读取中断
3.通过mmap和外设共享内存
五、dpdk核心优化:pmd
dpdk的uio驱动屏蔽了硬件发出中断,然后在用户态采用主动轮询的方式,这种模式被称为pmd(poll mode driver)。
uio旁路了内核,主动轮询去掉硬中断,dpdk从而可以在用户态做收发包处理。带来zero copy、无系统调用的好处,同步处理减少上下文切换带来的cache miss。
运行在pmd的core会处于用户态cpu100%的状态
网络空闲时cpu长期空转,会带来能耗问题。所以,dpdk推出interrupt dpdk模式。
interrupt dpdk:
它的原理和napi很像,就是没包可处理时进入睡眠,改为中断通知。并且可以和其他进程共享同个cpu core,但是dpdk进程会有更高调度优先级。
六、dpdk的高性能代码实现
1.采用hugepage减少tlb miss
默认下linux采用4kb为一页,页越小内存越大,页表的开销越大,页表的内存占用也越大。cpu有tlb(translation lookaside buffer)成本高所以一般就只能存放几百到上千个页表项。如果进程要使用64g内存,则64g/4kb=16000000(一千六百万)页,每页在页表项中占用16000000 * 4b=62mb。如果用hugepage采用2mb作为一页,只需64g/2mb=2000,数量不在同个级别。
而dpdk采用hugepage,在x86-64下支持2mb、1gb的页大小,几何级的降低了页表项的大小,从而减少tlb-miss。并提供了内存池(mempool)、mbuf、无锁环(ring)、bitmap等基础库。根据我们的实践,在数据平面(data plane)频繁的内存分配释放,必须使用内存池,不能直接使用rte_malloc,dpdk的内存分配实现非常简陋,不如ptmalloc。
2.sna(shared-nothing architecture)
软件架构去中心化,尽量避免全局共享,带来全局竞争,失去横向扩展的能力。numa体系下不跨node远程使用内存。
3.simd(single instruction multiple data)
从最早的mmx/sse到最新的avx2,simd的能力一直在增强。dpdk采用批量同时处理多个包,再用向量编程,一个周期内对所有包进行处理。比如,memcpy就使用simd来提高速度。
simd在游戏后台比较常见,但是其他业务如果有类似批量处理的场景,要提高性能,也可看看能否满足。
4.不使用慢速api
这里需要重新定义一下慢速api,比如说gettimeofday,虽然在64位下通过vdso已经不需要陷入内核态,只是一个纯内存访问,每秒也能达到几千万的级别。但是,不要忘记了我们在10ge下,每秒的处理能力就要达到几千万。所以即使是gettimeofday也属于慢速api。dpdk提供cycles接口,例如rte_get_tsc_cycles接口,基于hpet或tsc实现。
在x86-64下使用rdtsc指令,直接从寄存器读取,需要输入2个参数,比较常见的实现:
static inline uint64_trte_rdtsc(void){ uint32_t lo, hi; __asm__ __volatile__ ( rdtsc : =a(lo), =d(hi) ); return ((unsigned long long)lo) | (((unsigned long long)hi) << 32);}
这么写逻辑没错,但是还不够极致,还涉及到2次位运算才能得到结果,我们看看dpdk是怎么实现:
static inline uint64_trte_rdtsc(void){ union { uint64_t tsc_64; struct { uint32_t lo_32; uint32_t hi_32; }; } tsc; asm volatile(rdtsc : =a (tsc.lo_32), =d (tsc.hi_32)); return tsc.tsc_64;}
巧妙的利用c的union共享内存,直接赋值,减少了不必要的运算。但是使用tsc有些问题需要面对和解决
1) cpu亲和性,解决多核跳动不精确的问题
2) 内存屏障,解决乱序执行不精确的问题
3) 禁止降频和禁止intel turbo boost,固定cpu频率,解决频率变化带来的失准问题
5.编译执行优化
1) 分支预测
现代cpu通过pipeline、superscalar提高并行处理能力,为了进一步发挥并行能力会做分支预测,提升cpu的并行能力。遇到分支时判断可能进入哪个分支,提前处理该分支的代码,预先做指令读取编码读取寄存器等,预测失败则预处理全部丢弃。我们开发业务有时候会非常清楚这个分支是true还是false,那就可以通过人工干预生成更紧凑的代码提示cpu分支预测成功率。
#pragma once#if !__glibc_prereq(2, 3)# if !define __builtin_expect# define __builtin_expect(x, expected_value) (x)# endif#endif#if !defined(likely)#define likely(x) (__builtin_expect(!!(x), 1))#endif#if !defined(unlikely)#define unlikely(x) (__builtin_expect(!!(x), 0))#endif
2) cpu cache预取
cache miss的代价非常高,回内存读需要65纳秒,可以将即将访问的数据主动推送的cpu cache进行优化。比较典型的场景是链表的遍历,链表的下一节点都是随机内存地址,所以cpu肯定是无法自动预加载的。但是我们在处理本节点时,可以通过cpu指令将下一个节点推送到cache里。
api文档:https://doc.dpdk.org/api/rte__prefetch_8h.html
static inline void rte_prefetch0(const volatile void *p){ asm volatile (prefetcht0 %[p] : : [p] m (*(const volatile char *)p));}
#if !defined(prefetch)#define prefetch(x) __builtin_prefetch(x)#endif
…等等
3) 内存对齐
内存对齐有2个好处:
l 避免结构体成员跨cache line,需2次读取才能合并到寄存器中,降低性能。结构体成员需从大到小排序和以及强制对齐。参考《data alignment: straighten up and fly right》
#define __rte_packed __attribute__((__packed__))
l 多线程场景下写产生false sharing,造成cache miss,结构体按cache line对齐
#ifndef cache_line_size#define cache_line_size 64#endif#ifndef aligined#define aligined(a) __attribute__((__aligned__(a)))#endif
4) 常量优化
常量相关的运算的编译阶段完成。比如c++11引入了constexp,比如可以使用gcc的__builtin_constant_p来判断值是否常量,然后对常量进行编译时得出结果。举例网络序主机序转换
#define rte_bswap32(x) ((uint32_t)(__builtin_constant_p(x) ? \ rte_constant_bswap32(x) : \ rte_arch_bswap32(x)))
其中rte_constant_bswap32的实现
#define rte_static_bswap32(v) \ ((((uint32_t)(v) & uint32_c(0x000000ff)) << 24) | \ (((uint32_t)(v) & uint32_c(0x0000ff00)) 8) | \ (((uint32_t)(v) & uint32_c(0xff000000)) >> 24))
5)使用cpu指令
现代cpu提供很多指令可直接完成常见功能,比如大小端转换,x86有bswap指令直接支持了。
static inline uint64_t rte_arch_bswap64(uint64_t _x){ register uint64_t x = _x; asm volatile (bswap %[x] : [x] +r (x) ); return x;}
这个实现,也是glibc的实现,先常量优化、cpu指令优化、最后才用裸代码实现。毕竟都是顶端程序员,对语言、编译器,对实现的追求不一样,所以造轮子前一定要先了解好轮子。
google开源的cpu_features可以获取当前cpu支持什么特性,从而对特定cpu进行执行优化。高性能编程永无止境,对硬件、内核、编译器、开发语言的理解要深入且与时俱进。
七、dpdk生态
对我们互联网后台开发来说dpdk框架本身提供的能力还是比较裸的,比如要使用dpdk就必须实现arp、ip层这些基础功能,有一定上手难度。如果要更高层的业务使用,还需要用户态的传输协议支持。不建议直接使用dpdk。
目前生态完善,社区强大(一线大厂支持)的应用层开发项目是fd.io(the fast data project),有思科开源支持的vpp,比较完善的协议支持,arp、vlan、multipath、ipv4/v6、mpls等。用户态传输协议udp/tcp有tldk。从项目定位到社区支持力度算比较靠谱的框架。
腾讯云开源的f-stack也值得关注一下,开发更简单,直接提供了posix接口。
seastar也很强大和灵活,内核态和dpdk都随意切换,也有自己的传输协议seastar native tcp/ip stack支持,但是目前还未看到有大型项目在使用seastar,可能需要填的坑比较多。
我们gbn gateway项目需要支持l3/ip层接入做wan网关,单机20ge,基于dpdk开发。
韩国将在未来10年内为人工智能半导体技术研发投资1万亿韩元
如何寻找Python特定领域的库
信息保留的二值神经网络IR-Net,落地性能和实用性俱佳
排除电力系统故障的具体方法
基于51单片机设计的电动车控制器
初学者也能看懂的DPDK解析
工业互联网+5G的运营商端边云协同方案及网络架构
全志D1/D1s平台支持即将进入Linux主线
金鸽R40工业4G路由网关成功通过华为云、亚马逊云双认证
FPGA进行静态时序分析
爱立信推出全新AI解决方案,传输网络性能再升级
配电箱的接线方法和接线规范要求
瑞萨电子MCU/MPU助力中国新能源创新发展
30W*2高保真功率放大器
苹果第三季度无线耳机市场份额下滑35%
东芝V8CANVIO移动硬盘评测 日常必备TYPE-C接口是趋势
电子连接器老化测试作用
CR-AG24/1505蓄电池综合测试仪
浪涌抑制器的应用及注意事项?
图示均衡器的使用技巧