DPVS简介与部署

本文最后更新于:November 3, 2020 am

对DPVS进行简单的介绍和在centos7的物理机上部署dpvs负载均衡系统。

0、DPVS简介

DPVS架构

DPVS是一个基于DPDK高性能四层负载均衡器(Layer-4 load balancer),DPVS的名字来源于DPDK+LVS,注意这里的LVS是阿里巴巴改进版的LVS。下图是爱奇艺官方给出的一个DPVS架构以及主要特点:

用户态实现

DPVS主要的任务都是在用户态完成的,可以极大地提高效率。官方声称DPVS的包处理速度,1个工作线程可以达到 2.3Mpps,6个工作线程可以达到万兆网卡小包的转发线速(约 12Mpps)。这主要是因为DPVS绕过了内核复杂的协议栈,并采用轮询的方式收发数据包,避免了锁、内核中断、上下文切换、内核态和用户态数据拷贝产生的性能开销。

实际上四层负载均衡并不需要完整的协议栈,但是需要基本的网络组件,以便完成和周围设备的交互(ARP/NS/NA)、确定分组走向 (Route)、回应 Ping 请求、健全性检查(分组完整性,Checksum校验)、以及 IP 地址管理等基本工作。使用 DPDK 提高了收发包性能,但也绕过了内核协议栈,DPVS 依赖的协议栈需要自己实现。

Master/Worker模型

这一点和nginx一样,使用M/S模型,Master 处理控制平面,比如参数配置、统计获取等;Worker 实现核心负载均衡、调度、数据转发功能。

另外,DPVS 使用多线程模型,每个线程绑定到一个 CPU 物理核心上,并且禁止这些 CPU 被调度。这些 CPU 只运行 DPVS 的 Master 或者某个 Worker,以此避免上下文切换,别的进程不会被调度到这些 CPU,Worker 也不会迁移到其他 CPU 造成缓存失效。

网卡队列/CPU绑定

现在的服务器网卡绝大多数都是多队列网卡,支持多个队列同时收发数据,让不同的 CPU 处理不同的网卡队列的流量,分摊工作量,DPVS将其和CPU进行绑定,利用DPDK 的 API 实现一个网卡的一个收发队列对应一个CPU核心和一个Worker进程,实现一一对应和绑定,从而实现了处理能力随CPU核心、网卡队列数的增加而线性增长,并且很好地实现了并行处理和线性扩展。

关键数据无锁化

内核性能问题的一大原因就是资源共享和锁。所以,被频繁访问的关键数据需要尽可能的实现无锁化,其中一个方法是将数据做到 per-cpu 化,即每个CPU核心只处理自己本地的数据,不需要访问其他CPU的数据,这样就可以避免加锁。对于DPVS而言,连接表邻居表路由表等频繁修改或者频繁查找的数据,都做到了 per-cpu 化。但是在具体 per-cpu 的实现上,连接表和邻居表、路由表两者的实现方式并不相同。

连接表在高并发的情况下会被频繁的CRUD。DPVS中每个CPU核心维护的是不相同的连接表,不同的网络数据流(TCP/UDP/ICMP)按照 N 元组被定向到不同的CPU核心,在此特定的CPU核心上创建、查找、转发、销毁。同一个数据流的包,只会出现在某个CPU核心上,不会落到其他的CPU核心上。这样就可以做到不同的CPU核心只维护自己本地的表,无需加锁。

对于邻居表和路由表这种每个CPU核心都要使用的全局级别的操作系统数据,默认情况下是使用”全局表+锁保护“的方式。DPVS通过让每个CPU核心有同样的视图,也就是每个CPU核心需要维护同样的表,从而做到了per-cpu对于这两个表,虽然在具体实现上有小的差别(路由表是直接传递信息,邻居是克隆数据并传递分组给别的 CPU),但是本质上都是通过跨CPU通信来实现的跨CPU无锁同步,从而将表的变化同步到每个CPU,最后实现了无锁化。

跨CPU无锁通信

上面的关键数据无锁化和这一点实际上是殊途同归的。首先,虽然采用了关键数据 per-cpu等优化,但跨CPU还是需要通信的,比如:

  • Master 获取各个 Worker 的各种统计信息
  • Master 将路由、黑名单等配置同步到各个 Worker
  • Master 将来自DPVS的KNI网卡的数据发送到 Worker(只有 Worker 能操作DPDK网卡接口来发送数据)

既然需要通信,就不能存在互相影响、相互等待的情况,因为那会影响性能。DPVS的无锁通信还是主要依靠DPDK提供的无锁rte_ring库实现的,从底层保证通信是无锁的,并且我们在此之上封装一层消息机制来支持一对一,一对多,同步或异步的消息。

丰富的功能

