輕松理解 Docker 網絡虛擬化基礎之 veth 設備!


大家好,我是飛哥!Linux 中的 veth 是一對兒能互相連接、互相通信的虛擬網卡。通過使用它,我們可以讓 Docker 容器和母機通信,或者是在兩個 Docker 容器中進行交流。參見《輕松理解 Docker 網絡虛擬化基礎之 veth 設備!》。不過在實際中,我們會想在一台物理機上我們虛擬出來幾個、甚至幾十個容器,以求得充分壓榨物理機的硬件資源。但這樣帶來的問題是大量的容器之間的網絡互聯。很明顯上面簡單的 veth 互聯方案是沒有辦法直接工作的,我們該怎么辦???回頭想一下,在物理機的網絡環境中,多台不同的物理機之間是如何連接一起互相通信的呢?沒錯,那就是以太網交換機。同一網絡內的多台物理機通過交換機連在一起,然后它們就可以相互通信了。


在我們的網絡虛擬化環境里,和物理網絡中的交換機一樣,也需要這樣的一個軟件實現的設備。它需要有很多個虛擬端口,能把更多的虛擬網卡連接在一起,通過自己的轉發功能讓這些虛擬網卡之間可以通信。在 Linux 下這個軟件實現交換機的技術就叫做 bridge(再強調下,這是純軟件實現的)。


各個 Docker 容器都通過 veth 連接到 bridge 上,bridge 負責在不同的“端口”之間轉發數據包。這樣各個 Docker 之間就可以互相通信了!今天我們來展開聊聊 bridge 的詳細工作過程。一、如何使用 bridge在分析它的工作原理之前,很有必要先來看一看網橋是如何使用的。為了方便大家理解,接下來我們通過動手實踐的方式,在一台 Linux 上創建一個小型的虛擬網絡出來,並讓它們之間互相通信。1.1 創建兩個不同的網絡Bridge 是用來連接兩個不同的虛擬網絡的,所以在准備實驗 bridge 之前我們得先需要用 net namespace 構建出兩個不同的網絡空間來。


具體的創建過程如下。我們通過 ip netns 命令創建 net namespace。首先創建一個 net1:接下來創建一對兒 veth 出來,設備名分別是 veth1 和 veth1_p。並把其中的一頭 veth1 放到這個新的 netns 中。因為我們打算是用這個 veth1 來通信,所以需要為其配置上 ip,並把它啟動起來。查看一下,上述的配置是否成功。重復上述步驟,在創建一個新的 netns出來,命名分別為。netns: net2veth pair: veth2, veth2_pip: 192.168.0.102好了,這樣我們就在一台 Linux 就創建出來了兩個虛擬的網絡環境。1.2 把兩個網絡連接到一起在上一個步驟中,我們只是創建出來了兩個獨立的網絡環境而已。這個時候這兩個環境之間還不能互相通信。我們需要創建一個虛擬交換機 - bridge, 來把這兩個網絡環境連起來。


創建過程如下。創建一個 bridge 設備, 把剛剛創建的兩對兒 veth 中剩下的兩頭“插”到 bridge 上來。再為 bridge 配置上 IP,並把 bridge 以及插在其上的 veth 啟動起來。查看一下當前 bridge 的狀態,確認剛剛的操作是成功了的。1.3 網絡連通測試激動人心的時刻就要到了,我們在 net1 里(通過指定 ip netns exec net1 以及 -I veth1),ping 一下 net2 里的 IP(192.168.0.102)試試。


