You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

15 KiB

17容器网络配置2容器网络延时要比宿主机上的高吗?

你好,我是程远。

在上一讲里我们学习了在容器中的网络接口配置重点讲解的是veth的接口配置方式这也是绝大部分容器用的缺省的网络配置方式。

不过呢从veth的这种网络接口配置上看一个数据包要从容器里发送到宿主机外需要先从容器里的eth0 (veth_container) 把包发送到宿主机上veth_host然后再在宿主机上通过nat或者路由的方式经过宿主机上的eth0向外发送。

这种容器向外发送数据包的路径相比宿主机上直接向外发送数据包的路径很明显要多了一次接口层的发送和接收。尽管veth是虚拟网络接口在软件上还是会增加一些开销。

如果我们的应用程序对网络性能有很高的要求特别是之前运行在物理机器上现在迁移到容器上的如果网络配置采用veth方式就会出现网络延时增加的现象。

那今天我们就来聊一聊,容器网络接口对于容器中应用程序网络延时有怎样的影响,还有这个问题应该怎么解决。

问题重现

对于这种veth接口配置导致网络延时增加的现象我们可以通过运行netperfNetperf是一个衡量网络性能的工具它可以提供单向吞吐量和端到端延迟的测试来模拟一下。

这里我们需要两台虚拟机或者物理机,这两台机器需要同处于一个二层的网络中。

具体的配置示意图如下:

首先我们需要在第一台机器上启动一个veth接口的容器容器的启动和宿主机上的配置你可以参考一下这里的脚本。在第二台机器上我们只要启动一个netserver就可以了。

然后呢我们分别在容器里和宿主机上运行与netserver交互的netperf再比较一下它们延时的差异。

我们可以运行netperf的TCP_RR测试用例TCP_RR是netperf里专门用来测试网络延时的缺省每次运行10秒钟。运行以后我们还要计算平均每秒钟TCP request/response的次数这个次数越高就说明延时越小。

接下来我们先在第一台机器的宿主机上直接运行netperf的TCP_RR测试用例3轮得到的值分别是2504.922410.14和2422.81计算一下可以得到三轮Transactions平均值是2446/s。

# ./netperf -H 192.168.0.194 -t TCP_RR
MIGRATED TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.0.194 () port 0 AF_INET : first burst 0
Local /Remote
Socket Size   Request  Resp.   Elapsed  Trans.
Send   Recv   Size     Size    Time     Rate
bytes  Bytes  bytes    bytes   secs.    per sec
 
16384  131072 1        1       10.00    2504.92
16384  131072
# ./netperf -H 192.168.0.194 -t TCP_RR
MIGRATED TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.0.194 () port 0 AF_INET : first burst 0
Local /Remote
Socket Size   Request  Resp.   Elapsed  Trans.
Send   Recv   Size     Size    Time     Rate
bytes  Bytes  bytes    bytes   secs.    per sec
 
16384  131072 1        1       10.00    2410.14
16384  131072
 
# ./netperf -H 192.168.0.194 -t TCP_RR
MIGRATED TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.0.194 () port 0 AF_INET : first burst 0
Local /Remote
Socket Size   Request  Resp.   Elapsed  Trans.
Send   Recv   Size     Size    Time     Rate
bytes  Bytes  bytes    bytes   secs.    per sec
 
16384  131072 1        1       10.00    2422.81
16384  131072

同样我们再在容器中运行一下netperf的TCP_RR也一样运行三轮计算一下这三次的平均值得到的值是2141。

那么我们拿这次容器环境中的平均值和宿主机上得到的值2446做比较会发现Transactions下降了大概12.5%也就是网络的延时超过了10%。

[root@4150e2a842b5 /]# ./netperf -H 192.168.0.194 -t TCP_RR
MIGRATED TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.0.194 () port 0 AF_INET : first burst 0
Local /Remote
Socket Size   Request  Resp.   Elapsed  Trans.
Send   Recv   Size     Size    Time     Rate
bytes  Bytes  bytes    bytes   secs.    per sec
 
16384  131072 1        1       10.00    2104.68
16384  131072
 
[root@4150e2a842b5 /]# ./netperf -H 192.168.0.194 -t TCP_RR
MIGRATED TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.0.194 () port 0 AF_INET : first burst 0
Local /Remote
Socket Size   Request  Resp.   Elapsed  Trans.
Send   Recv   Size     Size    Time     Rate
bytes  Bytes  bytes    bytes   secs.    per sec
 
16384  131072 1        1       10.00    2146.34
16384  131072
 
