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.

245 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 18 | 容器网络配置3容器中的网络乱序包怎么这么高
你好,我是程远。这一讲,我们来聊一下容器中发包乱序的问题。
这个问题也同样来自于工作实践,我们的用户把他们的应用程序从物理机迁移到容器之后,从网络监控中发现,容器中数据包的重传的数量要比在物理机里高了不少。
在网络的前面几讲里我们已经知道了容器网络缺省的接口是vethveth接口都是成对使用的。容器通过veth接口向外发送数据首先需要从veth的一个接口发送给跟它成对的另一个接口。
那么这种接口会不会引起更多的网络重传呢?如果会引起重传,原因是什么,我们又要如何解决呢?接下来我们就带着这三个问题开始今天的学习。
## 问题重现
我们可以在容器里运行一下 `iperf3` 命令向容器外部发送一下数据从iperf3的输出"Retr"列里,我们可以看到有多少重传的数据包。
比如下面的例子里我们可以看到有162个重传的数据包。
```shell
# iperf3 -c 192.168.147.51
Connecting to host 192.168.147.51, port 5201
[ 5] local 192.168.225.12 port 51700 connected to 192.168.147.51 port 5201
[ ID] Interval Transfer Bitrate Retr Cwnd
[ 5] 0.00-1.00 sec 1001 MBytes 8.40 Gbits/sec 162 192 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 9.85 GBytes 8.46 Gbits/sec 162 sender
[ 5] 0.00-10.04 sec 9.85 GBytes 8.42 Gbits/sec receiver
iperf Done.
```
**网络中发生了数据包的重传,有可能是数据包在网络中丢了,也有可能是数据包乱序导致的。**那么,我们怎么来判断到底是哪一种情况引起的重传呢?
最直接的方法就是用tcpdump去抓包不过对于大流量的网络用tcpdump抓包瞬间就会有几个GB的数据。可是这样做的话带来的额外系统开销比较大特别是在生产环境中这个方法也不太好用。
所以这里我们有一个简单的方法那就是运行netstat命令来查看协议栈中的丢包和重传的情况。比如说在运行上面的iperf3命令前后我们都在容器的Network Namespace里运行一下netstat看看重传的情况。
我们会发现一共发生了162次604-442快速重传fast retransmits这个数值和iperf3中的Retr列里的数值是一样的。
```shell
-bash-4.2# nsenter -t 51598 -n netstat -s | grep retran
454 segments retransmited
442 fast retransmits
-bash-4.2# nsenter -t 51598 -n netstat -s | grep retran
616 segments retransmited
604 fast retransmits
```
## 问题分析
### 快速重传fast retransmit
在刚才的问题重现里我们运行netstat命令后统计了快速重传的次数。那什么是快速重传fast retransmit这里我给你解释一下。
我们都知道TCP协议里发送端sender向接受端receiver发送一个数据包接受端receiver都回应ACK。如果超过一个协议栈规定的时间RTO发送端没有收到ACK包那么发送端就会重传Retransmit数据包就像下面的示意图一样。
![](https://static001.geekbang.org/resource/image/bc/4b/bc6a6059f49a5b61e95ba3705894b64b.jpeg)
不过呢这样等待一个超时之后再重传数据对于实际应用来说太慢了所以TCP协议又定义了快速重传 fast retransmit的概念。它的基本定义是这样的**如果发送端收到3个重复的ACK那么发送端就可以立刻重新发送ACK对应的下一个数据包。**
就像下面示意图里描述的那样接受端没有收到Seq 2这个包但是收到了Seq 35的数据包那么接收端在回应Ack的时候Ack的数值只能是2。这是因为按顺序来说收到Seq 1的包之后后面Seq 2一直没有到所以接收端就只能一直发送Ack 2。
那么当发送端收到3个重复的Ack 2后就可以马上重新发送 Seq 2这个数据包了而不用再等到重传超时之后了。
![](https://static001.geekbang.org/resource/image/21/5d/21935661dda5069f8dyy91e6cd5b295d.jpeg)
虽然TCP快速重传的标准定义是需要收到3个重复的Ack不过你会发现在Linux中常常收到一个Dup Ack重复的Ack就马上重传数据了。这是什么原因呢
这里先需要提到 **SACK** 这个概念SACK也就是选择性确认Selective Acknowledgement。其实跟普通的ACK相比呢SACK会把接收端收到的所有包的序列信息都反馈给发送端。
你看看下面这张图,就能明白这是什么意思了。
![](https://static001.geekbang.org/resource/image/2d/55/2d61334e4066391ceeb90cac0bb25d55.jpeg)
那有了SACK对于发送端来说在收到SACK之后就已经知道接收端收到了哪些数据没有收到哪些数据。
在Linux内核中会有个判断你可以看看下面的这个函数大概意思是这样的如果在接收端收到的数据和还没有收到的数据之间两者数据量差得太大的话超过了reordering\*mss\_cache也可以马上重传数据。
这里你需要注意一下,**这里的数据量差是根据bytes来计算的而不是按照包的数目来计算的所以你会看到即使只收到一个SACKLinux也可以重发数据包。**
```shell
static bool tcp_force_fast_retransmit(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
return after(tcp_highest_sack_seq(tp),
tp->snd_una + tp->reordering * tp->mss_cache);
}
```
好了了解了快速重传的概念之后我们再来看看如果netstat中有大量的"fast retransmits"意味着什么?
如果你再用netstat查看"reordering"就可以看到大量的SACK发现的乱序包。
```shell
-bash-4.2# nsenter -t 51598 -n netstat -s | grep reordering
Detected reordering 501067 times using SACK
```
**其实在云平台的这种网络环境里,网络包乱序+SACK之后产生的数据包重传的量要远远高于网络丢包引起的重传。**
比如说像下面这张图里展示的这样Seq 2与Seq 3这两个包如果乱序的话那么就会引起Seq 2的立刻重传。
![](https://static001.geekbang.org/resource/image/a9/d6/a99709757a45279324600a45f7a44cd6.jpeg)
### Veth接口的数据包的发送
现在我们知道了网络包乱序会造成数据包的重传接着我们再来看看容器的veth接口配置有没有可能会引起数据包的乱序。
在上一讲里我们讲过通过veth接口从容器向外发送数据包会触发peer veth设备去接收数据包这个接收的过程就是一个网络的softirq的处理过程。
在触发softirq之前veth接口会模拟硬件接收数据的过程通过enqueue\_to\_backlog()函数把数据包放到某个CPU对应的数据包队列里softnet\_data
```shell
static int netif_rx_internal(struct sk_buff *skb)
{
int ret;
net_timestamp_check(netdev_tstamp_prequeue, skb);
trace_netif_rx(skb);
#ifdef CONFIG_RPS
if (static_branch_unlikely(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu;
preempt_disable();
rcu_read_lock();
cpu = get_rps_cpu(skb->dev, skb, &rflow);
if (cpu < 0)
cpu = smp_processor_id();
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
rcu_read_unlock();
preempt_enable();
} else
#endif
{
unsigned int qtail;
ret = enqueue_to_backlog(skb, get_cpu(), &qtail);
put_cpu();
}
return ret;
}
```
从上面的代码我们可以看到在缺省的状况下也就是没有RPS的情况下enqueue\_to\_backlog()把数据包放到了“当前运行的CPU”get\_cpu()对应的数据队列中。如果是从容器里通过veth对外发送数据包那么这个“当前运行的CPU”就是容器中发送数据的进程所在的CPU。
对于多核的系统这个发送数据的进程可以在多个CPU上切换运行。进程在不同的CPU上把数据放入队列并且raise softirq之后因为每个CPU上处理softirq是个异步操作所以两个CPU network softirq handler处理这个进程的数据包时处理的先后顺序并不能保证。
所以veth对的这种发送数据方式增加了容器向外发送数据出现乱序的几率。
![](https://static001.geekbang.org/resource/image/c1/3b/c11581ec8f390b13ebc89fdb4cc2043b.jpeg)
### RSS和RPS
那么对于veth接口的这种发包方式有办法减少一下乱序的几率吗
其实我们在上面netif\_rx\_internal()那段代码中,有一段在"#ifdef CONFIG\_RPS"中的代码。
我们看到这段代码中在调用enqueue\_to\_backlog()的时候传入的CPU并不是当前运行的CPU而是通过get\_rps\_cpu()得到的CPU那么这会有什么不同呢这里的RPS又是什么意思呢
要解释RPS呢需要先看一下RSS这个RSS不是我们之前说的内存RSS而是和网卡硬件相关的一个概念它是Receive Side Scaling的缩写。
现在的网卡性能越来越强劲了从原来一条RX队列扩展到了N条RX队列而网卡的硬件中断也从一个硬件中断变成了每条RX队列都会有一个硬件中断。
每个硬件中断可以由一个CPU来处理那么对于多核的系统多个CPU可以并行的接收网络包这样就大大地提高了系统的网络数据的处理能力.
同时在网卡硬件中可以根据数据包的4元组或者5元组信息来保证同一个数据流比如一个TCP流的数据始终在一个RX队列中这样也能保证同一流不会出现乱序的情况。
下面这张图大致描述了一下RSS是怎么工作的。
![](https://static001.geekbang.org/resource/image/ea/33/ea365c0d44625cf89c746d91799f9633.jpeg)
RSS的实现在网卡硬件和驱动里面而RPSReceive Packet Steering其实就是在软件层面实现类似的功能。它主要实现的代码框架就在上面的netif\_rx\_internal()代码里,原理也不难。
就像下面的这张示意图里描述的这样在硬件中断后CPU2收到了数据包再一次对数据包计算一次四元组的hash值得到这个数据包与CPU1的映射关系。接着会把这个数据包放到CPU1对应的softnet\_data数据队列中同时向CPU1发送一个IPI的中断信号。
这样一来后面CPU1就会继续按照Netowrk softirq的方式来处理这个数据包了。
![](https://static001.geekbang.org/resource/image/e0/f9/e0413b74cbc1b5edcde5442fe94e11f9.jpeg)
RSS和RPS的目的都是把数据包分散到更多的CPU上进行处理使得系统有更强的网络包处理能力。在把数据包分散到各个CPU时保证了同一个数据流在一个CPU上这样就可以减少包的乱序。
明白了RPS的概念之后我们再回头来看veth对外发送数据时候在enqueue\_to\_backlog()的时候选择CPU的问题。显然如果对应的veth接口上打开了RPS的配置以后那么对于同一个数据流就可以始终选择同一个CPU了。
其实我们打开RPS的方法挺简单的只要去/sys目录下在网络接口设备接收队列中修改队列里的rps\_cpus的值这样就可以了。rps\_cpus是一个16进制的数每个bit代表一个CPU。
比如说我们在一个12CPU的节点上想让host上的veth接口在所有的12个CPU上都可以通过RPS重新分配数据包。那么就可以执行下面这段命令
```shell
# cat /sys/devices/virtual/net/veth57703b6/queues/rx-0/rps_cpus
000
# echo fff > /sys/devices/virtual/net/veth57703b6/queues/rx-0/rps_cpus
# cat /sys/devices/virtual/net/veth57703b6/queues/rx-0/rps_cpus
fff
```
## 重点小结
好了,今天的内容讲完了,我们做个总结。我们今天讨论的是容器中网络包乱序引起重传的问题。
由于在容器平台中看到大部分的重传是快速重传fast retransmits我们先梳理了什么是快速重传。快速重传的基本定义是**如果发送端收到3个重复的ACK那么发送端就可以立刻重新发送ACK对应的下一个数据包而不用等待发送超时。**
不过我们在Linux系统上还会看到发送端收到一个重复的ACK就快速重传的这是因为Linux下对SACK做了一个特别的判断之后就可以立刻重传数据包。
我们再对容器云平台中的快速重传做分析,就会发现这些重传大部分是由包的乱序触发的。
通过对容器veth网络接口进一步研究我们知道它可能会增加数据包乱序的几率。同时在这个分析过程中我们也看到了Linux网络RPS的特性。
**RPS和RSS的作用类似都是把数据包分散到更多的CPU上进行处理使得系统有更强的网络包处理能力。它们的区别是RSS工作在网卡的硬件层而RPS工作在Linux内核的软件层。**
在把数据包分散到各个CPU时RPS保证了同一个数据流是在一个CPU上的这样就可以有效减少包的乱序。那么我们可以把RPS的这个特性配置到veth网络接口上来减少数据包乱序的几率。
不过我这里还要说明的是RPS的配置还是会带来额外的系统开销在某些网络环境中会引起softirq CPU使用率的增大。那接口要不要打开RPS呢这个问题你需要根据实际情况来做个权衡。
同时你还要注意TCP的乱序包并不一定都会产生数据包的重传。想要减少网络数据包的重传我们还可以考虑协议栈中其他参数的设置比如/proc/sys/net/ipv4/tcp\_reordering。
## 思考题
在这一讲中我们提到了Linux内核中的tcp\_force\_fast\_retransmit()函数。那么你可以想想看这个函数中的tp->recording和内核参数 /proc/sys/net/ipv4/tcp\_reordering是什么关系它们对数据包的重传会带来什么影响
```shell
static bool tcp_force_fast_retransmit(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
return after(tcp_highest_sack_seq(tp),
tp->snd_una + tp->reordering * tp->mss_cache);
}
```
欢迎你在留言区分享你的思考或疑问。如果学完这一讲让你有所收获,也欢迎转发给你的同事、或者朋友,一起交流探讨。