全面適配 DPDK 20-11,DPVS 發佈 v1-9-0 版本
經過 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/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 -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 有問題,可以通過如下幾種方式聯繫到我們。
-
愛奇藝網絡虛擬化團隊郵箱:iig_cloud_qlb@qiyi.com
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1lqZCkun77f_mZ8osByfvA