用了 TCP 协议,数据一定不会丢吗?

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

问大家一句:TCP 是一个可靠的传输协议,那它一定能保证数据不丢失吗?

这次,就跟大家探讨这个问题。

# 数据包的发送流程

首先,我们两个手机的绿皮聊天软件客户端,要通信,中间会通过它们家服务器。大概长这样。

但为了 简化模型 ,我们把中间的服务器给省略掉,假设这是个端到端的通信。且为了保证消息的可靠性,我们盲猜它们之间用的是 TCP协议 进行通信。

为了发送数据包,两端首先会通过 三次握手 ,建立TCP连接。

一个数据包,从聊天框里发出,消息会从 聊天软件 所在的 用户空间 拷贝到 内核空间 的 发送缓冲区(send buffer) ,数据包就这样顺着 传输层、网络层,进入到数据链路层,在这里数据包会经过流控(qdisc),再通过RingBuffer发到物理层的网卡 。数据就这样顺着 网卡 发到了 纷繁复杂 的网络世界里。这里头数据会经过n多个 路由器和交换机 之间的跳转,最后到达 目的机器的网卡 处。

此时目的机器的网卡会通知 DMA 将数据包信息放到RingBuffer中,再触发一个 硬中断 给CPUCPU触发 软中断 让ksoftirqdRingBuffer收包,于是一个数据包就这样顺着 物理层,数据链路层,网络层,传输层 ,最后从内核空间拷贝到用户空间里的 聊天软件 里。

画了那么大一张图,只水了200字做解释,我多少是有些心痛的。

到这里,抛开一些细节,大家大概知道了一个数据包从 发送到接收 的宏观过程。

可以看到,这上面全是密密麻麻的 名词 。

整条链路下来,有不少地方可能会发生丢包。

但为了不让大家 保持蹲姿太久 影响身体健康,我这边只重点讲下几个 常见容易发生丢包的场景 。

# 建立连接时丢包

TCP协议会通过 三次握手 建立连接。大概长下面这样。

在服务端,第一次握手之后,会先建立个 半连接 ,然后再发出第二次握手。这时候需要有个地方可以 暂存 这些半连接。这个地方就叫 半连接队列 。

如果之后第三次握手来了,半连接就会升级为全连接,然后暂存到另外一个叫 全连接队列 的地方,坐等程序执行accept()方法将其取走使用。

是队列就有长度,有长度就有可能会满,如果它们 满了 ,那新来的包就会被 丢弃 。

可以通过下面的方式查看是否存在这种丢包行为。

    # 全连接队列溢出次数
    # netstat -s | grep overflowed
        4343 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 ( Q ueueing Disc iplines,排队规则),这也是我们常说的 流量控制 机制。

排队,得先有个队列,而队列有个 长度 。

我们可以通过下面的ifconfig命令查看到,里面涉及到的txqueuelen后面的数字1000,其实就是流控队列的长度。

当发送数据过快,流控队列长度txqueuelen又不够大时,就容易出现 丢包 现象。

可以通过下面的ifconfig命令,查看TX下的dropped字段,当它大于0时,则 有可能 是发生了流控丢包。

    # ifconfig eth0
    eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
            inet 172.21.66.69  netmask 255.255.240.0  broadcast 172.21.79.255
            inet6 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 0
            TX 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接收缓冲区中,然后等着内核触发软中断慢慢收走。如果这个 缓冲区过小 ,而这时候发送的数据又过快,就有可能发生溢出,此时也会产生 丢包 。

我们可以通过下面的命令去查看是否发生过这样的事情。

    # ifconfig
    eth0:  RX errors 0  dropped 0  overruns 0  frame 0

查看上面的overruns指标,它记录了由于RingBuffer长度不足导致的溢出次数。

当然,用ethtool命令也能查看。

    # ethtool -S eth0|grep rx_queue_0_drops

