深入理解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的私有数据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_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
设备间通信