eBPF 技術實踐:加速容器網(wǎng)絡轉(zhuǎn)發(fā),耗時降低60%+!
背景
Linux 具有功能豐富的網(wǎng)絡協(xié)議棧,并且兼顧了非常優(yōu)秀的性能。但是,這是相對的。單純從網(wǎng)絡協(xié)議棧各個子系統(tǒng)的角度來說,確實做到了功能與性能的平衡。不過,當把多個子系統(tǒng)組合起來,去滿足實際的業(yè)務需求,功能與性能的天平就會傾斜。
容器網(wǎng)絡就是非常典型的例子,早期的容器網(wǎng)絡,利用 bridge、netfilter + iptables (或lvs)、veth等子系統(tǒng)的組合,實現(xiàn)了基本的網(wǎng)絡轉(zhuǎn)發(fā);然而,性能卻不盡如人意。原因也比較明確:受限于當時的技術發(fā)展情況,為了滿足數(shù)據(jù)包在不同網(wǎng)絡 namespace 之間的轉(zhuǎn)發(fā),當時可以選擇的方案只有 bridge + veth 組合;為了實現(xiàn) POD 提供服務、訪問 NODE 之外的網(wǎng)絡等需求,可以選擇的方案只有 netfilter + iptables(或 lvs)。這些組合的技術方案增加了更多的網(wǎng)絡轉(zhuǎn)發(fā)耗時,故而在性能上有了更多的損耗。
然而,eBPF 技術的出現(xiàn),徹底改變了這一切。eBPF 技術帶來的內(nèi)核可編程能力,可以在原有漫長轉(zhuǎn)發(fā)路徑上,制造一些“蟲洞”,讓報文快速到達目的地。針對容器網(wǎng)絡的場景,我們可以利用 eBPF,略過 bridge、netfilter 等子系統(tǒng),加速報文轉(zhuǎn)發(fā)。
下面我們以容器網(wǎng)絡為場景,用實際數(shù)據(jù)做支撐,深入分析 eBPF 加速容器網(wǎng)絡轉(zhuǎn)發(fā)的原理。
網(wǎng)絡拓撲

如圖,兩臺設備 Node-A/B 通過 eth1 直連,網(wǎng)段為 192.168.1.0/24。
Node-A/B 中分別創(chuàng)建容器 Pod-A/B,容器網(wǎng)卡名為 ve0,是 veth 設備,網(wǎng)段為172.17.0.0/16。
Node-A/B 中分別創(chuàng)建橋接口 br0,網(wǎng)段為 172.17.0.0/16,通過 lxc0(veth 設備)與 Pod-A/B 連通。
在 Node、Pod 網(wǎng)絡 namespace 中,分別設置靜態(tài)路由;其中,Pod 中靜態(tài)路由網(wǎng)關為 br0,Node 中靜態(tài)路由網(wǎng)關為對端 Node 接口地址。
為了方便測試與分析,我們將 eth1 的網(wǎng)卡隊列設置為 1,并且將網(wǎng)卡中斷綁定到 CPU0。
bridge
bridge + veth 是容器網(wǎng)絡最早的轉(zhuǎn)發(fā)模式,我們結(jié)合上面的網(wǎng)絡拓撲,分析一下網(wǎng)絡數(shù)據(jù)包的轉(zhuǎn)發(fā)路徑。
在上面網(wǎng)絡拓撲中,eth1 收到目的地址為 172.17.0.0/16 網(wǎng)段的報文,會經(jīng)過路由查找,走到 br0 的發(fā)包流程。
br0 的發(fā)包流程,會根據(jù) FDB 表查找目的 MAC 地址歸屬的子接口,如果沒有查找到,就洪泛(遍歷所有子接口,發(fā)送報文);否則,選擇特定子接口,發(fā)送報文。在本例中,會選擇 lxc0 接口,發(fā)送報文。
lxc0 口是 veth 口,內(nèi)核的實現(xiàn)是 veth 口發(fā)包,對端(peer)的 veth 口就會收包。在本例中,Pod-A/B 中的 ve0 口會收到報文。
至此,完成收包方向的主要流程。
當報文從 Pod-A/B 中發(fā)出,會先在 Pod 的網(wǎng)絡 namespace 中查找路由,假設流量從 Pod-A 發(fā)往 Pod-B,那么會命中我們之前設置的靜態(tài)路由:172.17.0.200 via 172.17.0.1 dev ve0,最終報文會從 ve0 口發(fā)出,目的 MAC 地址為 Node-A 上面 br0 的地址。
ve0 口是 veth 口,和收包方向類似,對端的 veth 口 lxc0 會收到報文。
lxc0 口是 br0 的子接口,由于報文目的 MAC 地址為 br0 的接口地址,報文會經(jīng)過br0 口上送到 3 層協(xié)議棧處理。
3 層協(xié)議棧會查找路由,命中我們之前設置的靜態(tài)路由:172.17.0.200 via 192.168.1.20 dev eth1,最終報文會從 eth1 口發(fā)出,發(fā)給 Node-B。
至此,完成發(fā)包方向的主要流程。
上面的流程比較抽象,我們用 perf ftrace 可以非常直觀地看到報文都經(jīng)過了哪些內(nèi)核協(xié)議棧路徑。
【文章福利】小編推薦自己的Linux內(nèi)核技術交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ?