但这里需要注意的是,因为一个网卡里是可以有 多个RingBuffer 的,所以上面的rx_queue_0_drops里的0代表的是 第0个RingBuffer 的丢包数,对于多队列的网卡,这个0还可以改成其他数字。但我的家庭条件不允许我看其他队列的丢包数,所以上面的命令对我来说是够用了。。。

当发现有这类型丢包的时候,可以通过下面的命令查看当前网卡的配置。

    #ethtool -g eth0
    Ring parameters for eth0:
    Pre-set maximums:
    RX:        4096
    RX Mini:    0
    RX Jumbo:    0
    TX:        4096
    Current hardware settings:
    RX:        1024
    RX Mini:    0
    RX Jumbo:    0
    TX:        1024

上面的输出内容,含义是 RingBuffer最大支持4096的长度,但现在实际只用了1024。

想要修改这个长度可以执行ethtool -G eth1 rx 4096 tx 4096将发送和接收RingBuffer的长度都改为4096。

RingBuffer 增大之后,可以减少因为容量小而导致的丢包情况。

# 网卡性能不足

网卡作为硬件, 传输速度是有上限的 。当网络传输速度过大,达到网卡上限时,就会发生丢包。这种情况一般常见于压测场景。

我们可以通过ethtool加网卡名,获得当前网卡支持的最大速度。

    # ethtool eth0
    Settings for eth0:
        Speed: 10000Mb/s

可以看到,我这边用的网卡能支持的最大传输速度 speed=1000Mb/s 。

也就是俗称的千兆网卡,但注意这里的单位是 Mb ,这里的 b是指bit,而不是Byte。1Byte=8bit 。所以10000Mb/s还要除以8,也就是理论上网卡最大传输速度是1000/8 = 125MB/s

我们可以通过sar命令从网络接口层面来分析数据包的收发情况。

    # sar -n DEV 1
    Linux 3.10.0-1127.19.1.el7.x86_64      2022年07月27日     _x86_64_    (1 CPU)
    
    08时35分39秒     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s    rxcmp/s   txcmp/s  rxmcst/s
    08时35分40秒      eth0      6.06      4.04      0.35    121682.33   0.00    0.00     0.00

其中 txkB/s是指当前每秒发送的字节(byte)总数,rxkB/s是指每秒接收的字节(byte)总数 。

当两者加起来的值约等于12~13w字节的时候,也就对应大概125MB/s的传输速度。此时达到网卡性能极限,就会开始丢包。

遇到这个问题,优先看下你的服务是不是真有这么大的 真实流量 ,如果是的话可以考虑下拆分服务,或者就忍痛充钱升级下配置吧。

# 接收缓冲区丢包

我们一般使用TCP socket进行网络编程的时候,内核都会分配一个 发送缓冲区 和一个 接收缓冲区 。

当我们想要发一个数据包,会在代码里执行send(msg),这时候数据包并不是一把梭直接就走网卡飞出去的。而是将数据拷贝到内核 发送缓冲区 就完事 返回 了,至于 什么时候发数据,发多少数据 ,这个后续由内核自己做决定。

而 接收缓冲区 作用也类似,从外部网络收到的数据包就暂存在这个地方,然后坐等用户空间的应用程序将数据包取走。

这两个缓冲区是有大小限制的,可以通过下面的命令去查看。

    # 查看接收缓冲区
    # sysctl net.ipv4.tcp_rmem
    net.ipv4.tcp_rmem = 4096    87380   6291456
    
    # 查看发送缓冲区
    # sysctl net.ipv4.tcp_wmem
    net.ipv4.tcp_wmem = 4096    16384   4194304

不管是接收缓冲区还是发送缓冲区,都能看到三个数值,分别对应缓冲区的 最小值,默认值和最大值 (min、default、max)。缓冲区会在min和max之间动态调整。

那么问题来了,如果缓冲区设置过小会怎么样?

对于 发送缓冲区 ,执行send的时候,如果是 阻塞 调用,那就会等,等到缓冲区有空位可以发数据。

如果是 非阻塞 调用,就会 立刻返回 一个 EAGAIN 错误信息,意思是 Try again。让应用程序下次再重试。这种情况下一般不会发生丢包。

