欢迎你们前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~html
1、网络IO的处境和趋势git
从咱们用户的使用就能够感觉到网速一直在提高,而网络技术的发展也从1GE/10GE/25GE/40GE/100GE的演变,从中能够得出单机的网络IO能力必须跟上时代的发展。程序员
1. 传统的电信领域github
IP层及如下,例如路由器、交换机、防火墙、基站等设备都是采用硬件解决方案。基于专用网络处理器(NP),有基于FPGA,更有基于ASIC的。可是基于硬件的劣势很是明显,发生Bug不易修复,不易调试维护,而且网络技术一直在发展,例如2G/3G/4G/5G等移动技术的革新,这些属于业务的逻辑基于硬件实现太痛苦,不能快速迭代。传统领域面临的挑战是急需一套软件架构的高性能网络IO开发框架。编程
2. 云的发展api
私有云的出现经过网络功能虚拟化(NFV)共享硬件成为趋势,NFV的定义是经过标准的服务器、标准交换机实现各类传统的或新的网络功能。急需一套基于经常使用系统和标准服务器的高性能网络IO开发框架。bash
3. 单机性能的飙升服务器
网卡从1G到100G的发展,CPU从单核到多核到多CPU的发展,服务器的单机能力经过横行扩展达到新的高点。可是软件开发却没法跟上节奏,单机处理能力没能和硬件门当户对,如何开发出与时并进高吞吐量的服务,单机百万千万并发能力。即便有业务对QPS要求不高,主要是CPU密集型,可是如今大数据分析、人工智能等应用都须要在分布式服务器之间传输大量数据完成做业。这点应该是咱们互联网后台开发最应关注,也最关联的。网络
2、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。
3、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体系架构:x8六、ARM、PowerPC(PPC)
DPDK支持的网卡列表:core.dpdk.org/supported/,咱们主流使用Intel 82599(光口)、Intel x540(电口)
4、DPDK的基石UIO
为了让驱动运行在用户态,Linux提供UIO机制。使用UIO能够经过read感知中断,经过mmap实现和网卡的通信。
UIO原理:
要开发用户态驱动有几个步骤:
1.开发运行在内核的UIO模块,由于硬中断只能在内核处理
2.经过/dev/uioX读取中断
3.经过mmap和外设共享内存
5、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进程会有更高调度优先级。
6、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_t
rte_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_t
rte_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有些问题须要面对和解决
CPU亲和性,解决多核跳动不精确的问题
内存屏障,解决乱序执行不精确的问题
禁止降频和禁止Intel Turbo Boost,固定CPU频率,解决频率变化带来的失准问题
5. 编译执行优化
现代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
复制代码
Cache Miss的代价很是高,回内存读须要65纳秒,能够将即将访问的数据主动推送的CPU Cache进行优化。比较典型的场景是链表的遍历,链表的下一节点都是随机内存地址,因此CPU确定是没法自动预加载的。可是咱们在处理本节点时,能够经过CPU指令将下一个节点推送到Cache里。
API文档:doc.dpdk.org/api/rte__pr…
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
复制代码
…等等
内存对齐有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
复制代码
常量相关的运算的编译阶段完成。好比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(0x00ff0000)) >> 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进行执行优化。高性能编程永无止境,对硬件、内核、编译器、开发语言的理解要深刻且与时俱进。
7、DPDK生态
对咱们互联网后台开发来讲DPDK框架自己提供的能力仍是比较裸的,好比要使用DPDK就必须实现ARP、IP层这些基础功能,有必定上手难度。若是要更高层的业务使用,还须要用户态的传输协议支持。不建议直接使用DPDK。
目前生态完善,社区强大(一线大厂支持)的应用层开发项目是FD.io(The Fast Data Project),有思科开源支持的VPP,比较完善的协议支持,ARP、VLAN、Multipath、IPv4/v六、MPLS等。用户态传输协议UDP/TCP有TLDK。从项目定位到社区支持力度算比较靠谱的框架。
腾讯云开源的F-Stack也值得关注一下,开发更简单,直接提供了POSIX接口。
Seastar也很强大和灵活,内核态和DPDK都随意切换,也有本身的传输协议Seastar Native TCP/IP Stack支持,可是目前还未看到有大型项目在使用Seastar,可能须要填的坑比较多。
咱们GBN Gateway项目须要支持L3/IP层接入作Wan网关,单机20GE,基于DPDK开发。
问答
相关阅读
此文已由做者受权腾讯云+社区发布,更多原文请点击
搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!
海量技术实践经验,尽在云加社区!