從 VLAN 到 IPVLAN: 聊聊虛擬網路裝置及其在雲原生中的應用

語言: CN / TW / HK

作者:張偉(謝石)

由於這篇文章真的很長,大量的篇幅在講述核心的實現,如果你對這部分不感興趣,那麼在建議你在看完第一部分的三個問題後,思考一下,然後直接跳轉到我們對問題的回答。

提出問題

注:本文所有的程式碼均為 Linux 核心原始碼,版本為 5.16.2

你聽說過 VLAN 麼?它的全稱是 Virtual Local Area Network,用於在乙太網中隔離不同的廣播域。它誕生的時間很早,1995 年,IEEE 就發表了 802.1Q 標準 [ 1] 定義了在乙太網資料幀中 VLAN 的格式,並且沿用至今。如果你知道 VLAN,那麼你聽說過 MACVlan 和 IPVlan 麼?隨著容器技術的不斷興起,IPVlan 和 MACVlan 作為 Linux 虛擬網路裝置,慢慢走上前臺,在 2017 年 Docker Engine 的 1.13.1 的版本 [2 ] 中,就開始引入了 IPVlan 和 MACVlan 作為容器的網路解決方案。

那麼你是否也有過以下的疑問呢?

1. VLAN 和 IPVlan,MACVlan 有什麼關係呢?為什麼名字裡都有 VLAN?

2. IPVlan 和 MACVlan 為什麼會有各種模式和 flag,比如 VEPA,Private,Passthrough 等等?它們的區別在哪裡?

3. IPVlan 和 MACVlan 的優勢在哪裡?你應該在什麼情況下接觸到,使用到它們呢?

我也曾有過一樣的問題,今天這篇文章,我們就針對上面三個問題一探究竟。

背景知識

以下為一些背景知識,如果你對 Linux 本身很瞭解,可以跳過。

  • 核心對網路裝置的抽象

在 Linux 中,我們操作一個網路裝置,不外乎使用 ip 命令或者 ifconfig 命令。對於 ip 命令的實現 iproute2 來說,它真正依賴的就是 Linux 提供的 netlink 訊息機制,核心會對每一類網路裝置(無論是真實的還是虛擬的)抽象出一個專門響應 netlink 訊息的結構體,它們都按照 rtnl_link_ops 結構來實現,用於響應對網路裝置的建立,銷燬和修改。例如比較直觀的 Veth 裝置:

static struct rtnl_link_ops veth_link_ops = { .kind = DRV_NAME, .priv_size = sizeof(struct veth_priv), .setup = veth_setup, .validate = veth_validate, .newlink = veth_newlink, .dellink = veth_dellink, .policy = veth_policy, .maxtype = VETH_INFO_MAX, .get_link_net = veth_get_link_net, .get_num_tx_queues = veth_get_num_queues, .get_num_rx_queues = veth_get_num_queues, };

對於一個網路裝置來說,Linux 的操作和硬體裝置的響應本身也需要一套規範,Linux 將其抽象為 net_device_ops 這個結構體,如果你對裝置驅動感興趣,那主要就是和它打交道,依然以 Veth 裝置為例:

``` static const struct net_device_ops veth_netdev_ops = { .ndo_init = veth_dev_init, .ndo_open = veth_open, .ndo_stop = veth_close, .ndo_start_xmit = veth_xmit, .ndo_get_stats64 = veth_get_stats64, .ndo_set_rx_mode = veth_set_multicast_list, .ndo_set_mac_address = eth_mac_addr,

ifdef CONFIG_NET_POLL_CONTROLLER

.ndo_poll_controller = veth_poll_controller,

endif

.ndo_get_iflink = veth_get_iflink, .ndo_fix_features = veth_fix_features, .ndo_set_features = veth_set_features, .ndo_features_check = passthru_features_check, .ndo_set_rx_headroom = veth_set_rx_headroom, .ndo_bpf = veth_xdp, .ndo_xdp_xmit = veth_ndo_xdp_xmit, .ndo_get_peer_dev = veth_peer_dev, }; ```

從上面的定義我們可以看到幾個語義很直觀的方法:ndo_start_xmit 用於傳送資料包,newlink 用於建立一個新的裝置。

對於接收資料包,Linux 的收包動作並不是由各個程序自己去完成的,而是由 ksoftirqd 核心執行緒負責了從驅動接收、網路層(ip,iptables)、傳輸層(tcp,udp)的處理,最終放到使用者程序持有的 Socket 的 recv 緩衝區中,然後由核心 inotify 使用者程序處理。對於虛擬裝置來說,所有的差異集中於網路層之前,在這裡有一個統一的入口,即__netif_receive_skb_core。

  • 801.2q 協議對 VLAN 的定義

802.1q 協議中,乙太網資料幀包頭中用於標記 VLAN 欄位是一個 32bit 的域,結構如下:

1.png