当接受缓冲区满了,事情就不一样了,它的TCP接收窗口会变为0,也就是所谓的 零窗口 ,并且会通过数据包里的win=0,告诉发送端,"球球了,顶不住了,别发了"。一般这种情况下,发送端就该停止发消息了,但如果这时候确实还有数据发来,就会发生 丢包 。

我们可以通过下面的命令里的TCPRcvQDrop查看到有没有发生过这种丢包现象。

    cat /proc/net/netstat
    TcpExt: SyncookiesSent TCPRcvQDrop SyncookiesFailed
    TcpExt: 0              157              60116

但是说个伤心的事情,我们一般也看不到这个TCPRcvQDrop,因为这个是5.9版本里引入的打点,而我们的服务器用的一般是2.x~3.x左右版本。你可以通过下面的命令查看下你用的是什么版本的linux内核。

    # cat /proc/version
    Linux version 3.10.0-1127.19.1.el7.x86_64

# 两端之间的网络丢包

前面提到的是两端机器内部的网络丢包,除此之外,两端之间那么长的一条链路都属于外部网络,这中间有各种路由器和交换机还有光缆啥的,丢包也是很经常发生的。

这些丢包行为发生在中间链路的某些个机器上,我们当然是没权限去登录这些机器。但我们可以通过一些命令观察整个链路的连通情况。

ping命令查看丢包

比如我们知道目的地的域名是 baidu.com。想知道你的机器到baidu服务器之间,有没有产生丢包行为。可以使用ping命令。

倒数第二行里有个100% packet loss,意思是丢包率100%。

但这样其实你只能知道 你的机器和目的机器之间有没有丢包。

那如果你想知道你和目的机器之间的这条链路,哪个节点丢包了,有没有办法呢?

有。

mtr命令

mtr命令可以查看到你的机器和目的机器之间的每个节点的丢包情况。

像下面这样执行命令。

其中 -r 是指report,以报告的形式打印结果。

可以看到Host那一列,出现的都是链路中间每一跳的机器,Loss的那一列就是指这一跳对应的丢包率。

需要注意的是,中间有一些是host是???,那个是因为 mtr默认用的是ICMP包 ,有些节点限制了 ICMP包 ,导致不能正常展示。

我们可以在mtr命令里加个-u,也就是使用 udp包 ,就能看到部分???对应的IP。

把 ICMP包和UDP包的结果 拼在一起看,就是 比较完整 的链路图了。

还有个小细节,Loss那一列,我们在icmp的场景下,关注 最后一行 ,如果是0%,那不管前面loss是100%还是80%都无所谓,那些都是 节点限制 导致的 虚报 。

但如果 最后一行是20%,再往前几行都是20%左右 ,那说明丢包就是从最接近的那一行开始产生的,长时间是这样,那很可能这一跳出了点问题。如果是公司内网的话,你可以带着这条线索去找对应的网络同事。如果是外网的话,那耐心点等等吧,别人家的开发会比你更着急。

# 发生丢包了怎么办

说了这么多。只是想告诉大家, 丢包是很常见的,几乎不可避免的一件事情 。

但问题来了,发生丢包了怎么办?

这个好办,用 TCP协议 去做传输。

建立了TCP连接的两端,发送端在发出数据后会等待接收端回复ack包ack包的目的是为了告诉对方自己确实收到了数据,但如果中间链路发生了丢包,那发送端会迟迟收不到确认ack,于是就会进行 重传 。以此来保证每个数据包都确确实实到达了接收端。

假设现在网断了,我们还用聊天软件发消息,聊天软件会使用TCP不断尝试重传数据, 如果重传期间网络恢复了 ,那数据就能正常发过去。但如果多次重试直到超时都还是失败,这时候你将收获一个 红色感叹号 。

这时候问题又来了。

假设 某绿皮聊天软件用的就是TCP协议。

在聊天的时候, 发生丢包了,丢包了会 重试 ,重试失败了还会出现 红色感叹号。

