数据从发送端到接收端,链路很长,任何一个地方都可能发生丢包,几乎可以说丢包不可避免。
首先,我们两个手机的绿皮聊天软件客户端,要通信,中间会通过它们家服务器。大概长这样。
聊天软件三端通信
但为了简化模型,我们把中间的服务器给省略掉,假设这是个端到端的通信。且为了保证消息的可靠性,我们盲猜它们之间用的是tcp协议进行通信。
聊天软件两端通信
为了发送数据包,两端首先会通过三次握手,建立tcp连接。
一个数据包,从聊天框里发出,消息会从聊天软件所在的用户空间拷贝到内核空间的发送缓冲区(send buffer),数据包就这样顺着传输层、网络层,进入到数据链路层,在这里数据包会经过流控(qdisc),再通过ringbuffer发到物理层的网卡。数据就这样顺着网卡发到了纷繁复杂的网络世界里。这里头数据会经过n多个路由器和交换机之间的跳转,最后到达目的机器的网卡处。
此时目的机器的网卡会通知dma将数据包信息放到ringbuffer中,再触发一个硬中断给cpu,cpu触发软中断让ksoftirqd去ringbuffer收包,于是一个数据包就这样顺着物理层,数据链路层,网络层,传输层,最后从内核空间拷贝到用户空间里的聊天软件里。
网络发包收包全景图
画了那么大一张图,只水了200字做解释,我多少是有些心痛的。
到这里,抛开一些细节,大家大概知道了一个数据包从发送到接收的宏观过程。
可以看到,这上面全是密密麻麻的名词。
整条链路下来,有不少地方可能会发生丢包。
但为了不让大家保持蹲姿太久影响身体健康,我这边只重点讲下几个常见容易发生丢包的场景。
tcp协议会通过三次握手建立连接。大概长下面这样。
tcp三次握手
在服务端,第一次握手之后,会先建立个半连接,然后再发出第二次握手。这时候需要有个地方可以暂存这些半连接。这个地方就叫半连接队列。
如果之后第三次握手来了,半连接就会升级为全连接,然后暂存到另外一个叫全连接队列的地方,坐等程序执行accept()
方法将其取走使用。
半连接队列和全连接队列
是队列就有长度,有长度就有可能会满,如果它们满了,那新来的包就会被丢弃。
可以通过下面的方式查看是否存在这种丢包行为。
# 全连接队列溢出次数# netstat -s | grep overflowed4343 times the listen queue of a socket overflowed# 半连接队列溢出次数# netstat -s | grep -i "syns to listen sockets dropped"109 times the listen queue of a socket overflowed
从现象来看就是连接建立失败。
应用层能发网络数据包的软件有那么多,如果所有数据不加控制一股脑冲入到网卡,网卡会吃不消,那怎么办?让数据按一定的规则排个队依次处理,也就是所谓的qdisc(queueing disciplines,排队规则),这也是我们常说的流量控制机制。
排队,得先有个队列,而队列有个长度。
我们可以通过下面的ifconfig命令查看到,里面涉及到的txqueuelen后面的数字1000,其实就是流控队列的长度。
当发送数据过快,流控队列长度txqueuelen又不够大时,就容易出现丢包现象。
qdisc丢包
可以通过下面的ifconfig命令,查看tx下的dropped字段,当它大于0时,则有可能是发生了流控丢包。
# ifconfig eth0eth0: flags=4163<up,broadcast,running,multicast> mtu 1500inet 172.21.66.69 netmask 255.255.240.0 broadcast 172.21.79.255inet6 fe80::216:3eff:fe25:269f prefixlen 64 scopeid 0x20<link>ether 00:16:3e:25:26:9f txqueuelen 1000 (ethernet)rx packets 6962682 bytes 1119047079 (1.0 gib)rx errors 0 dropped 0 overruns 0 frame 0tx packets 9688919 bytes 2072511384 (1.9 gib)tx errors 0 dropped 0 overruns 0 carrier 0 collisions 0
当遇到这种情况时,我们可以尝试修改下流控队列的长度。比如像下面这样将eth0网卡的流控队列长度从1000提升为1500.
# ifconfig eth0 txqueuelen 1500
网卡和它的驱动导致丢包的场景也比较常见,原因很多,比如网线质量差,接触不良。除此之外,我们来聊几个常见的场景。
上面提到,在接收数据时,会将数据暂存到ringbuffer接收缓冲区中,然后等着内核触发软中断慢慢收走。如果这个缓冲区过小,而这时候发送的数据又过快,就有可能发生溢出,此时也会产生丢包。
ringbuffer满了导致丢包
我们可以通过下面的命令去查看是否发生过这样的事情。
# ifconfigeth0: rx errors 0 dropped