深入理解Linux網絡隨筆(七):容器網絡虛擬化
微服務架構中服務被拆分成多個獨立的容器,docker網絡虛擬化的核心技術為:Veth設備對、Network Namespace、Bridg。
Veth設備對
veth
設備是一種 成對 出現的虛擬網絡接口,作用是 在 Linux 網絡命名空間或不同網絡棧之間建立一個虛擬的點對點連接,實現數據通信。例如實現容器與宿主機間的通信、不同neths間傳遞流量。
如圖所示:
虛擬網絡設備并不會直接連接物理網絡設備,而是一端連接到協議棧,另一端連接到另一個 veth 設備。從一對 veth 設備中發出的數據包會直接傳送到另一個 veth 設備。每個 veth 設備都可以配置 IP 地址,并作為 路由的一個接口,可以進行IP層通信。
特點:
(1)成對出現:創建時總是 兩個設備*成對出現,例如 veth0
和 veth1
,它們之間類似于一條網絡隧道
(2)工作方式:一端發送的流量會從另一端收到,就像網線直連一樣
(3)不處理 L2/L3 轉發:veth
設備 不會執行交換、路由*等功能,只是簡單地在兩端傳輸數據包
底層源碼分析
veth設備的初始化通過函數veth_init進行。
static __init int veth_init(void)
{//注冊veth_link_ops veth設備的操作方法return rtnl_link_register(&veth_link_ops);
}
veth_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,//檢查netlink請求參數的合法性.newlink = veth_newlink,//處理新建的veth設備回調函數.dellink = veth_dellink,//處理刪除的veth設備回調函數.policy = veth_policy,//netlink配置參數解析策略.maxtype = VETH_INFO_MAX,// netlink 解析時允許的最大屬性編號.get_link_net = veth_get_link_net,// 獲取 veth 設備所在的網絡命名空間.get_num_tx_queues = veth_get_num_queues,// 獲取設備的 TX 隊列數.get_num_rx_queues = veth_get_num_queues,// 獲取設備的 RX 隊列數
};
veth設備創建調用veth_newlink
函數。
static int veth_newlink(struct net *src_net, struct net_device *dev,struct nlattr *tb[], struct nlattr *data[],struct netlink_ext_ack *extack)
{int err;struct net_device *peer;struct veth_priv *priv;char ifname[IFNAMSIZ];struct nlattr *peer_tb[IFLA_MAX + 1], **tbp;unsigned char name_assign_type;struct ifinfomsg *ifmp;struct net *net;.....//創建peer設備peer = rtnl_create_link(net, ifname, name_assign_type,&veth_link_ops, tbp, extack);if (IS_ERR(peer)) {put_net(net);return PTR_ERR(peer);}....//注冊peer設備err = register_netdevice(peer);...//獲取dev的私有數據privpriv = netdev_priv(dev);//將dev的指針賦值給peer的私有數據peer->priv,建立連接,通過dev訪問peer設備rcu_assign_pointer(priv->peer, peer);//初始化dev的TX、RX隊列err = veth_init_queues(dev, tb);if (err)goto err_queues;//獲取 peer 設備的 priv 結構體priv = netdev_priv(peer);//讓 peer->priv->peer 指向 dev,建立 veth 設備對的雙向連接rcu_assign_pointer(priv->peer, dev);//初始化peer設備的隊列err = veth_init_queues(peer, tb);if (err)goto err_queues;.....
}
啟動veth設備,通過veth_netdev_ops
操作表找到發送過程中的回調函數veth_xmit。
static void veth_setup(struct net_device *dev)
{ether_setup(dev);dev->priv_flags &= ~IFF_TX_SKB_SHARING;dev->priv_flags |= IFF_LIVE_ADDR_CHANGE;dev->priv_flags |= IFF_NO_QUEUE;dev->priv_flags |= IFF_PHONY_HEADROOM;//veth操作列表dev->netdev_ops = &veth_netdev_ops;dev->ethtool_ops = &veth_ethtool_ops;dev->features |= NETIF_F_LLTX;dev->features |= VETH_FEATURES;dev->vlan_features = dev->features &~(NETIF_F_HW_VLAN_CTAG_TX |NETIF_F_HW_VLAN_STAG_TX |NETIF_F_HW_VLAN_CTAG_RX |NETIF_F_HW_VLAN_STAG_RX);dev->needs_free_netdev = true;dev->priv_destructor = veth_dev_free;dev->pcpu_stat_type = NETDEV_PCPU_STAT_TSTATS;dev->max_mtu = ETH_MAX_MTU;dev->hw_features = VETH_FEATURES;dev->hw_enc_features = VETH_FEATURES;dev->mpls_features = NETIF_F_HW_CSUM | NETIF_F_GSO_SOFTWARE;netif_set_tso_max_size(dev, GSO_MAX_SIZE);
}
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,//veth發送函數.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,
};
通信過程
數據包發送過程中到達網絡設備層會進入dev_hard_start_xmit
函數,遍歷鏈表上的所有skb包調用xmit_one
發送數據包。
//網絡設備數據包發送路徑(TX Path) 的關鍵部分,負責調用底層驅動的 ndo_start_xmit() 發送數據包
static int xmit_one(struct sk_buff *skb, struct net_device *dev,struct netdev_queue *txq, bool more)
{unsigned int len;int rc;//監測是否有協議棧上層監聽if (dev_nit_active(dev))//AF_PACKET 套接字正在監聽,發送數據包副本給監聽進程dev_queue_xmit_nit(skb, dev);
//記錄數據包長度len = skb->len;//觸發tracepoint機制,記錄數據包發送開始trace_net_dev_start_xmit(skb, dev);//調用底層驅動的ndo_start_xmit()方法發送數據包rc = netdev_start_xmit(skb, dev, txq, more);trace_net_dev_xmit(skb, rc, dev, len);return rc;
}
獲取驅動設備回調函數集合ops,結構體net_device_ops
,調用__netdev_start_xmit
發送數據包。
static inline netdev_tx_t netdev_start_xmit(struct sk_buff *skb, struct net_device *dev,struct netdev_queue *txq, bool more)
{const struct net_device_ops *ops = dev->netdev_ops;netdev_tx_t rc;//發送數據包rc = __netdev_start_xmit(ops, skb, dev, more);if (rc == NETDEV_TX_OK)txq_trans_update(txq);return rc;
}
在這里會首先判斷當前cpu發送隊列是否還有數據待處理,然后調用驅動的ndo_start_xmit函數發送數據包,回調函數veth_xmit,lo是loopback_xmit。也就是在veth啟動的時候注冊的回調函數。
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops,struct sk_buff *skb, struct net_device *dev,bool more)
{__this_cpu_write(softnet_data.xmit.more, more);return ops->ndo_start_xmit(skb, dev);
}
在veth_xmit
核心是獲取veth設備數據,將數據發送到對端設備。
static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{struct veth_priv *rcv_priv, *priv = netdev_priv(dev);struct veth_rq *rq = NULL;int ret = NETDEV_TX_OK;struct net_device *rcv;int length = skb->len;bool use_napi = false;int rxq;rcu_read_lock();//獲取veth設備rcv = rcu_dereference(priv->peer);...//獲取rcv設備私有數據rcv_priv = netdev_priv(rcv);//獲取skb隊列索引rxq = skb_get_queue_mapping(skb);if (rxq < rcv->real_num_rx_queues) {rq = &rcv_priv->rq[rxq];//rq綁定了napi,且數據包適合GRO,開啟NAPI機制輪詢數據包use_napi = rcu_access_pointer(rq->napi) &&veth_skb_is_eligible_for_gro(dev, rcv, skb);}skb_tx_timestamp(skb);//嘗試將skb轉發到對端veth設備if (likely(veth_forward_skb(rcv, skb, rq, use_napi) == NET_RX_SUCCESS)) {//未使用NAPI機制,更新統計信息if (!use_napi)dev_sw_netstats_tx_add(dev, 1, length);} else {
.....
}
veth_forward_skb
會根據數據包選擇不同路徑,數據包轉發到對端設備dev_forward_skb
,對端開啟XDP則調用veth_xdp_rx
,普通數據包調用netif_rx
。
static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb,struct veth_rq *rq, bool xdp)
{return __dev_forward_skb(dev, skb) ?: xdp ?veth_xdp_rx(rq, skb) :__netif_rx(skb);
}
函數調用關系:__dev_forward_skb-->__dev_forward_skb2-->____dev_forward_skb
。
//處理dev設備的轉發數據包
static int __dev_forward_skb2(struct net_device *dev, struct sk_buff *skb,bool check_mtu)
{//實際數據包轉發處理int ret = ____dev_forward_skb(dev, skb, check_mtu);if (likely(!ret)) {//將skb所屬設備設置成剛才取到的veth對端設備rcvskb->protocol = eth_type_trans(skb, dev);//修正skb校驗和skb_postpull_rcsum(skb, eth_hdr(skb), ETH_HLEN);}return ret;
}
在eth_type_trans
設置完成會繼續執行__netif_rx
路徑,函數調用邏輯netif_rx_internal-->enqueue_to_backlog
,在這里獲取每個CPU核心對應的softnet_data數據結構,將skb添加到等待隊列input_pkt_queue
,在函數__test_and_set_bit
檢查sd->backlog.state
是否已包含 NAPI_STATE_SCHED
,NAPI_STATE_SCHED
是NAPI輪詢處理的一個 狀態標志位,防止同一個 NAPI
任務被重復調度,未設置調用napi_schedule_rps
觸發NAPI調度,觸發軟中斷 NET_RX_SOFTIRQ
。
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,unsigned int *qtail)
{enum skb_drop_reason reason;struct softnet_data *sd;unsigned long flags;unsigned int qlen;//丟包原因:未指定reason = SKB_DROP_REASON_NOT_SPECIFIED;sd = &per_cpu(softnet_data, cpu);if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state))napi_schedule_rps(sd);
.....
}
在這里根據是否開啟RPS機制走不同的路徑,RPS 是 Linux 內核的一種 多核負載均衡機制,將收到的數據包分配到多個 CPU 進行處理,避免所有網絡流量只由單個 CPU 處理,減少 CPU 瓶頸,在這里通過rps_ipi_list將數據包從一個CPU轉發到另外一個CPU上,提高多核環境下的負載均衡,減少CPU之間的競爭。如果沒有開啟RPS機制,數據包會在當前CPU軟中斷上下文中處理NAP任務。
static int napi_schedule_rps(struct softnet_data *sd)
{struct softnet_data *mysd = this_cpu_ptr(&softnet_data);
// RPS將接收的數據包調度到 不同的 CPU 進行處理
#ifdef CONFIG_RPS
//sd不等于mysd,說明要在另一個 CPU 上執行 NAPI 任務,而不是本 CPU 處理if (sd != mysd) {//rps_ipi_list 負責存儲要在其他 CPU 處理的 softnet_data 隊列,并通過 NET_RX_SOFTIRQ 觸發軟中斷來完成調度。sd->rps_ipi_next = mysd->rps_ipi_list;mysd->rps_ipi_list = sd;//出發軟中斷,處理softnet_data隊列的NAPI任務__raise_softirq_irqoff(NET_RX_SOFTIRQ);return 1;}
//本地CPU處理
#endif /* CONFIG_RPS */
//直接調度 mysd->backlog 設備的 NAPI 任務,并安排 net_rx_action() 來執行數據包處理。__napi_schedule_irqoff(&mysd->backlog);return 0;
}
實踐操作
Linux 中創建 veth 設備對,設備名veth,指定的虛擬網卡類型為veth,創建的另一端設備名為veth1。
ip link add veth0 type veth peer name veth1
使用ip link show
可以看到veth0
和 veth1
設備已創建,但還未啟用。
veth設備需要配置IP地址才能進行通信,為veth0
和 veth1
設備配置ip。
sudo ip addr add 192.168.1.1/24 dev veth0
sudo ip addr add 192.168.1.2/24 dev veth1
啟動設備
sudo ip link set veth1 up
sudo ip link set veth0 up
使用ip link show
可以看到veth0
和 veth1
設備狀態已變成開啟
為了使veth設備之間能夠順利通信,需要關閉反向路徑過濾(rp_filter)并設置允許接收本機數據包。root角色下修改系統配置如下:
ping測試veth0
和 veth1
設備間通信