经过 DPVS 团队和社区开发者一个多季度的开发迭代,爱奇艺开源项目DpVS已经正式发布了v1.9.0版本。DPVS v1.9.0 正式发布于2021/9/1,它适配了当前DPDK 稳定版本DPDK-20.11(LTS),支持 DPDK API/ABI 以及多种设备驱动的更新和优化。目前 DPVS v1.9.0 已在爱奇艺的多个核心数据中心部署上线,且稳定运行三个月。

一、关于 DPVS

DPVS 是爱奇艺网络虚拟化团队基于DPDK (Data Plane Development Kit)和 LVS (Linux Virtual Server) 开发的高性能四层网络软件负载均衡器,支持 FullNAT /DR /Tunnel /SNAT /NAT64 /NAT 六种负载均衡转发方式和IPv4 /IPv6 /TCP /UDP /ICMP /IMCPv6 等多种网络协议,单核性能达到 2.3M PPS(每秒转发 230 万个包),单机性能可以达到万兆网卡线速(约为 15M PPS)。爱奇艺的四层负载均衡服务、SNAT 代理服务几乎全部都是基于 DPVS 实现的。此外,DPVS 于2017 年 10 月开源后,已吸引了来自包括网易、小米、中国移动、Shopee、字节跳动等在内的国内外众多知名企业的核心贡献者参与社区共建。

项目地址:

https://GitHub.com/iqiyi/dpvs

使用文档:

https://github.com/iqiyi/dpvs/blob/master/doc/tutorial.md

二、DPVS v1.9.0 内容更新列表

发布地址:

https://github.com/iqiyi/dpvs/releases/tag/v1.9.0

DPVS 整个 1.9 大版本都将基于 DPDK 20.11 开发,v1.9.0 版本核心更新就是全面适配了 DPDK-20.11(LTS)。对 DPDK-18.11(LTS) 的支持已经移动到 DPVS-1.8-LTS中,同时终止了对DPDK-17.11(LTS) 的支持。

注:DPVS v1.9.0 使用的 DPDK 具体版本号是DPDK 20.11.1。

DPVS v1.9.0 是在 v1.8.10 基础上开发的,其主要的内容更新分为功能更新和漏洞修复两类,分别列举如下。

2.1 功能更新

2.2 漏洞修复

三、DPVS v1.9.0 重点更新介绍

3.1 更友好的编译使用安装方式

DPDK 20.11 用 meson/ninja彻底取代了之前版本的 Makefile 构建方式,而 DPVSv1.9.0 虽然继续沿用了 Makefile 构建方式,但是适配了 DPDK 20.11 的构建方式,通过 pkg-config工具自动查找依赖 DPDK 的头文件和库文件,解决了 DPVS 安装时复杂的环境依赖问题,使得 DPVS 构建更加智能。

CFLAGS = -DALLOW_EXPERIMENTAL_API $(shell pkg-config --cflags libdpdk) LIBS = $(shell pkg-config --static --libs libdpdk)

完整的文件请参考 dpdk.mk文件。可以看到,DPVS 链接阶段使用了 DPDK 静态库。这虽然增加了 DPVS 可执行程序的大小,但避免了 DPVS 运行时在系统里安装 DPDK 动态链接库的需求;同时,由于 DPVS 对 DPDK 打了一些补丁,用静态链接的方式也避免了 DPDK 动态链接库安装时可能出现的版本冲突的麻烦。

为了简化 DPVS 的编译安装流程,DPVS v1.9.0 提供了一个辅助脚本 dpdk-build.sh,其用法如下。

$ ./scripts/dpdk-build.sh -h usage: ./scripts/dpdk-build.sh [-d] [-w work-directory] [-p patch-directory] OPTIONS: -v specify the dpdk version, default 20.11.1 -d build dpdk libary with DEBUG info -w specify the work directory prefix, default {{ pwd }} -p specify the dpdk patch directory, default {{ pwd }}/patch/dpdk-stable-20.11.1

这个脚本参数支持用户指定编译 DPDK 使用的工作目录前缀、DPDK patch 文件所在的目录、DPDK 版本号(目前仅支持 20.11.1)、是否编译为 DEBUG 版本,其主要的工作流程如下:

利用这个辅助脚本,编译 DPVS 仅需要如下三个简单步骤:

S1. 编译安装 DPDK

$ ./scripts/dpdk-build.sh -d -w /tmp -p ./patch/dpdk-stable-20.11.1/ ... DPDK library installed successfully into directory: //tmp/dpdk/dpdklib You can use this library in dpvs by running the command below: export PKG_CONFIG_PATH=//tmp/dpdk/dpdklib/lib64/pkgconfig