如上所示,有 16 個 bit 用於標記 Protocol,3 個 bit 用於標記優先順序,1 個 bit 用於標記格式,12 個 bit 用於存放   VLAN id,看到這裡我想你可以輕易計算出,依靠 VLAN 我們能劃分出多少個廣播域?沒錯,正是 2*12,4096 個,減去保留的全 0 和全 1 ,客戶劃分出 4094 個可用的廣播域。(在 OpenFlow 興起之前,雲端計算最早期雛形中的 vpc 的實現正是依賴 VLAN 進行網路的區分,但是由於這個限制,很快就被淘汰了,這也催生了另一個你也許似曾相識的名詞,VxLAN,儘管兩者差別很大,但是仍有借鑑的緣故)。

VLAN 原本和 bridge 一樣是一個交換機上的概念,不過 Linux 將它們都進行了軟體的實現,Linux 在每個乙太網資料幀中使用一個 16bit 的 vlan_proto 欄位和 16bit 的 vlan_tci 欄位實現 802.1q 協議,同時對於每一個 VLAN,都會虛擬出一個子裝置來處理去除 VLAN 之後的報文,沒錯 VLAN 也有屬於自己的子裝置,即 VLAN sub-interface,不同的 VLAN 子裝置通過一個主裝置進行物理上的報文收發,這個概念是否又有點熟悉?沒錯,這正是 ENI-Trunking 的原理。

深入 VLAN/MACVlan/IPVlan 的核心實現

補充了背景知識後,我們就先從 VLAN 子裝置開始,看看 Linux 核心究竟是怎麼做的,這裡所有的核心程式碼都以時下較新的 5.16.2 版本為例。

VLAN 子裝置

  • 裝置建立

VLAN 子裝置起初並沒有被當作一類單獨的虛擬裝置來處理,畢竟出現的時間很早,程式碼分佈比較亂,不過核心邏輯位於/net/8021q/路徑下。從背景中我們可以瞭解到,netlink 機制中實現了網絡卡裝置建立的入口,對於 VLAN 子裝置,它們的 netlink 訊息實現的結構體是 vlan_link_ops,而負責建立 VLAN 子裝置的是 vlan_newlink 方法,核心初始化程式碼流程如下:

2.png

  1. 首先建立一個 Linux 通用的 net_device 結構體儲存裝置的配置資訊,進入 vlan_newlink 之後,會進行 vlan_check_real_dev 檢查傳入的 VLAN id 是否是可用的,這其中會呼叫到 vlan_find_dev 方法,這個方法用於針對一個主裝置查詢到符合條件的子裝置,後面還會用到,我們擷取一部分程式碼觀察一下:

``` static int vlan_newlink(struct net src_net, struct net_device dev, struct nlattr tb[], struct nlattr data[], struct netlink_ext_ack extack) { struct vlan_dev_priv vlan = vlan_dev_priv(dev); struct net_device *real_dev; unsigned int max_mtu; __be16 proto; int err;

/這裡省略掉了用於引數校驗的部分/

// 這裡會設定vlan子裝置的vlan資訊,也就是背景知識中vlan相關的protocol,vlanid,優先順序和flag資訊的預設值

vlan->vlan_proto = proto; vlan->vlan_id = nla_get_u16(data[IFLA_VLAN_ID]); vlan->real_dev = real_dev; dev->priv_flags |= (real_dev->priv_flags & IFF_XMIT_DST_RELEASE); vlan->flags = VLAN_FLAG_REORDER_HDR;

err = vlan_check_real_dev(real_dev, vlan->vlan_proto, vlan->vlan_id, extack); if (err < 0) return err;

/這裡會進行mtu的設定/

err = vlan_changelink(dev, tb, data, extack); if (!err) err = register_vlan_dev(dev, extack); if (err) vlan_dev_uninit(dev); return err; } ```

  1. 接下來是通過 vlan_changelink 方法對裝置的屬性進行設定,如果你有特殊的配置,則會覆蓋預設值。

  2. 最後進入 register_vlan_dev 方法,這個方法就是把前面已經完成好的資訊裝填到 net_device 結構體,並按照  Linux 的裝置管理統一介面註冊到核心中。

  3. 接收報文

從建立過程來看, VLAN 子裝置與一般裝置的區別就在於它能夠被主裝置和 VLAN id 通過 vlan_find_dev 的方式找到,這一點很重要。

接下來我們來看報文的接收過程,根據背景知識,物理裝置接收到報文後,在進入協議棧處理之前,常規的入口是 __netif_receive_skb_core,我們就從這個入口開始逐漸分析,核心操作流程如下:

3.png