哇塞,通了通了!!這樣,我們就在一台 Linux 上虛擬出了 net1 和 net2 兩個不同的網絡環境。我們還可以按照這種方式創建更多的網絡,都可以通過一個 bridge 連接到一起。這就是 Docker 中網絡系統工作的基本原理。二、Bridge 是如何創建出來的在內核中,bridge 是由兩個相鄰存儲的內核對象來表示的。我們先看下它是如何被創建出來的。內核中創建 bridge 的關鍵代碼在 br_add_bridge 這個函數里。上述代碼中注冊網橋的關鍵代碼是 alloc_netdev 這一行。在這個函數里,將申請網橋的內核對象 net_device。在這個函數調用里要注意兩點。1.第一個參數傳入了 struct net_bridge 的大小2.第三個參數傳入的 br_dev_setup 是一個函數。帶着這兩點注意事項,我們進入到 alloc_netdev 的實現中。好吧,竟然是個宏。那就得看 alloc_netdev_mqs 了。在上述代碼中。kzalloc 是用來在內核態申請內核內存的。需要注意的是,申請的內存大小是一個 struct net_device 再加上一個 struct net_bridge(第一個參數傳進來的)。一次性就申請了兩個內核對象,這說明bridge 在內核中是由兩個內核數據結構來表示的,分別是 struct net_device 和 struct net_bridge。申請完了一家緊接着調用 setup,這實際是外部傳入的 br_dev_setup 函數。在這個函數內部進行進一步的初始化。總之,brctl addbr br0 命令主要就是完成了 bridge 內核對象(struct net_device 和 struct net_bridge)的申請以及初始化。三、添加設備調用 給網橋添加設備的時候,會將 veth 設備以虛擬的方式連到網橋上。當添加了若干個 veth 以后,內核中對象的大概邏輯圖如下。


其中 veth 是由 struct net_device來表示,bridge 的虛擬插口是由 struct net_bridge_port 來表示。我們接下來看看源碼,是如何達成上述的邏輯結果的。添加設備會調用到 net/bridge/br_if.c 下面的 br_add_if。這個函數中的第二個參數 dev 傳入的是要添加的設備。在本文中,就可以認為是 veth 的其中一頭。比較關鍵的是 net_bridge_port 這個結構體,它模擬的是物理交換機上的一個插口。它起到一個連接的作用,把 veth 和 bridge 給連接了起來。見 new_nbp 源碼如下:在 new_nbp 中,先是申請了代表插口的內核對象。find_portno 是在當前 bridge 下尋找一個可用的端口號。接下來插口對象通過 和 bridge 設備關聯了起來,通過 和代表 veth 設備的 dev 對象也建立了聯系。在 br_add_if 中還調用 netdev_rx_handler_register 注冊了設備幀接收函數,設置 veth 上的 rx_handler 為 br_handle_frame。后面在接收包的時候會回調到它。四、數據包處理過程在圖解Linux網絡包接收過程中我們講到過接收包的完整流程。數據包會被網卡先從到 RingBuffer 中,然后依次經過硬中斷、軟中斷處理。在軟中斷中再依次把包送到設備層、協議棧,最后喚醒應用程序。不過,拿 veth 設備來舉例,如果它連接到了網橋上的話,在設備層的 __netif_receive_skb_core 函數中和上述過程有所不同。連在 bridge 上的 veth 在收到數據包的時候,不會進入協議棧,而是會進入網橋處理。網橋找到合適的轉發口(另一個 veth),通過這個 veth 把數據轉發出去。工作流程如下圖。


我們從 veth1_p 設備的接收看起,所有的設備的接收都一樣,都會進入 __netif_receive_skb_core 設備層的關鍵函數。在 __netif_receive_skb_core 中先是過了 tcpdump 的抓包點,然后查找和執行了 rx_handler。在上面小節中我們看到,把 veth 連接到網橋上的時候,veth 對應的內核對象 dev 中的 rx_handler 被設置成了 br_handle_frame。所以連接到網橋上的 veth 在收到包的時候,會將幀送入到網橋處理函數 br_handle_frame 中。另外要注意的是網橋函數處理完的話,一般來說就 goto unlock 退出了。和普通的網卡數據包接收相比,並不會往下再送到協議棧了。接着來看下網橋是咋工作的吧,進入到 br_handle_frame 中來搜尋。上面我對 br_handle_frame 的邏輯進行了充分的簡化,簡化后它的核心就是調用 br_handle_frame_finish。同樣 br_handle_frame_finish 也有點小復雜。本文中,我們主要想了解的 Docker 場景下 bridge 上的 veth 設備轉發。所以根據這個場景,我又對該函數進行了充分的簡化。在硬件中,交換機和集線器的主要區別就是它會智能地把數據送到正確的端口上去,而不會像集線器那樣給所有的端口都群發一遍。所以在上面的函數中,我們看到了更新和查找轉發表的邏輯。這就是網橋在學習,它會根據它的自學習結果來工作。在找到要送往的端口后,下一步就是調用 br_forward => __br_forward 進入真正的轉發流程。在 __br_forward 中,將 skb 上的設備 dev 改為了新的目的 dev。


