# 28 | 网络数据传输慢,问题到底出在哪了? 你好,我是庄振运。 你一定有过在网页或者手机上下载照片的体验,如果数据传输太慢,那你的体验一定十分糟糕。你看,互联网实体之间的数据快速传输对用户体验至关重要。这里涉及到的其实就是网络传输问题。所以,今天我们就通过生产实践中的案例,来探讨一下互联网服务中的数据传输性能。 说到底,网络传输问题其实就分两种: 1. 数据根本没有传递; 2. 数据传送速度较慢。 “数据没有传递”虽然看起来更严重,但是相对“数据传送缓慢”来说,更容易判断和解决。所以,这一讲,我们就重点解决第二种问题。我们一起来看看,为什么网络传送速度会慢,在众多原因中怎么快速诊断出关键问题来,又该如何去解决。 造成网络传输缓慢的原因很多,我们这一讲,就是帮助你快速诊断问题出在哪里:是客户端,是服务器端,还是网络本身?在此基础上,你才能专门针对具体的领域继续分析。 ## 为什么数据传输慢? 我们先看一下,都有哪些可能的原因会导致数据传输缓慢呢?在宏观上,这种问题的可能原因可以分为三种场景: 1. 客户端应用程序的原因; 2. 网络的原因; 3. 服务器应用程序的原因。 也就是说,可能是由于数据发送方过载,而没有向接收方发送数据;也可能是网络通道很慢;又或者是数据接收方的服务器太忙,从而无法从网络缓冲区读取数据。 为了描述方便,我们根据平时客户浏览网页的场景,假设客户端是数据接收方,而服务器端是数据发送方。 进行此类分析诊断时,负责的工程师通常需要快速隔离出上述不同场景,以便他们可以专注于特定场景里面的可疑组件,并对本质原因进行更深入的分析。但这一快速诊断过程会遇到很多难点。 首先,**数据传输涉及多个网络实体**,包括两台机器(也就是发送者和接收者)和网络路由,这与仅涉及一台机器的常见性能问题形成鲜明对比。 其次,这种**诊断涉及多层信息**,包括应用程序层和网络传输层。为了找出原因,工程师必须检查各种数据,包括客户端日志、服务器日志、网络统计信息、CPU使用情况等。这些检查需要花费很多时间和精力,并且通常需要性能工程师的经验和专业知识。 更加让人郁闷的是,这些日志往往分散在不同的地方,比如客户端和服务器。为了节省时间和精力,性能工程师迫切需要更智能的工具,以帮助他们快速找出根本原因。 所以今天,我专注于解决这样的一个问题,就是:快速确定应归咎的组件范围和场景(无论是发送方、接收方还是网络本身)。我提出了一种**当发生数据传输缓慢的问题时,可以自动隔离原因的解决方案**。毕竟,你只有找出了要对“数据传输慢”负责任的那一部分,才可以进行后续分析工作,最终确定真正的问题。 ## 如何判断问题所在位置? 要想快速诊断,我们需要先看看三种问题场景的不同特征。 这个解决方案本质上依靠的是**客户端和服务器端的TCP层面的特征**。TCP是传输层协议之一,可提供有序且可靠的流字节传输,是当今使用最广泛的传输协议。TCP具有**流控制**功能,可避免接收方过载。接收方设置专用的接收缓冲区,发送方设置相应的发送缓冲区。数据发送方(服务器)的发送缓冲区和数据接收方(客户端)的接收缓冲区,都可以通过操作系统来监测当前队列大小。 为了能够识别瓶颈,你需要在发送方和接收方的传输层上,收集有关队列大小的信息。有很多收集此类信息的方法。你有两种工具可以使用,分别是Netstat和ss。 Netstat是一个命令行工具,可以显示网络连接和网络协议统计信息。我们主要是用它来观察TCP / IP套接字的发送队列和接收队列的大小。而ss命令,可以显示套接字统计信息,包括显示TCP以及其他类型套接字的统计信息。类似于Netstat,ss还可以显示发送和接收队列大小。 除了相应的工具的介绍,为了帮助你理解,我们还需要先重温一下传输数据时候,应用层和TCP层的交互。 ![](https://static001.geekbang.org/resource/image/db/89/dbe011441395739fbef5869848224789.png) 上图显示了任何基于TCP的数据传输中的典型流程。关于系统调用和网络传输的五个步骤如下: 1. 在步骤A,服务器应用程序发出write()系统调用,并将应用程序数据复制到套接字发送缓冲区。 2. 在步骤B,服务器的TCP层发出send()调用,并将一些数据发送到网络;数据量受TCP的拥塞控制和流控制。 3. 在步骤C,网络将数据逐跳路由到接收方(IP路由协议在这部分中发挥作用)。 4. 在步骤D,客户端的TCP层将通过recv()系统调用接收数据,数据放入接收缓冲区。 5. 在步骤E,客户端应用程序发出read()调用,以接收数据并将其复制到用户空间。 接下来,我们就来看看三种不同场景下的问题特征是什么样的。 我们先看第一个场景,**客户端接收数据缓慢**的情况。为了重现这一场景,我们做一个实验,让发送端发送一段固定大小的数据给接收方。我们强制接收方,也就是客户端,减慢数据的接收速度。具体做法,就是在应用程序代码的read()调用之前,注入了一定的延迟,这种场景代表了客户端数据接收成为瓶颈的情况。 ![](https://static001.geekbang.org/resource/image/1e/d8/1e28e9d5065f653f0cdc5ee1ddd639d8.png) 上图显示的是数据发送方的发送缓冲区,SendQ(Send Queue)的大小变化。开始时候,数据发送调用send(),立刻注满SendQ。随着数据的传输,慢慢变为0。 ![](https://static001.geekbang.org/resource/image/07/13/0757f0a16dbf81ebed5c9f092c260913.png) 第二张图是客户端的接收缓冲区,RecvQ(Receive Queue)的大小变化,客户端因为应用程序运行缓慢,所以RecvQ具有一定的积累,这可以由非零值来看出。这些非零值持续了一段时间,随着应用程序不断地读取,最终RecvQ减为0。 对于第二种场景,也就是**数据发送方是瓶颈**的情况,我们强制发送方(即服务器端),放慢数据的发送速度。具体来说,我们在应用程序代码中,对write()的调用之前注入了一定的延迟,模拟了发送者是瓶颈的情况。 ![](https://static001.geekbang.org/resource/image/8d/1a/8dc79b587e12571b62b7725256bdb11a.png) 上图显示了服务器端的SendQ的值,你可以看到,SendQ几乎全部是零。这是因为发送端是瓶颈,其他地方不是瓶颈,所以任何SendQ的数据会被很快发送出去。 你可以在图片中看到一个持续时间很短的峰值,这是因为SendQ取样的时候恰好取到数据还没有被传输到网络中的时候。但因为这个峰值持续时间很短,简单的过滤就可以去掉。 接收端的RecvQ显示在下图,你可以看到,因为接收端不是瓶颈,RecvQ是零。 ![](https://static001.geekbang.org/resource/image/d8/3c/d8283edde9ac0494f4f80029b2436a3c.png) 第三个场景,是**网络本身是瓶颈**造成的数据传输缓慢。我们通过向网络路径注入延迟来创造这一场景,以使TCP仅能以非常低的吞吐量进行传输。 ![](https://static001.geekbang.org/resource/image/6d/1d/6d0ed6a9c6cf1e51085e366a7a84061d.png) 图片中显示了发送端的SendQ值,你可以看到它的值不为零,因为那些数据不能很快地被传送出去。 再来看接收端的RecvQ,如下图。RecvQ全为零,这些零值就代表了快速的数据传递。 ![](https://static001.geekbang.org/resource/image/36/05/36e98d68b61e115e6b5a5ede39dc6205.png) 通过上面三种场景的分析,尤其是对发送端SendQ和接收端RecvQ的观察,我们不难总结出规律来。正常的数据传输情况下,客户端的接收队列和服务器端的发送队列都应该是零。 反之,如果数据传输缓慢,则有如下几种情况: 1. 如果客户端上的接收队列RecvQ不为零,则客户端应用程序是性能瓶颈; 2. 如果服务器上的发送队列SendQ为零,则服务器应用程序是性能瓶颈; 3. 如果客户端的接收队列RecvQ为零,而服务器的发送队列SendQ为非零,则网络本身是性能瓶颈。 为了帮助你加深记忆,我用表格来做了个归纳。 ![](https://static001.geekbang.org/resource/image/31/f3/312fe519d3db2f20f9b44c547f0e00f3.png) 你可以通过这个表格,快速判断问题出现的位置。 ## 解决方案如何落地? 根据前面的分析和总结,我们现在提出解决方案。这是一个基于**状态转移**的方案,需要从客户端和服务器端收集几个关键点的信息。 为了帮助你理解,我们需要先来看看数据请求和传输流程图。就用常见的HTTP协议的Request和Response方式来描述,如下图所示。 ![](https://static001.geekbang.org/resource/image/b0/24/b0b9e458e45019a7cdae9bf6b9b2da24.png) 当客户端需要下载服务器的数据时,首先在T0发出数据请求;网络将请求发送到服务器后,服务器在T1收到数据。 然后,服务器开始准备数据,数据准备好后,服务器将开始在T2时发回数据。通过一系列write()调用。发送在T4完成。网络传输后,客户端在T3开始接收数据,并在T5完成接收。请注意,尽管其他时间戳是按照严格的顺序,T3和T4的顺序可能会因实际情况而异。具体来说,对于小数据传输时,T4可以先于T3,因为单个write()调用就足够了。对于大数据传输,通常使用T3在T4之前。 接下来,我们来看看基于状态机的解决方案,它是一个针对HTTP数据传输问题的,完整而具体的解决方案。 从上面的过程中,我们可以看到,如果服务器无法接收到数据请求,则数据传递将不会发生,因此不会完成。 我用下图来表示整个状态机。这个状态机展示了整个HTTP数据传输的过程,包括Request和Response。如果数据传递成功,状态机最后会到达状态F。 ![](https://static001.geekbang.org/resource/image/46/ca/466e92b6401de7e5c476812c800b55ca.png) 如上图,数据传递的初始状态为State-S。客户端发出请求后,它将移至状态A;当网络通道完成其工作,并将请求传递到服务器OS时,状态变为B。当服务器准备好数据,并开始发出数据的第一个字节时,状态变为C。 当客户端收到第一个字节后,状态变为D;最后当服务器发出最后一个字节时,状态变为E。或者这两个次序交换,成功进行数据传递的最终状态是State-F。 在整个过程中,如果发生其他的转移,那么就是网络传输有问题了。我们就可以根据发送端的发送队列和接收端的接收队列长度的变化,轻松判断是谁的问题,比如是客户端,服务器或是网路。 ## 总结 今天我们讲述了,互联网服务在传输数据时,如果发生传输速度太慢的问题,怎样才能快速地诊断到底是客户端、服务器端,还是网络的问题。 ![](https://static001.geekbang.org/resource/image/4a/9d/4a785ec504c96d6bfbc079f61fe9539d.png) 唐代诗人高适的《燕歌行》有几句诗:“山川萧条极边土,胡骑凭陵杂风雨。战士军前半死生,美人帐下犹歌舞。”说的是前方的战士,在前线出生入死;后方却有人逍遥自在的观赏美人歌舞,醉生梦死。这种冰火两重天的讽刺,部分原因,就是责任没有分清,以至于滥竽充数者可以逍遥自在。 对待数据传输缓慢问题,我们也很希望能快速地搞清责任,分清是哪里的问题,然后才能有针对性地继续分析。我们的解决方案就是根据TCP的Send和Receive队列大小变化,来快速诊断的方案。它能智能而快速地分清问题的大致范围:就是数据发送方、数据接收方,还是网络。 ## 思考题 根据你平时的观察,公司业务有没有发生数据传输太慢的问题?如果发生了,你们一般怎么根因呢?如果采用本讲的思路,会不会更加快速地诊断? 欢迎你在留言区分享自己的思考,与我和其他同学一起讨论,也欢迎你把文章分享给自己的朋友。