根據上方的示意圖,我們擷取部分__netif_receive_skb_core 進行分析:

  1. 首先在資料包處理流程開始的時候,會進行 skb_vlan_untag 操作,對於 VLAN 資料包來說,資料包 Protocol 欄位一直是 VLAN 的 ETH_P_8021Q ,skb_vlan_untag 就是將 VLAN 資訊從資料包的 vlan_tci 欄位中提取後,呼叫 vlan_set_encap_proto 將 Protocol 更新為正常的網路層協議,這時 VLAN 已經一部分轉變為正常資料包了。

  2. 擁有 VLAN tag 的資料包會在 skb_vlan_tag_present 中進入 vlan_do_recieve 的處理流程,vlan_do_receive 的處理過程的核心就是通過 vlan_find_dev 找到子裝置,將資料包中的 dev 設定為子裝置,然後將 Priority 等與 VLAN 相關的資訊進行清理,到了這裡,VLAN 資料包已經轉變為一個發往 VLAN 子裝置的普通資料包了。

  3. 在 vlan_do_receive 完成後,會進入 another_round,重新按照正常資料包的流程執行一次__netif_receive_skb_core,按照正常包的處理邏輯進入,進入了 rx_handler 的處理,就像一個正常的資料包一樣,在子裝置上通過與主裝置相同的 rx_handler 進入到網路層。

``` static int __netif_receive_skb_core(struct sk_buff pskb, bool pfmemalloc, struct packet_type ppt_prev) { rx_handler_func_t rx_handler; struct sk_buff skb = pskb; struct net_device orig_dev;

another_round: skb->skb_iif = skb->dev->ifindex; / 這是嘗試對資料幀報文字身做一次vlan的解封裝,也就從將背景中的vlan相關的兩個欄位填充/ if (eth_type_vlan(skb->protocol)) { skb = skb_vlan_untag(skb); if (unlikely(!skb)) goto out; }

/* 這裡就是你所熟知的tcpdump的抓包點了,pt_prev記錄了上一個處理報文的handler,如你所見,一份skb可能被很多地方處理,包括pcap */

list_for_each_entry_rcu(ptype, &ptype_all, list) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; }

/ 這裡在存在vlan tag的情況下,如果有pt_prev已經存在,則做一次deliver_skb,這樣其他handler處理的時候就會複製一份,原始報文就不會被修改 / if (skb_vlan_tag_present(skb)) { if (pt_prev) { ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = NULL; } / 這裡是核心的部分,我們看到經過vlan_do_receive處理之後,會變成正常包文再來一遍 / if (vlan_do_receive(&skb)) goto another_round; else if (unlikely(!skb)) goto out; }

/* 這裡是正常報文應該到達的地方,pt_prev表示已經找到了正常的handler,然後呼叫rx_handler進入上層處理 */

rx_handler = rcu_dereference(skb->dev->rx_handler); if (rx_handler) { if (pt_prev) { ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = NULL; } switch (rx_handler(&skb)) { case RX_HANDLER_CONSUMED: ret = NET_RX_SUCCESS; goto out; case RX_HANDLER_ANOTHER: goto another_round; case RX_HANDLER_EXACT: deliver_exact = true; break; case RX_HANDLER_PASS: break; } }

if (unlikely(skb_vlan_tag_present(skb)) && !netdev_uses_dsa(skb->dev)) { check_vlan_id: if (skb_vlan_tag_get_id(skb)) { /* 這裡是對vlan id並沒有正確被摘除的處理,通常是因為vlan id不合法或者不存在在本地 } } ```

  • 資料傳送

VLAN 子裝置的資料傳送的入口是 vlan_dev_hard_start_xmit,相比於收包流程,其實發送的流程簡單很多,核心在傳送時的流程如下:

4.png

在硬體傳送時,VLAN 子裝置會進入 vlan_dev_hard_start_xmit 方法,這個方法實現了 ndo_start_xmit 介面,它通過__vlan_hwaccel_put_tag 方法填充 VLAN 相關的乙太網資訊到報文中,然後修改了報文的裝置為主裝置,呼叫主裝置的 dev_queue_xmit 方法重新進入主裝置的傳送佇列進行傳送,我們擷取關鍵的一部分來分析:

``` static netdev_tx_t vlan_dev_hard_start_xmit(struct sk_buff skb, struct net_device dev) { / 這裡就是上文提到的vlan_tci的填充,這些資訊都歸屬於子裝置本身 / if (veth->h_vlan_proto != vlan->vlan_proto || vlan->flags & VLAN_FLAG_REORDER_HDR) { u16 vlan_tci; vlan_tci = vlan->vlan_id; vlan_tci |= vlan_dev_get_egress_qos_mask(dev, skb->priority); __vlan_hwaccel_put_tag(skb, vlan->vlan_proto, vlan_tci); }

/* 這裡直接將裝置從子裝置改為了主裝置,非常直接 */

skb->dev = vlan->real_dev; len = skb->len; if (unlikely(netpoll_tx_running(dev))) return vlan_netpoll_send_skb(vlan, skb);

/* 這裡就可以直接呼叫主裝置進行報文傳送了 */

ret = dev_queue_xmit(skb);

...

return ret; } ```