注:为了说明脚本的用法,本例的命令是在 /tmp/dpdk 目录里编译安装有调试信息的 DPDK 版本。通常情况下脚本不用指定参数,使用默认值即可。

S2. 根据脚本输出提示设置环境变量

$ export PKG_CONFIG_PATH=/tmp/dpdk/dpdklib/lib64/pkgconfig

S3. 编译安装 DPVS

$ make && make install

DPVS 默认安装在当前目录的 ./bin 子目录下。

3.2 更通用的流(flow)配置管理

DPVS FullNAT 和 SNAT 的多核转发需要配置网卡的流处理规则。下图是一个典型的 DPVS 双臂模式部署形式,DPVS 服务器有两个网卡接口:网卡-1 负责和用户通信,网卡-2 负责和 RS 通信。一般地,如果服务是 FullNAT,连接由外网用户发起,网卡-1 是外网网卡,网卡-2 是内网网卡;如果服务是 SNAT,连接由用户从内网发起,网卡-1 是内网网卡,网卡-2 是外网网卡。

dpdk操作手册(全面适配DPDK20.11DPVS发布v1.9.0版本)(1)

Inbound(用户到 RS)方向的流量通过 RSS 分发到不同的 worker 线程上,而 Outbound(RS 到用户)的流量通过网卡流处理规则保证同一个会话的流量能匹配到正确的 worker 线程。DPVS v1.8 及其之前的版本使用 DPDK 的 rte_eth_dev_filter_ctrl接口配置 Flow Director 类型(RTE_ETH_FILTER_FDIR)的流规则以实现 Outbound 方向的数据流和 Inbound 方向数据流的会话匹配。但是,DPDK 20.11 彻底废弃了rte_eth_dev_filter_ctrl接口,改用 rte_flow屏蔽了不同网卡、不同类型的流规则实现细节,实现了一种更通用的网卡流规则配置接口。因此,DPVS v1.9.0 适配了rte_flow这种新的流配置接口。

rte_flow 接口需要提供一组 flow item 组成的 pattern 和一组 action。如果数据包和流规则中的 pattern 匹配,则 action 的配置会决定数据包的下一步处理方式,比如送到某个网卡队列、打上标签、或者丢弃。因为 DPVS 不仅支持物理设备接口,而且支持 Bonding、VLAN 等虚接口设备,所以我们增加了 netif_flow 模块来管理 DPVS 不同类型的设备的 rte_flow 流规则。功能上,目前主要提供了 sa_pool 的操作接口,用于实现上面所述的两个方向流的会话匹配。

/* * Add sapool flow rules (for fullnat and snat). * * @param dev [in] * Target device for the flow rules, supporting bonding/physical ports. * @param cid [in] * Lcore id to which to route the target flow. * @param af [in] * IP address family. * @param addr [in] * IP address of the sapool. * @param port_base [in] * TCP/UDP base port of the sapool. * @param port_mask [in] * TCP/UDP mask mask of the sapool. * @param flows [out] * Containing netif flow handlers if success, undefined otherwise. * * @return * DPVS error code. */ int netif_sapool_flow_add(struct netif_port *dev, lcoreid_t cid, int af, const union inet_addr *addr, __be16 port_base, __be16 port_mask, netif_flow_handler_param_t *flows); /* * Delete saflow rules (for fullnat and snat). * @param dev [in] * Target device for the flow rules, supporting bonding/physical ports. * @param cid [in] * Lcore id to which to route the target flow. * @param af [in] * IP address family. * @param addr [in] * IP address of the sapool. * @param port_base [in] * TCP/UDP base port of the sapool. * @param port_mask [in] * TCP/UDP mask mask of the sapool. * @param flows [in] * Containing netif flow handlers to delete. * * @return * DPVS error code. */ int netif_sapool_flow_del(struct netif_port *dev, lcoreid_t cid, int af, const union inet_addr *addr, __be16 port_base, __be16 port_mask, netif_flow_handler_param_t *flows); /* * Flush all flow rules on a port. * * @param dev * Target device, supporting bonding/physical ports. * * @return * DPVS error code. */ int netif_flow_flush(struct netif_port *dev);

说明:Bonding 802.3ad 模式的 dedicatedqueue 也是通过 rte_flow 配置的,如果使用了这个功能,请注意不能随意调用 rte_flow_flush 或 netif_flow_flush。

