理解 Net Device Ingress 和 Egress 雙重角色
本文是書稿《圖解 VPC & K8s 網絡模型》其中一篇。書稿還在繼續寫,進度不快也不慢,因為二哥不急也不躁。好肉需要慢燉,好書需要多磨。
為什么要單獨講這個話題呢?因為我在和同事討論 K8s 網絡尤其是網絡數據流向的時候,會反復提及到網絡設備,無論它是物理的還是虛擬的。而網絡設備在我們所討論到的數據流場景里,時而在接收數據,時而在發送數據。也就是說它同時扮演著雙重身份:Ingress 和 Egress。
另外我在整理 eBPF 相關的內容,尤其是 tc eBPF 的時候,再一次發現如果不能準確地在數據流中識別出網絡設備是 Ingress 還是 Egress ,就無法將代碼邏輯和實際運行結果對上號,更勿談能理解tc eBPF 了。
這樣的雙重角色扮演就如同一個調皮的孩子,總是帶上面具在錯綜復雜的網絡里面東躲西藏,肆意玩耍。而當你好不容易抓到它時,卻讓你猜猜此時此刻他是誰。
簡單來說:對于網卡而言,無論它是物理的還是虛擬的,對于 Ingress 角色,它是首先觸碰到數據的人,而對于 Egress 角色,它是最后一個碰到數據的人。
本文我們從一個簡單的物理網卡開始,然后對 veth、 bridge 還有 tc eBPF ,分別展開聊聊:
- 當網卡扮演 Ingress 角色時,它從哪里接收數據,又將數據遞交給了誰?
- 當網卡扮演 Egress 角色時,它從哪里接收數據,又將數據遞交給了誰?
1、單個物理網卡
圖1
這是一個簡單的圖,圖中有一張物理網卡。我們的臺式機通常是這樣的配置。橘色的線代表著輸入流程,而藍色的線表示輸出流程。
(1)輸入流程 Ingress
對于這張網卡而言,輸入過程伴隨著以下幾個重要的事情:物理網卡首先接收到物理信號 -> 物理網卡通過 DMA 機制將數據保存至其專屬的 RingBuffer 里面 -> 向 CPU 發起中斷 -> OS kernel thread ksoftirqd/x 不斷地消費 RingBuffer 里面的數據。
這里的 ksoftirqd 是一個內核線程,每個 CPU 一個,x 為 CPU 編號。如 ksoftirqd/0 為 0 號 CPU 上運行的內核線程。
ksoftirqd/x 將數據以 skb 為處理粒度依次穿過鏈路層、網絡層、TCP/UDP 傳輸層 。不過 skb 在鏈路層和網絡層還可能直接 forward 給其它網卡,那這樣的話傳輸層就不會收到這個 skb 了。
整個過程如圖 2 所示,你可以從整體上感受一下。標號 1 及 1.x 為數據輸入和生產過程,這是本文的重點。而標號 3 為數據消費過程,它帶著 skb 從入口處的 net_rx_action() 沿著協議棧由底向上穿越協議棧,這個過程對本文所述的所有 Ingress 場景都是通用的,故后文不再贅述這部分。
圖2
總結:當物理網卡扮演 Ingress 角色時,它從主機外接收數據,將數據遞交給了環形隊列,然后由 ksoftirqd/x 進行后續的處理,這個處理過程也稱為網絡棧下半部分。
(2)輸出過程 Egress
從圖 1 中,我們大致可以看出來,對于輸出過程,數據來源有兩種,分別是通過 ip_forward() 過程和通過 ip_local_out() 過程送過來的數據。我們還會發現,在發送數據的路徑上,這兩個過程只是起點有些不同,剩下的路程大家都一樣。
ip_forward() 過程與 skb 在 IP 層路由結果強相關。如圖 3 所示,具體來說經過路由的判定,可能需要把 skb forward 至本機網絡設備或者網絡中的其它主機處理,不過無論是哪種情況,都需要將 skb 送往本機的一個網絡設備。
圖3
而 ip_local_out() 過程則對應了本機進程通過 socket 發送數據的場景,如圖 4 所示。這張圖最后標注的“觸發 NET_RX 類型軟中斷”是數據已經被網卡發送完后發生的事情,中斷的目的是為了清理 skb ,略過不表。
圖 4 ,圖片來源:“開發內功修煉”公眾號
總結:當物理網卡扮演 Egress 角色時,它從本機 TCP/IP 協議棧接收數據,將數據通過驅動程序送離本機。
2、veth-pair
是不是覺得單個網卡的場景其實很容易分辨出來 Ingress 和 Egress ?
別得意,我們來加點難度。我們知道 K8s 的默認 CNI flannel 用到了 veth 。veth 是什么以及它的特性二哥就不細說了。我們聊一個話題:圖 5 中,當左側進程向右側進程通信發送數據時, 左端的 veth_left 是 Ingress 還是 Egress ? 右端的 veth_right 呢?
圖 5
結合圖 5 上的箭頭示意,答案應該不難猜。對 veth_left 來說,它扮演的是 Egress 角色,因為進程需要通過它把數據發送出去。對 veth_right 而言是 Ingress ,因為它需要負責接收數據并把它送給右側的進程。
下一個問題:既然 veth_left 扮演了 Egress 角色,流量從離開 network namespace 1 之后去哪里了?既然 veth_right 是 Ingress ,那它從哪里接收到流量的?
答案都在圖 6 里面。圖中標號 2 及 2.x 在進行數據發送的工作,都屬于 veth_left 的 Egress 的過程,這個過程是發生在 network namespace 1 里面的,函數調用棧和圖 4 一樣。而標號 3 為數據消費也即 veth_right 的 Ingress 過程,這個過程和物理網卡一模一樣。
圖 6
總結:既然 veth 是一對虛擬網卡,那我們把對它倆的總結放在一起。
當 veth 網卡扮演 Egress 角色時,如圖 7 中的 veth_left,它從其所在的 network namespace TCP/IP 協議棧接收數據,并將數據遞交給了 per CPU input_pkt_queue 隊列,并觸發軟件中斷。
當 veth 網卡扮演 Ingress 角色時,如圖 7 中的 veth_right,它并沒有物理網卡那種環形隊列,而是由 ksoftirqd/x 直接從 per CPU input_pkt_queue 隊列讀取 veth_left 塞進來的數據。
veth_left 和 veth_right 共享了同一個 queue 。典型的生產者 / 消費者設計模式的即視感有沒有?
圖 7
3、bridge
上一節,二哥把 veth pair 單獨拿出來和大家一起觀賞。可它們終究不是花瓶,它們被創造出來是要有實際使用價值的。veth 典型使用場景就是把一端插入到 bridge 里面,如圖 8 所示。
從 veth 的特性來說,流量從下圖 veth1-left 流出后,會進入 veth1-right ,這也就意味著流量進入了網橋。
我想這個時候你可以確定 veth1-left 是 Egress ,而 veth1-right 是 Ingress 。那么對于 bridge 的 Port 1 和 Port 2 呢?再進一步,對于 veth2-left 和 veth2-right 呢 ?
圖 8
其實對于 bridge 這種虛擬的網橋,它的 port 口也是一個虛擬的概念,說得更直白一點,在內核里它就是一個數據結構:struct net_bridge_port 。這個結構里有 3 個重要的成員:br / port_no / dev 。下面的代碼用于插入網絡設備到 bridge ,這 3 個成員的作用顯而易見。
對于圖 8 來說, Port 1 (net_bridge_port) 就是一個粘合劑,左手 bridge ,右手 veth1-right 。理解了這點也就明白了對于 bridge 的 Port 而言,它是沒有所謂的 Ingress 和 Egress 的概念的。
Port 1 接收數據其實是 veth1-right 在 Ingress,而 bridge 把這個流量 forward 給 veth2-right 時,veth2-right 其實在扮演 Egress 角色。那流量從 veth2-right 傳至 veth2-left 的過程和 veth1-left 向 veth1-right 發送數據的過程是完全一樣的。
總結:當 veth 這樣的虛擬網卡插入在 bridge 上時:
圖8 中 veth1-left 扮演 Egress 角色,它從其所在的 network namespace TCP/IP 協議棧接收數據,將數據遞交給了 per CPU input_pkt_queue 隊列,并觸發軟件中斷。
veth1-right 扮演 Ingress 角色,它并沒有物理網卡那種環形隊列,而是由 ksoftirqd/x 直接從 per CPU input_pkt_queue 隊列讀取 veth1-left 塞進來的數據。當 ksoftirqd/x 把流量送至鏈路層時,從 br_forward() 開始進入 forward 流程。這個流程的效果就是流量從 veth1-right 轉至 veth2-right 發送出去了。
那自然 veth2-right 這個時候就扮演了 Egress 角色,veth2-left 扮演了 Ingress 角色。
4、veth-pair plus
如果你沒有暈的話,那抖索一下精神,我們開始 veth-pair 的進階版。
上一節我們看到 veth 和 bridge 搭配使用的場景。veth 另一端一定要插在 bridge 上嗎?從圖 9 你也看到了,答案是:不一定。
圖 9
現在我們知道,在圖 9 中,從 container-1 發出的流量經過 veth 發出后,veth-p 會以 Ingress 角色開始接收。根據前文的解釋,當網卡進行 Ingress 時,流量會被 ksoftirqd/x 送往協議棧進一步處理。這個處理的過程當然也就包括了圖 9 中的路由過程。
圖 9 這里巧妙的地方是:流量是產生于容器內,但對這份流量的路由卻發生在主機 root(default) network namespance 里面,使用的也是主機的路由表。如果路由結果發現需要把這份流量發往其它主機,那自然流量就從主機的 eth0 這個網卡設備離開了。在這個過程中,主機其實扮演了網關的角色。
說到這里,你能理解圖 10 的工作過程了嗎?它是 K8s host-gateway 網絡模型,顧名思義,這種網絡模型以 host 為 gateway ,更具體地說,host 的 root network namespace 充當了路由的角色。
圖 10
5、tc eBPF
如果你對 tc 和 eBPF 了解得不多或者不感興趣,可以跳過這部分。
以 Cilium 為代表的 K8s CNI 提供商一直在嘗試使用 eBPF 代替 iptables 以便優化服務網格數據面性能。其中 bpf_redicrect() 函數即為其中一項優化產出。
bpf_redicrect() 函數的特性用一句話就能解釋清楚:當 veth Ingress 時,將流量直接通過 bpf_redirect() 重定向到另一個 veth Ingress。如圖 11 所示。
不過如果你對 veth pair 哪一端在何時會扮演 Ingress 角色了解得不是很清楚的話,上面這句話其實會把你繞暈。
圖 11
但在看完二哥這篇文章后,希望你不會再暈了。在圖 11 中,從右下 Pod 出來的流量會流到位于 host network ns 這一端的 veth 上,這個 veth 是以 Ingress 角色工作的。
你看到在它身上附上了一個 eBPF 小蜜蜂圖標,表示這個時候 eBPF 程序會介入執行,執行的結果就是流量被直接通過 dev_forward_skb() forward 給了另外一個同樣位于 host network ns 端的 veth (如圖 11 箭頭所指的那個 veth),當然對這個 veth 而言,它會扮演 Egress 角色。
這個過程也可以用下面這樣的函數調用層次圖來表示。
下面是網易輕舟的一篇文章里面所附的圖。它畫出了網易輕舟基于 Cilium 網絡方案的探索和實踐細節,包括跨節點 Pod 間通信、同節點 Pod 間通信、Pod 訪問外網等各類常見的場景。
圖中 cilium_net/cilium_host? 是一對 veth pair ,它們在 Kernel 4.19? + Cilium ?1.8? 部署中已經沒什么作用了(事實上社區在考慮去掉它們)。
我想有了上面所有的鋪墊和知識,至少對標號 ① ② 所示的流程,你應該能看得懂了。
圖 12
6、總結
文末,二哥來做個小總結:
- 對于網卡而言,無論它是物理的還是虛擬的,對于 Ingress 角色,它是首先觸碰到數據的人,而對于 Egress 角色,它是最后一個碰到數據的人。
- 對于 Ingress 過程,無論是物理網卡還是虛擬網卡,在它接收到數據后,總是通過 ksoftirqd/x 進行網絡棧下半部分處理。
- 對于 Egress 過程,無論是物理網卡還是虛擬網卡,在它從鏈路層那里拿到數據后,總是通過網卡驅動程序將數據送離本設備。
- ?Ingress 過程和 Egress 過程在內核中的處理路徑完全不同,也更無對稱可言。
- 對于 veth ,無論是搭配 bridge 還是用于 host-gateway 場景,在數據流經過的關鍵位置區分是 Ingress 還是 Egress ,有助于理解系統對數據流的處理行為。
- tc eBPF 強依賴 Ingress 和 Egress,理解了它們也才能更好地理解 bpf_redirect() 。