經過 DPVS 團隊和社區開發者一個多季度的開發迭代,愛奇藝開源項目DPVS已經正式發佈了v1.9.0版本。DPVS v1.9.0 正式發佈於2021/9/1,它適配了當前DPDK 穩定版本DPDK-20.11(LTS),支持 DPDK API/ABI 以及多種設備驅動的更新和優化。目前 DPVS v1.9.0 已在愛奇藝的多個核心數據中心部署上線,且穩定運行三個月。
01
關於 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
02
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 驅動在調試工作模式下程序崩潰問題。
03
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 是外網網卡。
/*
* 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。
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 更完善的調度算法
因爲每個 worker 上的調度算法和數據是相互獨立的,而且所有 worker 以相同的方式初始化,所以每個 worker 會以相同的順序選取 RS。比如,對於輪詢(rr)調度,所有 worker 上的第一個連接都會選擇 RS 列表中的第一臺服務器。下圖給出了 8 個 worker, 5 個 RS 的調度情況:假設 RSS HASH 算法是平衡的,則很可能前 8 個用戶連接分別哈希到 8 個不同 worker 上,而 8 個 worker 獨立調度,將 8 個用戶流量全都轉發到第一個 RS 上,而其餘 4 個 RS 沒有用戶連接,使得負載在 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 健康檢查
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
-
性能低,每次檢查需要啓動一個進程,並在新進程裏執行一個腳本,CPU 消耗大,一般只能支持數百個 RS 的情況。 -
檢查不準確,一般只能探測到端口是否可用,不能根據實際業務情況配置。 -
配置複雜,需要在系統裏額外安裝健康檢查腳本。 -
檢查結果依賴外部工具,可靠性、一致性不能保證。
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 的健康檢查。 -
不僅支持端口探測,而且支持業務探測。 -
配置簡單,無外部依賴,使用方便。
04
未來版本計劃
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+任意端口" 類型的業務類型。
05
參與社區
愛奇藝網絡虛擬化團隊郵箱:[email protected]
關注我們,更多精彩內容陪伴你!
本文分享自微信公衆號 - 愛奇藝技術產品團隊(iQIYI-TP)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。