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

16,440 阅读14分钟

经过 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、字节跳动等在内的国内外众多知名企业的核心贡献者参与社区共建。

项目地址:

github.com/iqiyi/dpvs

使用文档:

github.com/iqiyi/dpvs/…

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

发布地址:

github.com/iqiyi/dpvs/…

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

  • 从 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/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 是外网网卡。

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

因为每个 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 $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 健康检查方式有如下缺点:

  • 性能低,每次检查需要启动一个进程,并在新进程里执行一个脚本,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 有问题,可以通过如下几种方式联系到我们。