MACVlan 裝置

看完 VLAN 子裝置之後,馬上對 MACVlan 進行分析,MACVlan 與 VLAN 子裝置不一樣的是,它已經不再是乙太網本身的能力了,而是一種有自己驅動的虛擬網裝置,這一點首先就體現在驅動程式碼的獨立,MACVlan 相關的程式碼基本都位於/drivers/net/macvlan.c 中。

MACVlan 裝置有有五種 mode,其中除了 source 模式外,其餘四種都出現比較早,定義如下:

enum macvlan_mode { MACVLAN_MODE_PRIVATE = 1, /* don't talk to other macvlans */ MACVLAN_MODE_VEPA = 2, /* talk to other ports through ext bridge */ MACVLAN_MODE_BRIDGE = 4, /* talk to bridge ports directly */ MACVLAN_MODE_PASSTHRU = 8,/* take over the underlying device */ MACVLAN_MODE_SOURCE = 16,/* use source MAC address list to assign */ };

這裡先記住這些模式的行為,關於其中的原因是我們後面要回答的問題。

  • 裝置建立

對於 MACVlan 裝置來說,它的 netlink 響應結構體是 macvlan_link_ops,我們可以找到建立裝置的響應方法為macvlan_newlink,從入口開始,建立一個 MACVlan 裝置的整體流程如下:

5.png

  1. macvlan_newlink 會呼叫 macvlan_common_newlink 進行實際的子裝置建立操作,macvlan_common_newlink 首先會進行一個合法性的校驗,這其中需要注意的就是 netif_is_MACVlan 檢查,如果把一個 MACVlan 子裝置作為主裝置來建立的話,那麼會自動採用這個子裝置的主裝置作為新建網絡卡的主裝置。

  2. 接下來會通過  eth_hw_addr_random 給 MACVlan 裝置建立一個隨機的 mac 地址,沒錯,MACVlan 子裝置的 mac 地址是隨機的,這一點很重要,後面會提到。

  3. 在有了 mac 地址之後,開始在主裝置上初始化 MACVlan 邏輯,這裡會有個檢查,如果主裝置從未建立過 MACVlan 裝置,則會通過 macvlan_port_create 來支援 MACVlan 的初始化,而這個初始化最為核心的就是,呼叫 netdev_rx_handler_register 進行了 MACVlan 的 rx_handler 方法 macvlan_handle_frame 去取代了裝置原來註冊的 rx_handler 的動作。

  4. 在初始化完成後,獲得一個 port,也就是子裝置,然後對子裝置的資訊進行了設定。

  5. 最後通過 register_netdevice 完成了裝置的建立動作。我們擷取部分核心邏輯進行分析:

``` int macvlan_common_newlink(struct net src_net, struct net_device dev, struct nlattr tb[], struct nlattr data[], struct netlink_ext_ack *extack) { ...

/* 這裡檢查了主裝置是否是macvlan裝置,如果是則直接使用他的主裝置 */

if (netif_is_macvlan(lowerdev)) lowerdev = macvlan_dev_real_dev(lowerdev);

/* 這裡生成了隨機的mac地址 */

if (!tb[IFLA_ADDRESS]) eth_hw_addr_random(dev);

/ 這裡進行了初始化操作,也就是替換了rx_handler / if (!netif_is_macvlan_port(lowerdev)) { err = macvlan_port_create(lowerdev); if (err < 0) return err; create = true; } port = macvlan_port_get_rtnl(lowerdev);

/* 接下來一大段都是省略的關於模式的設定 */

vlan->lowerdev = lowerdev; vlan->dev = dev; vlan->port = port; vlan->set_features = MACVLAN_FEATURES; vlan->mode = MACVLAN_MODE_VEPA;

/* 最後註冊了裝置 */

err = register_netdevice(dev); if (err < 0) goto destroy_macvlan_port; } ```

  • 接收報文

MACVlan 裝置的報文接收依然是從__netif_receive_skb_core 入口開始,具體的程式碼流程如下:

6.png

  1. 當__netif_receive_skb_core 在主裝置接收後,會進入 MACVlan 驅動註冊的 macvlan_handle_frame 方法,這個方法首先會處理多播的報文,然後處理單播的報文。

  2. 對於多播報文,經過 is_multicast_ether_addr 後,首先通過 macvlan_hash_lookup,通過子裝置上的相關資訊查詢到子裝置,則根據網絡卡的 mode 進行處理,如果是 private 或者 passthrou,找到子裝置並單獨通過 macvlan_broadcast_one 送給它;如果是 bridge 或者沒有 VEPA,則所有的子裝置都會通過 macvlan_broadcast_enqueue 收到廣播報文。

  3. 對於單播的報文,首先會將 source 模式和 passthru 模式進行處理,直接觸發上層的操作,對於其他模式,根據源 mac 進行 macvlan_hash_lookup 操作,如果找到了 VLAN 資訊,則將報文的 dev 設定為找到的子裝置。

  4. 最後對報文進行 pkt_type 的設定,將其通過 RX_HANDLER_ANOTHER 的返回,再進行一次__netif_receive_skb_core 的操作,這次操作中,走到 macvlan_hash_lookup 時,由於已經是子裝置,所以會返回 RX_HANDLER_PASS 從而進入上層的處理。

  5. 對於 MACVlan 的資料接收過程,最為關鍵的就是主裝置接收到報文後選擇子裝置的邏輯,這部分程式碼如下:

``` static struct macvlan_dev macvlan_hash_lookup(const struct macvlan_port port, const unsigned char addr) { struct macvlan_dev vlan; u32 idx = macvlan_eth_hash(addr);

hlist_for_each_entry_rcu(vlan, &port->vlan_hash[idx], hlist, lockdep_rtnl_is_held()) { / 這部分邏輯就是macvlan查詢子裝置的核心,比較mac地址 / if (ether_addr_equal_64bits(vlan->dev->dev_addr, addr)) return vlan; } return NULL; } ```

  • 傳送報文

MACVlan 的傳送報文過程也是從子裝置接收到 ndo_start_xmit 回撥函式開始,它的入口是 macvlan_start_xmit,整體的核心程式碼流程如下:

7.png

  1. 當資料包進入 macvlan_start_xmit 後,主要執行資料包傳送操作的是 macvlan_queue_xmit 方法。

  2. macvlan_queue_xmit 首先處理 bridge 模式,我們從 mode 的定義可知,只有 bridge 模式下才可能在主裝置內部出現不同子裝置的直接通訊,所有這裡處理了這種特殊的情況,把多播報文和目的地為其他子裝置的單播報文直接發給子裝置。

  3. 對於其他報文,則會通過 dev_queue_xmit_accel 進行傳送,dev_queue_xmit_accel 會直接呼叫主裝置的 netdev_start_xmit 方法,從而實現報文真正的傳送。

``` static int macvlan_queue_xmit(struct sk_buff skb, struct net_device dev) { ...

/ 這裡首先是bridge模式下的邏輯,需要考慮不通子裝置間的通訊 / if (vlan->mode == MACVLAN_MODE_BRIDGE) { const struct ethhdr *eth = skb_eth_hdr(skb);

/* send to other bridge ports directly */
if (is_multicast_ether_addr(eth->h_dest)) {
  skb_reset_mac_header(skb);
  macvlan_broadcast(skb, port, dev, MACVLAN_MODE_BRIDGE);
  goto xmit_world;
}
/* 這裡對發往同一個主裝置的其他子裝置進行處理,直接進行轉發 */
dest = macvlan_hash_lookup(port, eth->h_dest);
if (dest && dest->mode == MACVLAN_MODE_BRIDGE) {
  /* send to lowerdev first for its network taps */
  dev_forward_skb(vlan->lowerdev, skb);

  return NET_XMIT_SUCCESS;
}

} xmit_world: skb->dev = vlan->lowerdev; / 這裡已經將報文的裝置設定為主裝置,然後通過主裝置進行傳送 / return dev_queue_xmit_accel(skb, netdev_get_sb_channel(dev) ? dev : NULL); } ```

IPVlan 裝置

IPVlan 子裝置相比於 MACVlan 和 VLAN 子裝置來說,模型就更加複雜了,不同於 MACVlan,IPVlan 將與子裝置間互通行為通過 flag 來定義,同時又提供了三種 mode,定義如下:

``` / 最初只有l2和l3,後面linux有了l3mdev,於是就出現了l3s,他們主要的區別還是在rx / enum ipvlan_mode { IPVLAN_MODE_L2 = 0, IPVLAN_MODE_L3, IPVLAN_MODE_L3S, IPVLAN_MODE_MAX };

/ 這裡其實還有個bridge,因為預設就是bridge,所有省略了,他們的語義和macvlan一樣 /

define IPVLAN_F_PRIVATE 0x01

define IPVLAN_F_VEPA 0x02

```

  • 裝置建立

有了之前兩種子裝置的分析,在 IPVlan 的分析上,我們也可以按照這個思路繼續進行分析,IPVlan 裝置的 netlink 訊息處理結構體是 ipvlan_link_ops,而建立裝置的入口方法是 ipvlan_link_new,建立 IPVlan 子裝置的流程如下:

8.png

  1. 進入 ipvlan_link_new,進行合法性判斷,與 MACVlan 類似,如果以一個 IPVlan 裝置作為主裝置進行新增,就會自動將 IPVlan 裝置的主裝置作為新裝置的主裝置。

  2. 通過 eth_hw_addr_set 設定 IPVlan 裝置的 mac 地址為主裝置的 mac 地址,這是 IPVlan 與 MACVlan 最明顯的特徵區分。

  3. 進入統一網絡卡註冊的 register_netdevice 流程,在這個流程裡,如果當前沒有 IPVlan 子裝置存在,則會和 MACVlan 一樣,進入到 ipvlan_init 的初始化過程,它會在主裝置上建立 ipvl_port,並且用 IPVlan 的 rx_handler 去取代主裝置原有的 rx_handler,同時也會啟動一個專門的核心 worker 去處理多播報文,也就是說,對於 IPVlan,所有的多播報文其實都是統一處理的。

  4. 接下來繼續處理當前這個新增的子裝置,通過 ipvlan_set_port_mode 將當前子裝置儲存到主裝置的資訊中,同時針對 l3s 的子裝置,會將它的 l3mdev 處理方法註冊到 nf_hook 中,沒錯,這是和上面裝置最大的區別,l3s 的主裝置和子裝置交換資料包實際上是在網路層完成的。

對於 IPVlan 網路裝置,我們擷取 ipvlan_port_create 一部分程式碼進行分析:

``` static int ipvlan_port_create(struct net_device dev) { / 從這裡可以看到,port是主裝置對子裝置管理的核心 / struct ipvl_port port; int err, idx;

/* 子裝置的各種屬性,都在port中體現,也可以看到預設的mode是l3 */

write_pnet(&port->pnet, dev_net(dev)); port->dev = dev; port->mode = IPVLAN_MODE_L3;

/* 這裡可以看到,對於ipvlan,多播的報文都是單獨處理的 */

skb_queue_head_init(&port->backlog); INIT_WORK(&port->wq, ipvlan_process_multicast);

/* 這裡就是常規操作了,其實他是靠著這裡來讓主裝置的收包可以順利配合ipvlan的動作 */

err = netdev_rx_handler_register(dev, ipvlan_handle_frame, port); if (err) goto err; } ```

  • 接收報文

IPVlan 子裝置的三種 mode 分別有不同的收包處理流程,在核心的流程如下:

9.png

  1. 與 MACVlan 類似,首先會經過__netif_receive_skb_core 進入到建立時註冊的 ipvlan_handle_frame 的處理流程,此時資料包依然是主裝置所擁有。

  2. 對於 mode l2 模式的報文處理,只處理多播的報文,將報文放進前面建立子裝置時初始化的多播處理的佇列;對於單播報文,會直接交給 ipvlan_handle_mode_l3 進行處理!

  3. 對於 mode l3 或者單播的 mode l2 報文,進入 ipvlan_handle_mode_l3 處理流程,首先通過 ipvlan_get_L3_hdr 獲取到網路層的頭資訊,然後根據 ip 地址去查詢到對應的子裝置,最後呼叫 ipvlan_rcv_frame,將報文的 dev 設定為 IPVlan 子裝置並返回 RX_HANDLER_ANOTHER,進行下一次收包。

  4. 對於 mode l3s,在 ipvlan_handle_frame 中會直接返回 RX_HANDLER_PASS,也就是說,mode l3s 的報文會在主裝置就進入到網路層的處理階段,對於 mode l3s 來說,預先註冊的 nf_hook 會在 NF_INET_LOCAL_IN 時觸發,執行 ipvlan_l3_rcv 操作,通過 addr 找到子裝置,更換報文的網路層目的地址,然後直接進入 ip_local_deliver 進行網路層餘下的操作。

  5. 傳送報文

IPVlan 的報文傳送,儘管在實現上相對複雜,但是究其根本,還是各個子裝置在想辦法用主裝置來做到傳送報文的工作,IPVlan 子裝置進行資料包傳送時,首先進入 ipvlan_start_xmit,其核心的傳送操作在 ipvlan_queue_xmit,核心程式碼流程如下:

10.png

  1. ipvlan_queue_xmit 根據子裝置的模式選擇不同的傳送方法,mode l2 通過 ipvlan_xmit_mode_l2 傳送,mode l3 和 mode l3s 進行 ipvlan_xmit_mode_l3 傳送。

  2. 對於 ipvlan_xmit_mode_l2,首先判斷是否是本地地址或者 VEPA 模式,如果不是 VEPA 模式的本地報文,則首先通過 ipvlan_addr_lookup 查到到是否是相同主裝置下的 IPVlan 子裝置,如果是,則通過 ipvlan_rcv_frame 讓其他子裝置進行收包處理;如果不是,則通過 dev_forward_skb 讓主裝置進行處理。

  3. 接下來 ipvlan_xmit_mode_l2 會對多播報文進行處理,在處理之前,通過 ipvlan_skb_crossing_ns 清理掉資料包的 netns 相關的資訊,包括 priority 等,最後將資料包放到 ipvlan_multicast_enqueue,觸發上述的多播處理流程。

  4. 對於非本地的資料包,通過主裝置的 dev_queue_xmit 進行傳送。

  5. ipvlan_xmit_mode_l3 的處理首先也是對 VEPA 進行判斷,對與非 VEPA 模式的資料包,通過ipvlan_addr_lookup 查詢是否是其他子裝置,如果是則呼叫 ipvlan_rcv_frame 觸發其他裝置進行收包處理。

  6. 對於非 VEPA 模式的資料包,首先進行 ipvlan_skb_crossing_ns 的處理,然後進行 ipvlan_process_outbound的操作,此時根據資料包的網路層協議,選擇 ipvlan_process_v4_outbound 或者 ipvlan_process_v6_outbound 進行處理。

  7. 以 ipvlan_process_v6_outbound 為例,首先會通過 ip_route_output_flow 進行路由的查詢,然後直接通過網路層的 ip_local_out,在主裝置的網路層繼續進行發包操作。