[root@4150e2a842b5 /]# ./netperf -H 192.168.0.194 -t TCP_RR
MIGRATED TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.0.194 () port 0 AF_INET : first burst 0
Local /Remote
Socket Size   Request  Resp.   Elapsed  Trans.
Send   Recv   Size     Size    Time     Rate
bytes  Bytes  bytes    bytes   secs.    per sec
 
16384  131072 1        1       10.00    2173.79
16384  131072

分析问题

刚才我们已经得到了测试的数值我们发现veth方式的确带来了很高的网络延时。那现在我们先来分析一下为什么veth会带来这么大的网络延时然后再看看有什么方法可以降低容器里的网络延时。

我们先回顾一下容器里veth接口的配置还是拿我们上一讲里容器veth的图作为例子。

上一讲中我提到过veth的虚拟网络接口一般都是成对出现就像上面图里的veth_container和veth_host一样。

在每次网络传输的过程中数据包都需要通过veth_container这个接口向外发送而且必须保证veth_host先接收到这个数据包。

虽然veth是一个虚拟的网络接口但是在接收数据包的操作上这个虚拟接口和真实的网路接口并没有太大的区别。这里除了没有硬件中断的处理其他操作都差不多特别是软中断softirq的处理部分其实就和真实的网络接口是一样的。

我们可以通过阅读Linux内核里的veth的驱动代码drivers/net/veth.c)确认一下。

veth发送数据的函数是veth_xmit()它里面的主要操作就是找到veth peer设备然后触发peer设备去接收数据包。

比如veth_container这个接口调用了veth_xmit()来发送数据包最后就是触发了它的peer设备veth_host去调用netif_rx()来接收数据包。主要的代码我列在下面了:

static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
…
       /* 拿到veth peer设备的net_device */
       rcv = rcu_dereference(priv->peer);
…
       /* 将数据送到veth peer设备 */ 
       if (likely(veth_forward_skb(rcv, skb, rq, rcv_xdp) == NET_RX_SUCCESS)) {}
 
static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb,
                            struct veth_rq *rq, bool xdp)
{
        /* 这里最后调用了 netif_rx() */
        return __dev_forward_skb(dev, skb) ?: xdp ?
                veth_xdp_rx(rq, skb) :
                netif_rx(skb);
}

而netif_rx()是一个网络设备驱动里面标准的接收数据包的函数netif_rx()里面会为这个数据包raise一个softirq。

   __raise_softirq_irqoff(NET_RX_SOFTIRQ);

其实softirq这个概念我们之前在CPU的模块中也提到过。在处理网络数据的时候一些运行时间较长而且不能在硬中断中处理的工作就会通过softirq来处理。

一般在硬件中断处理结束之后网络softirq的函数才会再去执行没有完成的包的处理工作。即使这里softirq的执行速度很快还是会带来额外的开销。

所以根据veth这个虚拟网络设备的实现方式我们可以看到它必然会带来额外的开销这样就会增加数据包的网络延时。

解决问题

那么我们有什么方法可以减少容器的网络延时呢你可能会想到我们可不可以不使用veth这个方式配置网络接口而是换成别的方式呢

的确是这样其实除了veth之外容器还可以选择其他的网络配置方式。在Docker的文档中提到了macvlan的配置方式和macvlan很类似的方式还有ipvlan。

那我们先来简单看一下macvlan和ipvlan的异同点。

我们先来看这两个方式的相同之处无论是macvlan还是ipvlan它们都是在一个物理的网络接口上再配置几个虚拟的网络接口。在这些虚拟的网络接口上都可以配置独立的IP并且这些IP可以属于不同的Namespace。

然后我再说说它们的不同点。**对于macvlan每个虚拟网络接口都有自己独立的mac地址而ipvlan的虚拟网络接口是和物理网络接口共享同一个mac地址。**而且它们都有自己的L2/L3的配置方式不过我们主要是拿macvlan/ipvlan来和veth做比较这里可以先忽略macvlan/ipvlan这些详细的特性。

我们就以ipvlan为例运行下面的这个脚本为容器手动配置上ipvlan的网络接口。

docker run --init --name lat-test-1 --network none -d registry/latency-test:v1 sleep 36000
 
pid1=$(docker inspect lat-test-1 | grep -i Pid | head -n 1 | awk '{print $2}' | awk -F "," '{print $1}')
echo $pid1
ln -s /proc/$pid1/ns/net /var/run/netns/$pid1
 
ip link add link eth0 ipvt1 type ipvlan mode l2
ip link set dev ipvt1 netns $pid1
 
