一、网络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密集型,然而当初大数据分析、人工智能等利用都须要在分布式服务器之间传输大量数据实现作业。这点应该是咱们互联网后盾开发最应关注,也最关联的。

dpdk视频学习地址:链接:[https://pan.baidu.com/s/1G8iD...]

提取码:dyej

二、Linux + x86网络IO瓶颈

依据教训,在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。

须要C/C++ Linux高级服务器架构师学习材料加群812855908(包含C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

三、DPDK的基本原理

从后面的剖析能够得悉IO实现的形式、内核的瓶颈,以及数据流过内核存在不可控因素,这些都是在内核中实现,内核是导致瓶颈的起因所在,要解决问题须要绕过内核。所以支流解决方案都是旁路网卡IO,绕过内核间接在用户态收发包来解决内核的瓶颈。

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. 编译执行优化

分支预测

古代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

CPU Cache预取

Cache Miss的代价十分高,回内存读须要65纳秒,能够将行将拜访的数据被动推送的CPU Cache进行优化。比拟典型的场景是链表的遍历,链表的下一节点都是随机内存地址,所以CPU必定是无奈主动预加载的。然而咱们在解决本节点时,能够通过CPU指令将下一个节点推送到Cache里。

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指令优化、最初才用裸代码实现。毕竟都是顶端程序员,对语言、编译器,对实现的谋求不一样,所以造轮子前肯定要先理解好轮子。