然后調用 br_forward_finish 進入發送流程。在 br_forward_finish 里會依次調用 br_dev_queue_push_xmit、dev_queue_xmit。dev_queue_xmit 就是發送函數,在上一篇《輕松理解 Docker 網絡虛擬化基礎之 veth 設備!》中我們介紹過,后續的發送過程就是 dev_queue_xmit => dev_hard_start_xmit => veth_xmit。在 veth_xmit 中會獲取到當前 veth 的對端,然后把數據給它發送過去。


至此,bridge 上的轉發流程就算是完畢了。要注意到的是,整個 bridge 的工作的源碼都是在 net/core/dev.c 或 net/bridge 目錄下。都是在設備層工作的。這也就充分印證了我們經常說的 bridge(物理交換機也一樣) 是二層上的設備。接下來,收到網橋發過來數據的 veth 會把數據包發送給它的對端 veth2,veth2再開始自己的數據包接收流程。


五、總結所謂網絡虛擬化,其實用一句話來概括就是用軟件來模擬實現真實的物理網絡連接。Linux 內核中的 bridge 模擬實現了物理網絡中的交換機的角色。和物理網絡類似,可以將虛擬設備插入到 bridge 上。不過和物理網絡有點不一樣的是,一對兒 veth 插入 bridge 的那端其實就不是設備了,可以理解為退化成了一個網線插頭。當 bridge 接入了多對兒 veth 以后,就可以通過自身實現的網絡包轉發的功能來讓不同的 veth 之間互相通信了。回到 Docker 的使用場景上來舉例,完整的 Docker 1 和 Docker 2 通信的過程是這樣的:


大致步驟是:1.Docker1 往 veth1 上發送數據2.由於 veth1_p 是 veth1 的 pair, 所以這個虛擬設備上可以收到包3.veth 收到包以后發現自己是連在網橋上的,於是乎進入網橋處理。在網橋設備上尋找要轉發到的端口,這時找到了 veth2_p 開始發送。網橋完成了自己的轉發工作4.veth2 作為 veth2_p 的對端,收到了數據包5.Docker2 里的就可以從 veth2 設備上收到數據了覺得這個流程圖還不過癮?那我們再繼續拉大視野,從兩個 Docker 的用戶態來開始看一看。


Docker 1 在需要發送數據的時候,先通過 send 系統調用發送,這個發送會執行到協議棧進行協議頭的封裝等處理。經由鄰居子系統找到要使用的設備(veth1)后,從這個設備將數據發送出去,veth1 的對端 veth1_p 會收到數據包。收到數據的 veth1_p 是一個連接在 bridge 上的設備,這時候 bridge 會接管該 veth 的數據接收過程。從自己連接的所有設備中查找目的設備。找到 veth2_p 以后,調用該設備的發送函數將數據發送出去。同樣 veth2_p 的對端 veth2 即將收到數據。其中 veth2 收到數據后,將和 lo、eth0 等設備一樣,進入正常的數據接收處理過程。Docker 2 中的用戶態進程將能夠收到 Docker 1 發送過來的數據了就。怎么樣,今天你有沒有更深入地理解了 Docker 的工作原理呢?最后轉發到朋友圈,讓你的朋友們也一起來學學吧~~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM