背景
Linux 具有功能豐富的網(wǎng)絡(luò)協(xié)議棧,并且兼顧了非常優(yōu)秀的性能。但是,這是相對的。單純從網(wǎng)絡(luò)協(xié)議棧各個子系統(tǒng)的角度來說,確實做到了功能與性能的平衡。不過,當(dāng)把多個子系統(tǒng)組合起來,去滿足實際的業(yè)務(wù)需求,功能與性能的天平就會傾斜。
容器網(wǎng)絡(luò)就是非常典型的例子,早期的容器網(wǎng)絡(luò),利用 bridge、netfilter + iptables (或lvs)、veth等子系統(tǒng)的組合,實現(xiàn)了基本的網(wǎng)絡(luò)轉(zhuǎn)發(fā);然而,性能卻不盡如人意。原因也比較明確:受限于當(dāng)時的技術(shù)發(fā)展情況,為了滿足數(shù)據(jù)包在不同網(wǎng)絡(luò) namespace 之間的轉(zhuǎn)發(fā),當(dāng)時可以選擇的方案只有 bridge + veth 組合;為了實現(xiàn) POD 提供服務(wù)、訪問 NODE 之外的網(wǎng)絡(luò)等需求,可以選擇的方案只有 netfilter + iptables(或 lvs)。這些組合的技術(shù)方案增加了更多的網(wǎng)絡(luò)轉(zhuǎn)發(fā)耗時,故而在性能上有了更多的損耗。
然而,eBPF 技術(shù)的出現(xiàn),徹底改變了這一切。eBPF 技術(shù)帶來的內(nèi)核可編程能力,可以在原有漫長轉(zhuǎn)發(fā)路徑上,制造一些“蟲洞”,讓報文快速到達(dá)目的地。針對容器網(wǎng)絡(luò)的場景,我們可以利用 eBPF,略過 bridge、netfilter 等子系統(tǒng),加速報文轉(zhuǎn)發(fā)。
下面我們以容器網(wǎng)絡(luò)為場景,用實際數(shù)據(jù)做支撐,深入分析 eBPF 加速容器網(wǎng)絡(luò)轉(zhuǎn)發(fā)的原理。
網(wǎng)絡(luò)拓?fù)?/strong>
如圖,兩臺設(shè)備 Node-A/B 通過 eth1 直連,網(wǎng)段為 192.168.1.0/24。
Node-A/B 中分別創(chuàng)建容器 Pod-A/B,容器網(wǎng)卡名為 ve0,是 veth 設(shè)備,網(wǎng)段為172.17.0.0/16。
Node-A/B 中分別創(chuàng)建橋接口 br0,網(wǎng)段為 172.17.0.0/16,通過 lxc0(veth 設(shè)備)與 Pod-A/B 連通。
在 Node、Pod 網(wǎng)絡(luò) namespace 中,分別設(shè)置靜態(tài)路由;其中,Pod 中靜態(tài)路由網(wǎng)關(guān)為 br0,Node 中靜態(tài)路由網(wǎng)關(guān)為對端 Node 接口地址。
為了方便測試與分析,我們將 eth1 的網(wǎng)卡隊列設(shè)置為 1,并且將網(wǎng)卡中斷綁定到 CPU0。
# ethtool -L eth1 combined 1 # echo 0 > /proc/irq/$(cat /proc/interrupts | awk -F ':' '/eth1/ {gsub(/ /,""); print $1}')/smp_affinity_list
bridge
bridge + veth 是容器網(wǎng)絡(luò)最早的轉(zhuǎn)發(fā)模式,我們結(jié)合上面的網(wǎng)絡(luò)拓?fù)洌治鲆幌戮W(wǎng)絡(luò)數(shù)據(jù)包的轉(zhuǎn)發(fā)路徑。
在上面網(wǎng)絡(luò)拓?fù)渲校琫th1 收到目的地址為 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 口會收到報文。
至此,完成收包方向的主要流程。
當(dāng)報文從 Pod-A/B 中發(fā)出,會先在 Pod 的網(wǎng)絡(luò) namespace 中查找路由,假設(shè)流量從 Pod-A 發(fā)往 Pod-B,那么會命中我們之前設(shè)置的靜態(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é)議棧會查找路由,命中我們之前設(shè)置的靜態(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é)議棧路徑。
收包路徑
# perf ftrace -C0 -G '__netif_receive_skb_list_core' -g 'smp_*'
?
?
如圖,收包路徑主要經(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。
但是報文并沒有到達(dá)終點,后續(xù)軟中斷到來時,會有機會調(diào)用 process_backlog,處理每個 CPU 私有的 input_pkt_queue,將報文丟入 Pod 網(wǎng)絡(luò) namespace 的協(xié)議棧繼續(xù)處理,直到將報文送往 socket 的隊列,才算是到達(dá)了終點。
綜上,收包路徑要消耗2個軟中斷,才能將報文送達(dá)終點。
發(fā)包路徑
# perf ftrace -C0 -G '__netif_receive_skb_core' -g 'smp_*'
?
?
如圖,發(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),過程非常冗長,導(dǎo)致了轉(zhuǎn)發(fā)性能的下降。
我們接下來看一下,如何用eBPF跳過非必須的流程,加速網(wǎng)絡(luò)轉(zhuǎn)發(fā)。
TC redirect
首先,我們先看一下內(nèi)核協(xié)議棧主要支持的 eBPF hook 點,在這些 hook 點我們可以注入 eBPF 程序,實現(xiàn)具體的業(yè)務(wù)需求。
我們可以看到,與網(wǎng)絡(luò)轉(zhuǎn)發(fā)相關(guān)的 hook 點主要有 XDP(eXpress Data Path)、TC(Traffic Control)、LWT(Light Weight Tunnel)等。
針對于容器網(wǎng)絡(luò)轉(zhuǎn)發(fā)的場景,比較合適的 hook 點是 TC。因為 TC hook 點是協(xié)議棧的入口和出口,比較底層,eBPF 程序能夠獲取非常全面的上下文(如:socket、cgroup 信息等),這點是 XDP 沒有辦法做到的。而 LWT 則比較靠上層,報文到達(dá)這個 hook 點,會經(jīng)過很多子系統(tǒng)(如:netfilter)。
加速收包路徑
如圖,在 eth1 的 TC hook 點(收包方向)掛載 eBPF 程序。
# tc qdisc add dev eth1 clsact #?tc?filter?add?dev?eth1?ingress?bpf?da?obj?ingress_redirect.o?sec?classifier-redirect
eBPF 程序如下所示,其中 lxc0 接口的 index 為 2。bpf_redirect 函數(shù)為內(nèi)核提供的 helper 函數(shù),該函數(shù)會將 eth1 收到的數(shù)據(jù)包,直接轉(zhuǎn)發(fā)至 lxc0 接口。
SEC("classifier-redirect") int cls_redirect(struct __sk_buff *skb) { /* The ifindex of lxc0 is 2 */ return bpf_redirect(2, 0); }
加速發(fā)包路徑
如圖,在 lxc0 的 TC hook 點(收包方向)掛載 eBPF 程序。
# tc qdisc add dev lxc0 clsact # tc filter add dev lxc0 ingress bpf da obj egress_redirect.o sec classifier-redirect
eBPF 程序如下所示,其中 eth1 接口的 index 為 1。bpf_redirect 函數(shù)會將 lxc0 收到的數(shù)據(jù)包,直接轉(zhuǎn)發(fā)至 eth1 接口。
SEC("classifier-redirect") int cls_redirect(struct __sk_buff *skb) { /* The ifindex of eth1 is 1 */ return bpf_redirect(1, 0); }
分析
由上面的操作可以看到,我們直接跳過了 bridge 的轉(zhuǎn)發(fā),利用 eBPF 程序,將 eth1 與 lxc0 之間建立了一個快速轉(zhuǎn)發(fā)通路。下面我們用 perf ftrace 看一下加速效果。
收包路徑
# perf ftrace -C0 -G '__netif_receive_skb_list_core' -g 'smp_*'
?
?
如圖,在收包路徑的 TC 子系統(tǒng)中,由 bpf_redirect 函數(shù)設(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 個軟中斷,才能將報文送達(dá)終點。
發(fā)包路徑
如圖,在發(fā)包路徑的 TC 子系統(tǒng)中,由 bpf_redirect 函數(shù)設(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 技術(shù)來優(yōu)化這個流程。
TC redirect peer
加速收包路徑
如圖,在 eth1 的 TC hook 點(收包方向)掛載 eBPF 程序。
# tc qdisc add dev eth1 clsact # tc filter add dev eth1 ingress bpf da obj ingress_redirect_peer.o sec classifier-redirect
eBPF 程序如下所示,其中 lxc0 接口的 index 為 2。bpf_redirect_peer 函數(shù)為內(nèi)核提供的 helper 函數(shù),該函數(shù)會將 eth1 收到的數(shù)據(jù)包,直接轉(zhuǎn)發(fā)至 lxc0 接口的 peer 接口,即 ve0 接口。
SEC("classifier-redirect") int cls_redirect(struct __sk_buff *skb) { /* The ifindex of lxc0 is 2 */ return bpf_redirect_peer(2, 0); }
分析
由于 bpf_redirect_peer 會直接將數(shù)據(jù)包轉(zhuǎn)發(fā)到 Pod 網(wǎng)絡(luò) namespace 中,避免了enqueue_to_backlog 操作,節(jié)省了一次軟中斷,性能理論上會有提升。我們用 perf ftrace 驗證一下。
# perf ftrace -C0 -G '__netif_receive_skb_list_core' -g 'smp_*
?
?
'
如圖,在收包路徑的 TC 子系統(tǒng)中,由 bpf_redirect_peer 函數(shù)設(shè)置轉(zhuǎn)發(fā)信息( lxc0 接口 index),由 skb_do_redirect 函數(shù)調(diào)用 veth_peer_dev 查找 lxc0 的 peer 接口,設(shè)置 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)絡(luò) namespace 的切換(在一次軟中斷上下文中)。
最終,通過 tcp_v4_rcv 函數(shù)到達(dá)報文的終點,整個轉(zhuǎn)發(fā)流程耗時 75us。從上面的函數(shù)耗時可以看到,ip_list_rcv 函數(shù)相當(dāng)于 Pod 網(wǎng)絡(luò) 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)絡(luò)為例,對比了 3 種容器網(wǎng)絡(luò)轉(zhuǎn)發(fā)模式的性能差異。通過 perf ftrace 的函數(shù)調(diào)用關(guān)系以及耗時情況,詳細(xì)分析了導(dǎo)致性能差異的原因。我們演示了僅僅通過幾行 eBPF 代碼,就可以大大縮短報文轉(zhuǎn)發(fā)路徑,加速內(nèi)核網(wǎng)絡(luò)轉(zhuǎn)發(fā)的效率,網(wǎng)絡(luò)轉(zhuǎn)發(fā)耗時最多可減少82%。
目前 eBPF 技術(shù)在開源社區(qū)非常流行,在 tracing、安全、網(wǎng)絡(luò)等領(lǐng)域有廣泛應(yīng)用,我們可以利用這項技術(shù)做很多有意思的事情。感興趣的朋友可以加入我們,一起討論交流。
作者:王棟棟,字節(jié)跳動系統(tǒng)技術(shù)與工程團隊內(nèi)核工程師,10年系統(tǒng)工程師工作經(jīng)驗,關(guān)注Linux networking、eBPF等領(lǐng)域。目前在字節(jié)跳動,主要負(fù)責(zé)eBPF、內(nèi)核網(wǎng)絡(luò)協(xié)議棧相關(guān)的開發(fā)工作。
編輯:黃飛
?
評論
查看更多