具体到 rte_flow 的配置上,sa_pool 的 flow pattern 匹配的是目标 IP 地址和目标端口信息。为了减少网卡中流的数量,我们把目标端口地址空间,即 0 ~ 65535,按照 DPVS 配置的 worker 数量,设置了非全地址空间的掩码。基本思路是把端口地址空间等分为worker数量的份数,每个 worker 关联其中一份端口地址子空间。所以,假如有 8 个 worker,我们仅需要配置3-bit 的端口地址掩码,数据包的目标端口地址和 flow item 中指定的端口地址掩码进行 ”与”操作后得到的结果与 flowitem 中的端口基值比较,如果相等,则将数据包送到对应 action 设置的网卡队列中。下面是 DPVS sa_pool 的 flow pattern 和 action 的具体配置。

dpdk操作手册(全面适配DPDK20.11DPVS发布v1.9.0版本)(2)

需要说明的是,rte_flow 仅给我们提供了网卡流规则配置的统一接口,具体的流规则能否支持仍依赖于网卡硬件功能以及网卡的 DPDK PMD 驱动。目前,我们已经验证 Mellanox ConnextX-5(mlx5)可以支持 DPVS 的sa_pool flow 配置。Intel 82599 系列网卡(ixgbe 驱动)的虽然硬件支持 Flow Director,但是其 DPDK PMD 驱动却没有适配好 rte_flow 接口,甚至在 Debug 模式下出现因非法内存访问导致程序崩溃的问题,所以我们给 ixgbe PMD驱动开发了补丁0004-ixgbe_flow-patch-ixgbe-fdir-rte_flow-for-dpvs.patch,使其也成功支持了 DPVS 的流处理规则。其它更多的网卡类型仍有待 DPVS 使用者的验证。

3.3 更合理的 mbuf 自定义数据

为了提高效率,DPVS 使用 DPDK 的 mbuf 用户自定义空间存储与数据包相关的、需要被多个模块使用的关键数据。目前,DPVS 在 mbuf 中存储的数据有两种类型:路由信息和 IP header 指针。DPDK 18.11 中 mbuf 的用户自定义数据空间长度是 8 个字节,在 64 位机器上最多只能存储一个指针数据,DPVS 需要小心区分两种数据的存放和使用时机,保证它们不冲突。DPDK 20.11 的 mbuf 用 dynamic fields取代了 userdata,并将长度增加到 36 个字节,且提供了一组API让开发者动态注册和申请使用。DPVS v1.9.0 为两种用户数据申请了独立的存储空间,开发者不用再担心数据冲突的问题了。

dpdk操作手册(全面适配DPDK20.11DPVS发布v1.9.0版本)(3)

为了利用 mbuf 的dynamic fields机制,DPVS 定义了两个宏。

#define MBUF_USERDATA(m, type, field) \ (*((type *)(mbuf_userdata((m), (field))))) #define MBUF_USERDATA_CONST(m, type, field) \ (*((type *)(mbuf_userdata_const((m), (field)))))

其中,m表示 DPDK 的 mbuf 数据包结构,type是 DPVS 用户数据的类型,field是 DPVS 定义的用户数据类型的枚举值。

typedef enum { MBUF_FIELD_PROTO = 0, MBUF_FIELD_ROUTE, } mbuf_usedata_field_t; mbuf_userdata(_const)

通过 mbuf 用户数据注册时返回的地址偏移量获取存储在 dynamic fields 里的用户数据。

#define MBUF_DYNFIELDS_MAX 8 static int mbuf_dynfields_offset[MBUF_DYNFIELDS_MAX]; void *mbuf_userdata(struct rte_mbuf *mbuf, mbuf_usedata_field_t field) { return (void *)mbuf mbuf_dynfields_offset[field]; } void *mbuf_userdata_const(const struct rte_mbuf *mbuf, mbuf_usedata_field_t field) { return (void *)mbuf mbuf_dynfields_offset[field]; }

最后,我们在 DPVS 初始化时调用 DPDK 接口 rte_mbuf_dynfield_register,初始化 mbuf_dynfields_offset偏移量数组。

int mbuf_init(void) { int i, offset; const struct rte_mbuf_dynfield rte_mbuf_userdata_fields[] = { [ MBUF_FIELD_PROTO ] = { .name = "protocol", .size = sizeof(mbuf_userdata_field_proto_t), .align = 8, }, [ MBUF_FIELD_ROUTE ] = { .name = "route", .size = sizeof(mbuf_userdata_field_route_t), .align = 8, }, }; for (i = 0; i < NELEMS(rte_mbuf_userdata_fields); i ) { if (rte_mbuf_userdata_fields[i].size == 0) continue; offset = rte_mbuf_dynfield_register(&rte_mbuf_userdata_fields[i]); if (offset < 0) { RTE_LOG(ERR, MBUF, "fail to register dynfield[%d] in mbuf!\n", i); return EDPVS_NOROOM; } mbuf_dynfields_offset[i] = offset; } return EDPVS_OK; }