ip netns exec $pid1 ip link set ipvt1 name eth0
ip netns exec $pid1 ip addr add 172.17.3.2/16 dev eth0
ip netns exec $pid1 ip link set eth0 up

在这个脚本里,我们先启动一个容器,这里我们用"—network none"的方式来启动,也就是在容器中没有配置任何的网络接口。

接着我们在宿主机eth0的接口上增加一个ipvlan虚拟网络接口ipvt1再把它加入到容器的Network Namespace里面重命名为容器内的eth0并且配置上IP。这样我们就配置好了第一个用ipvlan网络接口的容器。

我们可以用同样的方式配置第二个容器这样两个容器可以相互ping一下IP看看网络是否配置成功了。脚本你可以在这里得到。

两个容器配置好之后就像下面图中描述的一样了。从这张图里你很容易就能看出macvlan/ipvlan与veth网络配置有什么不一样。容器的虚拟网络接口直接连接在了宿主机的物理网络接口上了形成了一个网络二层的连接。

如果从容器里向宿主机外发送数据看上去通过的接口要比veth少了那么实际情况是不是这样呢我们先来看一下ipvlan接口发送数据的代码。

从下面的ipvlan接口的发送代码中我们可以看到如果是往宿主机外发送数据发送函数会直接找到ipvlan虚拟接口对应的物理网络接口。

比如在我们的例子中这个物理接口就是宿主机上的eth0然后直接调用dev_queue_xmit(),通过物理接口把数据直接发送出去。

static int ipvlan_xmit_mode_l2(struct sk_buff *skb, struct net_device *dev)
{if (!ipvlan_is_vepa(ipvlan->port) &&
            ether_addr_equal(eth->h_dest, eth->h_source)) {} else if (is_multicast_ether_addr(eth->h_dest)) {}
        /* 
         * 对于普通的对外发送数据上面的if 和 else if中的条件都不成立
         * 所以会执行到这一步拿到ipvlan对应的物理网路接口设备
         * 然后直接从这个设备发送数据。
         */ 
        skb->dev = ipvlan->phy_dev;
        return dev_queue_xmit(skb);
}

和veth接口相比我们用ipvlan发送对外数据就要简单得多因为这种方式没有内部额外的softirq处理开销。

现在我们还可以看一下在实际生产环境中一个应用程序跑在使用veth接口的容器中跟这个应用程序跑在使用ipvlan接口的容器中两者的网络延时差异是怎样的。

下面这张图是网络延时的监控图图里蓝色的线表示程序运行在veth容器中黄色线表示程序运行在ipvlan的容器里绿色的线代表程序直接运行在物理机上。

从这张延时Latency图里我们可以看到在veth容器里程序的网络延时要明显高一些而程序在ipvlan容器里的网络延时已经比较接近物理机上的网络延时了。

所以对于网络延时敏感的应用程序我们可以考虑使用ipvlan/macvlan的容器网络配置方式来替换缺省的veth网络配置。

重点小结

好了,今天的内容讲完了,我们来做个总结。今天我们主要讨论了容器网络接口对容器中应用程序网络延时的影响。

容器通常缺省使用veth虚拟网络接口不过veth接口会有比较大的网络延时。我们可以使用netperf这个工具来比较网络延时相比物理机上的网络延时使用veth接口容器的网络延时会增加超过10%。

我们通过对veth实现的代码做分析可以看到由于veth接口是成对工作在对外发送数据的时候peer veth接口都会raise softirq来完成一次收包操作这样就会带来数据包处理的额外开销。

如果要减小容器网络延时就可以给容器配置ipvlan/macvlan的网络接口来替代veth网络接口。Ipvlan/macvlan直接在物理网络接口上虚拟出接口在发送对外数据包的时候可以直接通过物理接口完成没有节点内部类似veth的那种softirq的开销。容器使用ipvlan/maclan的网络接口它的网络延时可以非常接近物理网络接口的延时。

对于延时敏感的应用程序我们可以考虑使用ipvlan/macvlan网络接口的容器。不过由于ipvlan/macvlan网络接口直接挂载在物理网络接口上对于需要使用iptables规则的容器比如Kubernetes里使用service的容器就不能工作了。这就需要你结合实际应用的需求做个判断再选择合适的方案。

思考题

在这节课的最后我提到“由于ipvlan/macvlan网络接口直接挂载在物理网络接口上对于需要使用iptables规则的容器比如Kubernetes里使用service的容器就不能工作了”请你思考一下这个判断背后的具体原因。

欢迎你在留言区写下你的思考和疑问。如果这篇文章让你有所收获,也欢迎分享给你的朋友,一起交流进步。