从转发模式上看:DPVS 支持 DirectRouting(DR)、NAT、Tunnel、Full-NAT、SNAT五种转发模式,可以灵活适配各种网络应用场景

从协议支持上看:DPVS 支持 IPv4和 IPv6 协议、且最新版本增加了 NAT64的转发功能,实现了用户从 IPv6网络访问 IPv4服务

从设备支持上看:DPVS支持主流的硬件网卡设备,同时还支持了Bonding(mode 0 and 4 ), VLAN, kni, ipip/GRE等虚拟设备

从管理工具上看:可以使用包括 ipvsadm、keepalived、dpip等工具对DPVS进行配置和管理,也支持使用进行 quagga 集群化部署

小结

从上面列出的几个DPVS的主要特点我们不难发现,DPVS的主要设计思路就是通过减少各种切换和避免加锁来提高性能,具体的实现上则主要依赖了DPDK的许多功能特性以及使用了常用的几个开源负载均衡软件(ipvsadm、keepalived、dpip等),结合用户态的轻量级网络协议栈(只保留了四层负载均衡所必须的),就实现了超强性能的四层负载均衡系统。

1、机器配置

DPVS由于引入了DPDK套件作为底层的支撑,因此想要最大化发挥它的性能,需要对硬件有一定的要求,dpdk官方给出了一份支持列表,虽然支持性列表上面的平台支持得很广泛,但是实际上兼容性和表现最好的似乎还是要Intel的硬件平台。网卡的兼容性方面,主流的Intel网卡几乎都支持,需要注意的是不同型号的网卡在flow-director功能和网卡的收发数据包支持的队列数可能会有不同。

机器参数

  • CPU:两颗 Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz

  • 内存:16G*8 DDR4-2400 MT/s,每个CPU64G,共计128G

  • 网卡:两张双口的Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)

  • 系统:Red Hat Enterprise Linux Server release 7.6 (Maipo)

  • 内核:3.10.0-1127.19.1.el7.x86_64

关闭超线程和启用NUMA策略

关闭超线程最好的办法是在BIOS中找到相关的超线程设置并且将其禁用,而NUMA策略也是一样,最好在BIOS中直接打开。

打开超线程技术的时候我们可以看到Thread(s) per core是2,也就是每个物理核心对应有2个逻辑核心,而Core(s) per socket表示每个socket有10个物理核心(一般一个CPU对应一个socket),Socket(s)表示当前服务器有两个CPU,也就是常说的双路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 40
On-line CPU(s) list: 0-39
Thread(s) per core: 2
Core(s) per socket: 10
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 79
Model name: Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz
Stepping: 1
CPU MHz: 2669.567
CPU max MHz: 3100.0000
CPU min MHz: 1200.0000
BogoMIPS: 4399.75
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 25600K
NUMA node0 CPU(s): 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38
NUMA node1 CPU(s): 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39
# 当硬件的超线程设置打开的时候,显示为1,表明硬件启用了超线程
$ cat /sys/devices/system/cpu/smt/active
1
# 当然较高版本的内核也可以通过smt.control参数来从操作系统层面控制超线程技术
$ cat /sys/devices/system/cpu/smt/control
on

关闭超线程技术之后的时候我们可以看到Thread(s) per core是1,也就是每个物理核心对应有1个逻辑核心。这时候CPU(s)的数值和CPU的物理核心数值应该相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 20
On-line CPU(s) list: 0-19
Thread(s) per core: 1
Core(s) per socket: 10
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 79
Model name: Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz
Stepping: 1
CPU MHz: 1200.036
CPU max MHz: 3100.0000
CPU min MHz: 1200.0000
BogoMIPS: 4400.07
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 25600K
NUMA node0 CPU(s): 0,2,4,6,8,10,12,14,16,18
NUMA node1 CPU(s): 1,3,5,7,9,11,13,15,17,19
$ cat /sys/devices/system/cpu/smt/active
0

使用numastat命令可以看到有两个node,表明已经打开NUMA策略,同样的还可以使用lscpu命令查看NUMA node(s)的数量。

1
2
3
4
5
6
7
8
$ numastat
node0 node1
numa_hit 654623151 11334196
numa_miss 0 0
numa_foreign 0 0
interleave_hit 34599 34359
local_node 654619810 11281809
other_node 3341 52387

2、安装

开始之前我们需要使用yum安装一些编译安装的时候需要使用的工具和软件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ yum group install "Development Tools"
$ yum install patch libnuma* numactl numactl-devel kernel-devel openssl* popt* -y
# 2021-0722更新,centos7.9系统需要安装
$ yum install libpcap-devel -y
# 2021-0722更新,需要支持ipv6需要安装libnl3-devel
# *** WARNING - this build will not support IPVS with IPv6. Please install libnl/libnl-3 dev libraries to support IPv6 with IPVS.
$ yum install libnl3-devel -y

