写点什么

全面适配 DPDK 20.11,DPVS 发布 v1.9.0 版本

  • 2021 年 12 月 06 日
  • 本文字数:7765 字

    阅读完需:约 25 分钟

经过 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 功能更新


  • Dpvs: 新增 flow 管理功能,使用通用的 rte_flow API 替换了基于 flow director 的流管理机制。

  • Dpvs: Mbuf 用户自定义数据分类管理,使用 dynfiels 实现了 mbuf 内部用户自定义数据的分类管理。

  • Dpvs: 适配 DPDK 20.11 数据类型,优化 DPVS 协议栈处理。

  • Dpvs: 优化 Makefile,适配 DPDK 20.11 meson/ninja 构建机制。

  • Dpvs: 增加"dedicated_queues" 配置选项,支持 802.3ad 网卡绑定模式下 LACP 包专用队列可配置。

  • Dpdk: 移植多个补丁文件到 DPDK 20.11,并废弃 DPDK 18.11 和 DPDK 17.11 的补丁文件。

  • Dpdk: 优化 DPDK 部署和安装,支持 DPVS 开发环境的快速构建。

  • Keeaplived: 增加 UDP_CHECK 健康检查方法,提高了 UDP 业务健康检查的可靠性和效率。

  • Docs: 更新文档,适配 DPDK 20.11。

  • CI: 更新 GitHubworkflow,支持 DPDK 20.11 (DPVS v1.9) 和 DPDK 18.11(DPVS


2.2 漏洞修复


  • Dpvs: 修复 rr/wrr/wlc 调度算法不同 RS 上负载不均问题。

  • Dpvs: 修复 802.3ad 网卡绑定模式下 Mellanox 25G 网卡不通的问题。

  • Dpdk: 修复 DPDK ixgbe PMD 驱动无法支持 DPVS 的 flow 配置问题。

  • Dpdk: 修复 DPDK mellanox PMD 驱动在调试工作模式下程序崩溃问题。


三、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 -husage: ./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 版本,其主要的工作流程如下:


  • 从 DPDK 官网下载指定版本的 DPDK 压缩包(需要访问公网)到用户指定的工作目录里,如果目录里已存在则跳过下载直接使用;

  • 解压 DPDK 包到工作目录下中;

  • 打上 DPVS 提供的所有补丁文件;

  • 在当前目录的 dpdkbuild 子目录下编译 DPDK,编译完成后安装到 dpdklib 子目录下;

  • 给出 PKG_CONFIG_PATH 环境变量配置方法。


利用这个辅助脚本,编译 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/dpdklibYou 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 20.11,DPVS 发布 v1.9.0 版本 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 的具体配置。


需要说明的是,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 为两种用户数据申请了独立的存储空间,开发者不用再担心数据冲突的问题了。



为了利用 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   8static 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 上。



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



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 上了。



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 $ipif [ $? -ne 0 ]; then  nmap -sU -n $ip -p $port | grep 'udp open' && exit 0 || exit 1else  nmap -6 -sU -n $ip -p $port | grep 'udp open' && exit 0 || exit 1fi
复制代码


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


  • 性能低,每次检查需要启动一个进程,并在新进程里执行一个脚本,CPU 消耗大,一般只能支持数百个 RS 的情况。

  • 检查不准确,一般只能探测到端口是否可用,不能根据实际业务情况配置。

  • 配置复杂,需要在系统里额外安装健康检查脚本。

  • 检查结果依赖外部工具,可靠性、一致性不能保证。


为了支持高性能的 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 服务的可用性,这种方式的优点如下。


  • 性能高,基于 epoll 多路复用模型,可以支持上万个 RS 的健康检查。

  • 不仅支持端口探测,而且支持业务探测。

  • 配置简单,无外部依赖,使用方便。

四、未来版本计划


4.1 DPVS v1.8.12(2021 Q4)


  • 功能开发:ipset 功能模块

  • 功能开发:基于 tc/ipset 的流量控制功能

  • 功能开发:基于 netfilter/ipset 访问控制功能


4.2 DPVS v1.9.2(2022 Q1)


  • 性能优化:基于 rte_flow 实现 KNI 收包隔离,提高控制面的可靠性。

  • 性能优化:协议栈优化,减少数据包的重复解析计算。

  • 功能优化:优化二层组播地址管理问题,解决 KNI 接口组播地址覆盖问题。

  • 功能优化:解决 keepalived 某些情况下无法加载新的配置的问题。

  • 性能测试:测试 v1.9.2 的性能,给出 25G 网卡的多核性能数据


4.3 长远版本


  • 日志优化,兼容 RTE_LOG,解决当前异步日志崩溃问题,并支持分类、去重、限速。

  • FullNAT46 和 XOA 内核模块,支持外部 IPv4 网络访问 IPv6 内网的场景。

  • DPVS 内存池设计,支持高性能、并发安全、动态伸缩、不同长度对象的存取。

  • 优化 DPVS 接口(netif_port)管理,解决多线程动态增删接口的不安全问题。

  • RSS Precalculating,实现一种对硬件要求更低的数据流和 worker 的匹配方案。

  • Portless service,支持 "IP+任意端口" 类型的业务类型。

五、参与社区


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

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


  • DPVS Issues

  • 爱奇艺网络虚拟化团队邮箱:iig_cloud_qlb@qiyi.com

用户头像

科技赋能娱乐,“码”出快乐生活 2020.02.13 加入

爱奇艺技术产品团队秉持高效、开放、创新的理念,分享前沿技术,传达爱奇艺生态理念及技术进展。

评论

发布
暂无评论
全面适配DPDK 20.11,DPVS发布v1.9.0版本