收包路徑

如圖,收包路徑主要經(jīng)歷路由查找、橋轉(zhuǎn)發(fā)、veth 轉(zhuǎn)發(fā)、veth 收包等階段,中間多次經(jīng)過 netfilter 的 hook 點。
最終調(diào)用 enqueue_to_backlog 函數(shù),數(shù)據(jù)包暫存到每個 CPU 私有的 input_pkt_queue中,一次軟中斷結(jié)束,總耗時 79us。
但是報文并沒有到達終點,后續(xù)軟中斷到來時,會有機會調(diào)用 process_backlog,處理每個 CPU 私有的 input_pkt_queue,將報文丟入 Pod 網(wǎng)絡 namespace 的協(xié)議棧繼續(xù)處理,直到將報文送往 socket 的隊列,才算是到達了終點。
綜上,收包路徑要消耗2個軟中斷,才能將報文送達終點。
發(fā)包路徑

如圖,發(fā)包路徑主要經(jīng)歷 veth 收包、橋上送、路由查找、物理網(wǎng)卡轉(zhuǎn)發(fā)等階段,中間多次經(jīng)過 netfilter 的 hook點 。
最終調(diào)用網(wǎng)卡驅(qū)動發(fā)包函數(shù),一次軟中斷結(jié)束,總耗時 62us。
分析
由 perf ftrace 的結(jié)果可以看出,利用 bridge + veth 的轉(zhuǎn)發(fā)模式,會多次經(jīng)歷netfilter、路由等子系統(tǒng),過程非常冗長,導致了轉(zhuǎn)發(fā)性能的下降。
我們接下來看一下,如何用eBPF跳過非必須的流程,加速網(wǎng)絡轉(zhuǎn)發(fā)。
TC redirect

首先,我們先看一下內(nèi)核協(xié)議棧主要支持的 eBPF hook 點,在這些 hook 點我們可以注入 eBPF 程序,實現(xiàn)具體的業(yè)務需求。
我們可以看到,與網(wǎng)絡轉(zhuǎn)發(fā)相關的 hook 點主要有 XDP(eXpress Data Path)、TC(Traffic Control)、LWT(Light Weight Tunnel)等。
針對于容器網(wǎng)絡轉(zhuǎn)發(fā)的場景,比較合適的 hook 點是 TC。因為 TC hook 點是協(xié)議棧的入口和出口,比較底層,eBPF 程序能夠獲取非常全面的上下文(如:socket、cgroup 信息等),這點是 XDP 沒有辦法做到的。而 LWT 則比較靠上層,報文到達這個 hook 點,會經(jīng)過很多子系統(tǒng)(如:netfilter)。
加速收包路徑

如圖,在 eth1 的 TC hook 點(收包方向)掛載 eBPF 程序。
eBPF 程序如下所示,其中 lxc0 接口的 index 為 2。bpf_redirect 函數(shù)為內(nèi)核提供的 helper 函數(shù),該函數(shù)會將 eth1 收到的數(shù)據(jù)包,直接轉(zhuǎn)發(fā)至 lxc0 接口。
加速發(fā)包路徑

如圖,在 lxc0 的 TC hook 點(收包方向)掛載 eBPF 程序。
eBPF 程序如下所示,其中 eth1 接口的 index 為 1。bpf_redirect 函數(shù)會將 lxc0 收到的數(shù)據(jù)包,直接轉(zhuǎn)發(fā)至 eth1 接口。
分析
由上面的操作可以看到,我們直接跳過了 bridge 的轉(zhuǎn)發(fā),利用 eBPF 程序,將 eth1 與 lxc0 之間建立了一個快速轉(zhuǎn)發(fā)通路。下面我們用 perf ftrace 看一下加速效果。
收包路徑