解決問題

經歷上面的分析和一番體會思考,我想至少第一個問題已經可以很輕易的回答出來了:

VLAN 與 MACVlan/IPVlan 的關係

VLAN 和 IPVlan,MACVlan 有什麼關係呢?為什麼名字裡都有 VLAN?

既然 MACVlan 和 IPVlan 選擇叫這個名字,那說明在某些方面還是有相似之處的。我們整體分析下來發現,VLAN 子裝置和 MACVlan,IPVlan 的核心邏輯很相似:

  1. 主裝置負責物理上的收發包。

  2. 主裝置將子裝置管理為多個 port,然後根據一定的規則找到 port,比如 VLAN 資訊,mac 地址以及 ip 地址(macvlan_hash_lookup,vlan_find_dev,ipvlan_addr_lookup)。

  3. 主裝置收包後,都需要經過在__netif_receive_skb_core 中走一段“回頭路”.

  4. 子裝置發包最終都是直接通過修改報文的 dev,然後讓主裝置去操作。

所以不難推論,MACVlan/IPVlan 的內在邏輯其實很大程度上參考了 Linux 的 VLAN 的實現。Linux 最早加入 MACVlan 是在 2007 年 6 月 18 日釋出的 2.6.63 版本 [ 3] ,對他的描述是:

The new "MACVlan" driver allows the system administrator to create virtual interfaces mapped to and from specific MAC addresses.

而到了 2014 年 12 月 7 日 釋出的 3.19 版本 [ 4] 中,第一次引入了 IPVlan,他的描述是:

The new "IPVlan" driver enable the creation of virtual network devices for container interconnection. It is designed to work well with network namespaces. IPVlan is much like the existing MACVlan driver, but it does its multiplexing at a higher level in the stack.

至於 VLAN,出現的遠比 Linux 2.4 版本還要早,很多裝置的第一版驅動就已經支援了 VLAN,不過,Linux 對於 VLAN 的 hwaccel 實現,是 2004 年的 2.6.10 [ 5] ,當時更新的一大批特性中,出現了這一條:

I was poking about in the National Semi 83820 driver, and I happened to notice that the chip supports VLAN tag add/strip assist in hardware, but the driver wasn't making use of it. This patch adds in the driver support to use the VLAN tag add/remove hardware, and enables the drivers use of the kernel VLAN hwaccel interface.

也就是說,當 Linux 開始把 VLAN 當作一個 interface 處理後,才有了後面的 MACVlan 和 IPVlan 兩個 virtual interface,Linux 為了實現對於 VLAN 資料包的處理流程的加速,把不同的 VLAN 虛擬成了裝置,而後期 MACVlan 和 IPVlan 在這種思路之下,讓虛擬裝置有了更大的用武之地。

這樣看來,它們的關係更像是一種致敬。

關於 VEPA/passthrough/bridge/private

IPVlan 和 MACVlan 為什麼會有各種模式和 flag,比如 VEPA,private,passthrough 等等?它們的區別在哪裡?

其實在核心的分析中,我們已經大致瞭解了這幾種模式的表現,假如主裝置是一個釘釘群,所有的群友都可以向外發訊息,那麼其實幾種模式就非常直觀:

  1. private 模式,群友們相互之間都是禁言的,既不能在群內,也不能在群外。
  2. bridge 模式,群友們可以在群內愉快發言。
  3. VEPA 模式,群友們在群內禁言了,但是你們在群外直接私聊,相當於年會搶紅包時期的集體禁言。
  4. passthrough 模式,這時候你就是群主了,除了你沒人能發言。 

那麼為什麼會有這幾種模式呢?我們從核心的表現來看,無論是 port,還是 bridge,其實都是網路的概念,也就是說,從一開始,Linux 就在努力將自己表現成一個合格的網路裝置,對於主裝置,Linux 努力將它做成一個交換機,對於子裝置,那就是一個個網線背後的裝置,這樣看起來就很合理了。

實際上正是這樣,無論是 VEPA 還是 private,它們最初都是網路概念。其實不止是 Linux,我們見到很多致力於把自己偽裝成物理網路的專案,都沿襲了這些行為模式,比如 OpenvSwitch [ 6]

MACVlan 與 IPVlan 的應用