于是乎,问题就变成了, 用了 TCP 协议,就一定不会丢包吗?

# 用了TCP协议就一定不会丢包吗

我们知道TCP位于 传输层 ,在它的上面还有各种 应用层协议 ,比如常见的HTTP或者各类RPC协议。

TCP保证的可靠性,是 传输层的可靠性 。也就是说, TCP只保证数据从A机器的传输层可靠地发到B机器的传输层。

至于数据到了接收端的传输层之后,能不能保证到应用层,TCP并不管。

假设现在,我们输入一条消息,从聊天框发出,走到 传输层TCP协议的发送缓冲区 ,不管中间有没有丢包,最后通过重传都保证发到了对方的 传输层TCP接收缓冲区 ,此时接收端回复了一个ack,发送端收到这个ack后就会将自己 发送缓冲区 里的消息给扔掉。到这里TCP的任务就结束了。

TCP任务是结束了,但聊天软件的任务没结束。

聊天软件还需要将数据从TCP的接收缓冲区里读出来,如果在读出来这一刻,手机由于内存不足或其他各种原因,导致软件崩溃闪退了。

发送端以为自己发的消息已经发给对方了,但接收端却并没有收到这条消息。

于是乎, 消息就丢了。

虽然概率很小,但它就是发生了 。

合情合理,逻辑自洽。

# 这类丢包问题怎么解决?

故事到这里也到尾声了,感动之余,我们来 聊点掏心窝子的话 。

其实前面说的都对,没有一句是假话 。

但某绿皮聊天软件这么成熟,怎么可能没考虑过这一点呢。

大家应该还记得我们文章开头提到过, 为了简单 ,就将服务器那一方给省略了,从三端通信变成了两端通信,所以才有了这个丢包问题。

现在我们重新将服务器加回来。

大家有没有发现,有时候我们在手机里聊了一大堆内容,然后登录电脑版,它能将最近的聊天记录都同步到电脑版上。也就是说服务器 可能 记录了我们最近发过什么数据,假设 每条消息都有个id ,服务器和聊天软件每次都拿 最新消息的id 进行对比,就能知道两端消息是否一致,就像 对账 一样。

对于 发送方 ,只要定时跟服务端的内容对账一下,就知道哪条消息没发送成功,直接重发就好了。

如果 接收方 的聊天软件崩溃了,重启后跟服务器稍微通信一下就知道少了哪条数据,同步上来就是了,所以也不存在上面提到的丢包情况。

可以看出, TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。

那么问题叒来了, 两端通信的时候也能对账,为什么还要引入第三端服务器?

主要有三个原因。

  • 第一,如果是两端通信,你聊天软件里有1000个好友,你就得建立1000个连接。但如果引入服务端,你只需要跟服务器建立1个连接就够了, 聊天软件消耗的资源越少,手机就越省电 。
  • 第二,就是 安全问题 ,如果还是两端通信,随便一个人找你对账一下,你就把聊天记录给同步过去了,这并不合适吧。如果对方别有用心,信息就泄露了。引入第三方服务端就可以很方便的做各种 鉴权 校验。
  • 第三,是 软件版本问题 。软件装到用户手机之后,软件更不更新就是由用户说了算了。如果还是两端通信,且两端的 软件版本跨度太大 ,很容易产生各种兼容性问题,但引入第三端服务器,就可以强制部分过低版本升级,否则不能使用软件。但对于大部分兼容性问题,给服务端加兼容逻辑就好了,不需要强制用户更新软件。

所以看到这里大家应该明白了,我把服务端去掉,并不单纯是 为了简单 。

# 总结

  • 数据从发送端到接收端,链路很长,任何一个地方都可能发生丢包,几乎可以说丢包不可避免。
  • 平时没事也不用关注丢包,大部分时候TCP的重传机制保证了消息可靠性。
  • 当你发现服务异常的时候,比如接口延时很高,总是失败的时候,可以用ping或者mtr命令看下是不是中间链路发生了丢包。
  • TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。
  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值