3.4 更完善的调度算法

长连接、低并发、高负载的 gRPC 业务反馈 DPVS 在他们这种应用场景下,连接数量在 RS 上分布不均匀。经排查分析,这个问题是由 rr/wrr/wlc 调度算法的 per-lcore 的实现方式导致的。如下图所示,假设 DPVS 配置了 8 个转发 worker,inbound方向(用户到 RS 方向)的流量是通过网卡 RSS HASH 功能,将流量分发到 w0...w7 不同 worker 上。

dpdk操作手册(全面适配DPDK20.11DPVS发布v1.9.0版本)(4)

因为每个 worker 上的调度算法和数据是相互独立的,而且所有 worker 以相同的方式初始化,所以每个 worker 会以相同的顺序选取 RS。比如,对于轮询(rr)调度,所有 worker 上的第一个连接都会选择 RS 列表中的第一台服务器。下图给出了 8 个 worker, 5 个 RS 的调度情况:假设 RSS HASH 算法是平衡的,则很可能前 8 个用户连接分别哈希到 8 个不同 worker 上,而 8 个 worker 独立调度,将 8 个用户流量全都转发到第一个 RS 上,而其余 4 个 RS 没有用户连接,使得负载在 RS 上分布很不均衡。

dpdk操作手册(全面适配DPDK20.11DPVS发布v1.9.0版本)(5)

DPVS v1.9.0 解决了这个问题,思路很简单,我们让不同 worker 上的调度算法按照如下策略选择不同的 RS 初始值:

InitR(cid) = ⌊N(rs) × cid / N(worker)⌋

其中,N(rs)、N(worker) 分别是 RS 和worker 的数量,cid 是 worker 的编号(从 0 开始编号),InitR(cid)为编号为 cid 的 worker 调度算法的RS 初始值。下图给出了上面的例子使用这种策略调度结果,用户连接可以均衡的分布到所有 RS 上了。

dpdk操作手册(全面适配DPDK20.11DPVS发布v1.9.0版本)(6)

3.5 更高效的 keepalived UDP 健康检查

此前版本的 DPVS keepalived 不支持 UDP_CHECK,UDP 业务的健康检查只能使用 MISC_CHECK 方式,这种方式的配置示例如下:

real_server 192.168.88.115 6000 { MISC_CHECK { misc_path "/usr/bin/lvs_udp_check 192.168.88.115 6000" misc_timeout 3 } }

其中, lvs_udp_check 脚本通过 nmap 工具探测 UDP 端口是否开放。

ipv4_check $ip if [ $? -ne 0 ]; then nmap -sU -n $ip -p $port | grep 'udp open' && exit 0 || exit 1 else nmap -6 -sU -n $ip -p $port | grep 'udp open' && exit 0 || exit 1 fi

基于 MISC_CHECK 的 UDP 健康检查方式有如下缺点:

为了支持高性能的 UDP 健康检查,DPVS 社区开发者 weiyanhua100移植了最新 keepalived 官方版本的 UDP_CHECK 模块到 DPVS 的 keepalived 中。这种方式的配置示例如下:

real_server 192.168.88.115 6000 { UDP_CHECK { retry 3 connect_timeout 5 connect_port 6000 payload hello require_reply hello ok min_reply_length 3 max_reply_length 16 } }

其中, payload指定健康检查程序发送给 RS 的 UDP 请求数据,require_reply是期望收到的 RS 的 UDP 响应数据。这样 UDP 服务器可以自定义健康检查接口,通过这种方式,我们既能探测到 RS 上的 UDP 服务是否真的可用,也能避免健康检查对真实业务的干扰。如果不指定 payload和 require_reply,则只进行 UDP 端口探测,效果和 nmap 端口探测方式类似。

UDP_CHECK 通过 keepalived 和 RS 之间的UDP 数据交互以及 ICMP 错误报文确定后端 UDP 服务的可用性,这种方式的优点如下。

四、未来版本计划

4.1 DPVS v1.8.12(2021 Q4)

4.2 DPVS v1.9.2(2022 Q1)

4.3 长远版本

五、参与社区

目前,DPVS 是一个由数十个公司的开发者、使用者参与的开源社区,我们非常欢迎对 DPVS 感兴趣的同学参与到该项目的使用、开发和社区的建设、维护中来。欢迎大家为 DPVS 提供任何方面的贡献,不论是文档,还是代码;issue 还是 bug fix;以及,也非常欢迎大家把公司添加到 DPVS 社区用户列表中。

如果你对 DPVS 有问题,可以通过如下几种方式联系到我们。

,