如圖,在收包路徑的 TC 子系統(tǒng)中,由 bpf_redirect 函數(shù)設置轉(zhuǎn)發(fā)信息( lxc0 接口 index),由 skb_do_redirect 函數(shù)直接調(diào)用了 lxc0 接口的 veth_xmit 函數(shù);略過了路由、bridge、netfilter 等子系統(tǒng)。
最終調(diào)用 enqueue_to_backlog 函數(shù),數(shù)據(jù)包暫存到每個 CPU 私有的input_pkt_queue 中,一次軟中斷結(jié)束,總耗時 43us;比 bridge 轉(zhuǎn)發(fā)模式的 79us,耗時減少約 45%。
但是,收包路徑仍然要消耗 2 個軟中斷,才能將報文送達終點。
發(fā)包路徑

如圖,在發(fā)包路徑的 TC 子系統(tǒng)中,由 bpf_redirect 函數(shù)設置轉(zhuǎn)發(fā)信息( eth1 接口 index ),由 skb_do_redirect 函數(shù)直接調(diào)用了 eth1 接口的 xmit 函數(shù);略過了路由、bridge、netfilter 等子系統(tǒng)。
最終調(diào)用網(wǎng)卡驅(qū)動發(fā)包函數(shù),一次軟中斷結(jié)束,總耗時 36us ,相比 bridge 模式 62us ,耗時減少了約 42% 。
小結(jié)
由 perf ftrace 的結(jié)果可以看出,利用 eBPF 在 TC 子系統(tǒng)注入轉(zhuǎn)發(fā)邏輯,可以跳過內(nèi)核協(xié)議棧非必須的流程,實現(xiàn)加速轉(zhuǎn)發(fā)。收發(fā)兩個方向的耗時分別減少40%左右,性能提升非常可觀。
但是,我們在收包路徑上面仍然需要消耗 2 個軟中斷,才能將報文送往目的地。接下來我們看,如何利用 redirect peer 技術來優(yōu)化這個流程。
TC redirect peer
加速收包路徑

如圖,在 eth1 的 TC hook 點(收包方向)掛載 eBPF 程序。
eBPF 程序如下所示,其中 lxc0 接口的 index 為 2。bpf_redirect_peer 函數(shù)為內(nèi)核提供的 helper 函數(shù),該函數(shù)會將 eth1 收到的數(shù)據(jù)包,直接轉(zhuǎn)發(fā)至 lxc0 接口的 peer 接口,即 ve0 接口。
分析
由于 bpf_redirect_peer 會直接將數(shù)據(jù)包轉(zhuǎn)發(fā)到 Pod 網(wǎng)絡 namespace 中,避免了enqueue_to_backlog 操作,節(jié)省了一次軟中斷,性能理論上會有提升。我們用 perf ftrace 驗證一下。

如圖,在收包路徑的 TC 子系統(tǒng)中,由 bpf_redirect_peer 函數(shù)設置轉(zhuǎn)發(fā)信息( lxc0 接口 index),由 skb_do_redirect 函數(shù)調(diào)用 veth_peer_dev 查找 lxc0 的 peer 接口,設置 skb->dev = ve0,返回 EAGAIN 給 tcf_classify 函數(shù)。
tcf_classify 函數(shù)會判斷 skb_do_redirect 的返回值,如果是 EAGAIN,則觸發(fā) __netif_receive_skb_core 函數(shù)偽遞歸調(diào)用(通過 goto 實現(xiàn))。這樣,就非常巧妙地實現(xiàn)了網(wǎng)絡 namespace 的切換(在一次軟中斷上下文中)。
最終,通過 tcp_v4_rcv 函數(shù)到達報文的終點,整個轉(zhuǎn)發(fā)流程耗時 75us。從上面的函數(shù)耗時可以看到,ip_list_rcv 函數(shù)相當于 Pod 網(wǎng)絡 namespace 的耗時,本文描述的 3 種轉(zhuǎn)發(fā)模式,這段轉(zhuǎn)發(fā)路徑是相同的。所以,將 ip_list_rcv 函數(shù)耗時減去,轉(zhuǎn)發(fā)耗時約為 14us(這里還忽略了2次軟中斷調(diào)度的時間)。比 TC redirect 模式的 43us、bridge 模式的 79us,轉(zhuǎn)發(fā)耗時分別減少為 67%、82%****。
總結(jié)
本文以容器網(wǎng)絡為例,對比了 3 種容器網(wǎng)絡轉(zhuǎn)發(fā)模式的性能差異。通過 perf ftrace 的函數(shù)調(diào)用關系以及耗時情況,詳細分析了導致性能差異的原因。我們演示了**僅僅通過幾行 eBPF 代碼,就可以大大縮短報文轉(zhuǎn)發(fā)路徑,加速內(nèi)核網(wǎng)絡轉(zhuǎn)發(fā)的效率,網(wǎng)絡轉(zhuǎn)發(fā)耗時最多可減少82%。
原文作者:王棟棟 Linux內(nèi)核之旅