# 注意kernel以及相应的kernel组件的版本需要和现在使用的kernel版本相对应
$ rpm -qa | grep kernel | grep "3.10.0-1127.19.1" | sort
kernel-3.10.0-1127.19.1.el7.x86_64
kernel-debug-devel-3.10.0-1127.19.1.el7.x86_64
kernel-devel-3.10.0-1127.19.1.el7.x86_64
kernel-headers-3.10.0-1127.19.1.el7.x86_64
kernel-tools-3.10.0-1127.19.1.el7.x86_64
kernel-tools-libs-3.10.0-1127.19.1.el7.x86_64
$ uname -r
3.10.0-1127.19.1.el7.x86_64

这时候需要机器能够连接外网(互联网),直接从github将dpvs项目clone下来,同时还需要下载特定版本的dpdk,无法联网的机器也可以直接下载之后传输到服务器中,后续的安装过程并不需要联网

1
2
3
4
5
6
7
# 首先我们从GitHub上面把dpvs整个项目clone下来
$ cd /home/
$ git clone https://github.com/iqiyi/dpvs.git
$ cd /home/dpvs
# 然后我们下载特定版本的dpdk并解压
$ wget https://fast.dpdk.org/rel/dpdk-17.11.2.tar.xz
$ tar vxf dpdk-17.11.2.tar.xz

接下来需要对dpdk进行打补丁,注意这里的补丁并不是必须的

1
2
3
4
5
6
7
8
9
10
$ cd /home/dpvs
$ cp patch/dpdk-stable-17.11.2/*.patch dpdk-stable-17.11.2/
$ cd dpdk-stable-17.11.2/
# 0001号补丁主要是用于在kni网卡上开启硬件多播功能,比如在kni设备上启动ospfd
$ patch -p 1 < 0001-kni-use-netlink-event-for-multicast-driver-part.patch
# patching file lib/librte_eal/linuxapp/kni/kni_net.c

# 0002号补丁主要是使用dpvs的UOA模块的时候需要用到
$ patch -p 1 < 0002-net-support-variable-IP-header-len-for-checksum-API.patch
# patching file lib/librte_net/rte_ip.h

编译dpdk

1
2
3
4
$ cd /home/dpvs/dpdk-stable-17.11.2
$ make config T=x86_64-native-linuxapp-gcc
# Configuration done using x86_64-native-linuxapp-gcc
$ make

如果出现下面的问题

我们需要手动更改文件,解决方案参考这里

用find / -name netdevice.h 查找内核中的头文件,找到struct net_device_ops 中的 ndo_change_mtu,

会看到ndo_change_mtu被替换成对应版本的ndo_change_mtu_rhXX,比如 ndo_change_mtu_rh75 将 /kni_net.c:704:2 中 ndo_change_mtu 用 ndo_change_mtu_rh75 替换试试?

查看对应内核中的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ find / -name netdevice.h
/usr/include/linux/netdevice.h
/usr/src/kernels/3.10.0-1127.19.1.el7.x86_64.debug/include/linux/netdevice.h
/usr/src/kernels/3.10.0-1127.19.1.el7.x86_64.debug/include/uapi/linux/netdevice.h
/usr/src/kernels/3.10.0-1127.19.1.el7.x86_64/include/linux/netdevice.h
/usr/src/kernels/3.10.0-1127.19.1.el7.x86_64/include/uapi/linux/netdevice.h
$ grep ndo_change_mtu /usr/src/kernels/3.10.0-1127.19.1.el7.x86_64/include/linux/netdevice.h
* int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
int (*ndo_change_mtu)(struct net_device *dev,
* int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
RH_KABI_RENAME(int (*ndo_change_mtu),
int (*ndo_change_mtu_rh74))(struct net_device *dev,
* .ndo_change_mtu_rh74 handler is *not* provided.
* both .extended.ndo_change_mtu() as well as .ndo_change_mtu_rh74() are
$ grep ndo_change_mtu /home/dpvs/dpdk-stable-17.11.2/lib/librte_eal/linuxapp/kni/kni_net.c
.ndo_change_mtu = kni_net_change_mtu,

或者对于CentOS7.5之后的版本可以直接执行

1
sed -i 's/ndo_change_mtu/ndo_change_mtu_rh74/g' /home/dpvs/dpdk-stable-17.11.2/lib/librte_eal/linuxapp/kni/kni_net.c

然后我们重新执行make操作

1
2
3
4
5
6
$ make clean
$ make
# 出现下面字段说明make成功
# Build complete [x86_64-native-linuxapp-gcc]
# 接着设置变量
$ export RTE_SDK=$PWD

配置hugepage

和其他的一般程序不同,dpvs使用的dpdk并不是从操作系统中索要内存,而是直接使用大页内存(hugepage),极大地提高了内存分配的效率。

官方的配置过程中使用的是2MB的大页内存,这里的8192指的是分配了8192个2MB的大页内存,也就是一个node对应16GB的内存,一共分配了32GB的内存,这里的内存可以根据机器的大小来自行调整。但是如果小于1GB可能会导致启动报错。

1
2
3
4
5
6
7
8
9
# for NUMA machine
$ echo 8192 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
$ echo 8192 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

$ mkdir /mnt/huge
$ mount -t hugetlbfs nodev /mnt/huge

# 需要开机自动挂载的话可以在
$ echo "nodev /mnt/huge hugetlbfs defaults 0 0" >> /etc/fstab

挂载驱动模块

1
2
3
4
$ modprobe uio
$ cd /home/dpvs/dpdk-stable-17.11.2
$ insmod /home/dpvs/dpdk-stable-17.11.2/build/kmod/igb_uio.ko
$ insmod /home/dpvs/dpdk-stable-17.11.2/build/kmod/rte_kni.ko

使用脚本查看目前机器和dpvs兼容的设备,这里我们只截取部分重点内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ ./usertools/dpdk-devbind.py --status

$ ./usertools/dpdk-devbind.py --status

Network devices using DPDK-compatible driver
============================================
<none>

Network devices using kernel driver
===================================
0000:01:00.0 '82599ES 10-Gigabit SFI/SFP+ Network Connection 10fb' if=eth0 drv=ixgbe unused=igb_uio
0000:01:00.1 '82599ES 10-Gigabit SFI/SFP+ Network Connection 10fb' if=eth1 drv=ixgbe unused=igb_uio
0000:04:00.0 '82599ES 10-Gigabit SFI/SFP+ Network Connection 10fb' if=eth4 drv=ixgbe unused=igb_uio *Active*
0000:04:00.1 '82599ES 10-Gigabit SFI/SFP+ Network Connection 10fb' if=eth5 drv=ixgbe unused=igb_uio *Active*
0000:08:00.0 'I350 Gigabit Network Connection 1521' if=eth2 drv=igb unused=igb_uio
0000:08:00.1 'I350 Gigabit Network Connection 1521' if=eth3 drv=igb unused=igb_uio

# 对需要使用dpvs的网卡加载特定的驱动
$ ifconfig eth5 down
$ ./usertools/dpdk-devbind.py -b igb_uio 0000:04:00.1

# 再次检查是否加载成功
$ ./usertools/dpdk-devbind.py --status

Network devices using DPDK-compatible driver
============================================
0000:04:00.1 '82599ES 10-Gigabit SFI/SFP+ Network Connection 10fb' drv=igb_uio unused=ixgbe

Network devices using kernel driver
===================================
0000:01:00.0 '82599ES 10-Gigabit SFI/SFP+ Network Connection 10fb' if=eth0 drv=ixgbe unused=igb_uio
0000:01:00.1 '82599ES 10-Gigabit SFI/SFP+ Network Connection 10fb' if=eth1 drv=ixgbe unused=igb_uio
0000:04:00.0 '82599ES 10-Gigabit SFI/SFP+ Network Connection 10fb' if=eth4 drv=ixgbe unused=igb_uio *Active*
0000:08:00.0 'I350 Gigabit Network Connection 1521' if=eth2 drv=igb unused=igb_uio
0000:08:00.1 'I350 Gigabit Network Connection 1521' if=eth3 drv=igb unused=igb_uio

编译安装dpvs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cd /home/dpvs/dpdk-stable-17.11.2/
$ export RTE_SDK=$PWD
$ cd /home/dpvs/
$ make
$ make install

$ cd bin/
$ ls
dpip dpvs ipvsadm keepalived

$ cp conf/dpvs.conf.single-nic.sample /etc/dpvs.conf
$ cd /home/dpvs/bin/
$ ./dpvs &
# 如果安装成功并且成功运行了,执行命令就可以看到
$ ./dpip link show
1: dpdk0: socket 0 mtu 1500 rx-queue 8 tx-queue 8
UP 10000 Mbps full-duplex auto-nego
addr 00:1B:21:BE:EA:C2 OF_RX_IP_CSUM OF_TX_IP_CSUM OF_TX_TCP_CSUM OF_TX_UDP_CSUM

为了方便管理可以将相关的操作命令软链接到/sbin下方便全局执行

1
2
3
4
ln -s /home/dpvs/bin/dpvs /sbin/dpvs
ln -s /home/dpvs/bin/dpip /sbin/dpip
ln -s /home/dpvs/bin/ipvsadm /sbin/ipvsadm
ln -s /home/dpvs/bin/keepalived /sbin/keepalived