IPVlan 和 MACVlan 的優勢在哪裡?你應該在什麼情況下接觸到,使用到它們呢?

其實到這裡,才開始說到本篇文章的初衷。我們從第二個問題發現,IPVlan 和 MACVlan 都是在做一件事:虛擬網路。我們為什麼要虛擬網路呢?這個問題有很多答案,但是和雲端計算的價值一樣,虛擬網路作為雲端計算的一項基礎技術,它們最終都是為了資源利用效率的提升。

MACVlan 和 IPVlan 就是服務了這個最終目的,土豪們一臺物理機跑一個 helloworld 的時代依然過去,從虛擬化到容器化,時代對網路密度提出了越來越高的要求,伴隨著容器技術的誕生,首先是 veth 走上舞臺,但是密度夠了,還要效能的高效,MACVlan 和 IPVlan 通過子裝置提升密度並保證高效的方式應運而生(當然還有我們的 ENI-Trunking)。

說到這裡,就要為大家推薦一下阿里雲容器服務 ACK 給大家帶來的高效能、高密度的網路新方案——IPVlan 方案 [ 7]

ACK 基於 Terway 外掛,實現了基於 IPVlan 的 K8s 網路解決方案。Terway 網路外掛是 ACK 自研的網路外掛,將原生的彈性網絡卡分配給 Pod 實現 Pod 網路,支援基於 Kubernetes 標準的網路策略(Network Policy)來定義容器間的訪問策略,併兼容 Calico 的網路策略。

在 Terway 網路外掛中,每個 Pod 都擁有自己網路棧和IP地址。同一臺 ECS 內的 Pod 之間通訊,直接通過機器內部的轉發,跨 ECS 的 Pod 通訊、報文通過 VPC 的彈性網絡卡直接轉發。由於不需要使用 VxLAN 等的隧道技術封裝報文,因此 Terway 模式網路具有較高的通訊效能。Terway 的網路模式如下圖所示:

11.png

客戶在使用 ACK 建立叢集時,如果選擇 Terway 網路外掛,可以配置其使用 Terway IPvlan 模式。Terway IPvlan 模式採用 IPvlan 虛擬化和 eBPF 核心技術實現高效能的 Pod 和 Service 網路。

不同於預設的 Terway 的網路模式,IPvlan 模式主要在 Pod 網路、Service、網路策略(NetworkPolicy)做了效能的優化:

  • Pod 的網路直接通過 ENI 網絡卡的 IPvlan L2 的子介面實現,大大簡化了網路在宿主機上的轉發流程,讓 Pod 的網路效能幾乎與宿主機的效能無異,延遲相對傳統模式降低 30%。 

  • Service 的網路採用 eBPF 替換原有的 kube-proxy 模式,不需要經過宿主機上的 iptables 或者 IPVS 轉發,在大規模叢集中效能幾乎無降低,擴充套件性更優。在大量新建連線和埠複用場景請求延遲比 IPVS 和 iptables 模式的大幅降低。 

  • Pod 的網路策略(NetworkPolicy)也採用 eBPF 替換掉原有的 iptables 的實現,不需要在宿主機上產生大量的 iptables 規則,讓網路策略對網路效能的影響降到最低。 

所以,利用 IPVlan 為每個業務 pod 分配 IPVlan 網絡卡,既保證了網路的密度,也使傳統網路的 Veth 方案實現巨大的效能提升(詳見參考連結 7)。同時,Terway IPvlan 模式提供了高效能的 Service 解決方案,基於 eBPF 技術,我們規避了詬病已久的 Conntrack 效能問題。

相信無論是什麼場景的業務,ACK with IPVlan 都是一個更為出色的選擇。

最後感謝你能閱讀到這裡,在這個問題背後,其實隱藏了一個問題,你知道為什麼我們選擇 IPVlan,而沒有選擇 MACVlan 麼?如果你對虛擬網路技術有了解,那麼結合上述的內容,你應該很快就有答案了,也歡迎你在評論區留言。

參考連結:

[1] 《關於 IEEE 802.1Q》**

https://zh.wikipedia.org/wiki/IEEE_802.1Q

[2] 《Docker Engine release notes》

https://docs.docker.com/engine/release-notes/prior-releases/

[3] 《Merged for MACVlan 2.6.23》

https://lwn.net/Articles/241915/

[4] 《MACVlan 3.19 Merge window part 2》

https://lwn.net/Articles/626150/

[5] 《VLan 2.6.10-rc2 long-format changelog》

https://lwn.net/Articles/111033/

[6] 《[ovs-dev] VEPA support in OVS》

https://mail.openvswitch.org/pipermail/ovs-dev/2013-December/277994.html

[7] 《阿里雲 Kubernetes 叢集使用 IPVlan 加速 Pod 網路》

https://developer.aliyun.com/article/743689

點選此處,瞭解基於阿里雲  ACK  Terway 的 IPvlan。