目录

深入理解Linux网络随笔七容器网络虚拟化-Veth设备对

深入理解Linux网络随笔(七):容器网络虚拟化–Veth设备对

深入理解Linux网络随笔(七):容器网络虚拟化

微服务架构中服务被拆分成多个独立的容器,docker网络虚拟化的核心技术为:Veth设备对、Network Namespace、Bridg。

Veth设备对

veth设备是一种 成对 出现的虚拟网络接口,作用是 在 Linux 网络命名空间或不同网络栈之间建立一个虚拟的点对点连接 ,实现数据通信。例如实现容器与宿主机间的通信、不同neths间传递流量。 如图所示: 虚拟网络设备并不会直接连接物理网络设备,而是一端连接到协议栈,另一端连接到另一个 veth 设备。从一对 veth 设备中发出的数据包会直接传送到另一个 veth 设备。每个 veth 设备都可以配置 IP 地址,并作为 路由的一个接口,可以进行IP层通信。 https://i-blog.csdnimg.cn/direct/5edc4a621c804c3fbd6d85905f0831d4.png 特点: (1)成对出现:创建时总是 两个设备成对出现,例如 veth0veth1,它们之间类似于一条网络隧道 (2)工作方式:一端发送的流量会从另一端收到,就像网线直连一样 (3)不处理 L2/L3 转发:veth 设备 不会执行交换、路由等功能,只是简单地在两端传输数据包

底层源码分析

https://i-blog.csdnimg.cn/direct/fdddec9a8ad74401b3890caf6f660202.png 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的私有数据priv priv = 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对端设备rcv skb->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_SCHEDNAPI_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可以看到veth0veth1 设备已创建,但还未启用。 https://i-blog.csdnimg.cn/direct/9004d6cec9e940f2846e8da955167e10.png veth设备需要配置IP地址才能进行通信,为veth0veth1 设备配置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可以看到veth0veth1 设备状态已变成开启 https://i-blog.csdnimg.cn/direct/b347720bfa954091b2c8f3645f535da1.png 为了使veth设备之间能够顺利通信,需要关闭反向路径过滤(rp_filter)并设置允许接收本机数据包。root角色下修改系统配置如下: https://i-blog.csdnimg.cn/direct/cb97242608124f7b897c0accc3ab183f.png ping测试veth0veth1 设备间通信 https://i-blog.csdnimg.cn/direct/3dd11655c6d342bc8726fb026d0bac08.png