乐趣区

关于linux:绝对干货初学者也能看懂的DPDK解析

一、网络 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_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 有些问题须要面对和解决

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

退出移动版