web性能优化(二):针对TCP传输过程中的堵塞

1,722 阅读9分钟

--本文采自本人公众号【猴哥别瞎说】

接着上次讲述到的TCP三次握手,我们来看看“小青蛙”和“小乌龟”开始了聊天模式之后,会遇到哪些可能的沟通问题呢?

铺垫

在前文中,我们提到了“发送能力”和“接收能力”。针对这两个能力,前文讲到了在握手过程中如何判断对方是否拥有此能力。

但在沟通过程中,还需要对这两项能力进行量的划分。比如:A 拥有发送能力,它能够一次发送 60 单位;B 拥有发送能力,它一次能够 10 单位。

在沟通过程中,是需要媒介的(比如,人与人之间的说话,需要空气作为媒介;真空中是无法对话的)。媒介有传输能力,并且也有量的划分(比如,对于声音的传播,在水中会比在空气中要慢一些)。

为简单起见,做两点说明:

  1. 在我们的 Story 中,媒介是一条管道
  2. 发送能力、接收能力与传输能力的单位,都不是一成不变的,它们会实时变化。

做了这些铺垫,我们来看看 Story 模式下的“小青蛙”和“小乌龟”会遇到哪些可能的沟通问题呢?

Story 模式

大于对方的接收能力

在沟通过程中,每个人的接收能力是不一样的。于是就有可能产生一种常见的沟通问题:发送方的发送单位大于接收方的接收能力。如下图:

在上图中,“小青蛙”的接受能力是 40 单元,“小乌龟”的发送能力是 100 单元,管道的传输能力是 1000 单元。这个时候,“小乌龟”发送了一个100 单元给“小青蛙”,此时发送的大小超过了“小青蛙”的接收能力。

遇到这个问题该怎么办呢?

也好办~ 在每次传输数据的时候都尝试动态性地告知对方,现阶段自己的接收能力 即可。

在 TCP 的传输过程中,针对这个情况的解决方案叫“流量控制”。针对此,下面的章节会技术性展开。现在继续我们的 Story 模式。

管道传输能力不够

假设“小青蛙”和“小乌龟”已经就上面的小分歧达成了一直解决方案,每次都发送不超过对方接受能力的数据给到对方。但是依然会存在问题:

在两者沟通的过程中,有传输介质(管道)的存在。管道的传输能力是不确定的,它有可能小于双方已经商议好的传送单元大小。如下图:

在上图中,“小青蛙”的接受能力是 120 单元,“小乌龟”的发送能力是 100 单元,它们彼此商定好的传送数据单位是 90 单元。但此时的管道传输能力只有 50单元,此时的沟通也是不成功的。

遇到这个问题的时候该怎么办呢?

在 TCP 的传输过程中,针对这个情况的解决方案叫“慢启动”。技术性细节在此先不展开,只是表述一下它的解决思路:因为不清楚实时的管道传输能力,于是在刚开始传送数据的时候,从一个很小的单位值做尝试;在之后的传送过程中,逐渐翻倍增大传送数据单位值。遇到失败的情况,就立马减小传送数据单位值。

技术讲解TCP中的堵塞

Story 模式结束。现在从技术角度来分析一下,流量控制与慢启动的详细过程。

流量控制

流量控制是一种预防发送端过多向接收端发送数据的机制。否则,接收端可能因为忙碌、负载重或缓冲区既定而无法处理。为实现流量控制,TCP 连接的每一方都要通告自己的接收窗口(rwnd),其中包含能够保存数据的缓冲区空间大小信息。详细如下图:

这里的 接收窗口(rwnd) 就是 Story 中提到的 接收能力单位值。

值得注意的是:第一次建立连接的时候,两端都会使用自身系统的默认设置值来发送 rwnd。这意味着,在某些情况下,我们可以考虑通过改变系统的 rwnd 的值来尝试优化 Web 网络性能。

慢启动方案

尽管 TCP 有了流量控制机制,但网络拥塞崩溃仍然在 1980 年代中后期浮出水面。 流量控制确实可以防止发送端向接收端过多发送数据,但却没有机制预防任何一端向潜在网络(即 Story 中的管道)过多发送数据。换句话说,发送端和接收端在连接建立之初,谁也不知道可用带宽是多少,因此需要一个估算机制,然后还要根据网络中不断变化的条件而动态改变速度。

慢启动的设计思路在于:服务器通过 TCP 连接初始化一个新的拥塞窗口(cwnd)变量,将其值设置为一个系统设定的保守值(在 Linux 中就是 initcwnd)。

拥塞窗口大小(cwnd):

发送端对从客户端接收确认(ACK)之前可以发送数据量的限制。

这里的 cwnd 变量可以理解为 Story 中管道的传输能力。

在实现慢启动的过程中,一直贯穿着一条规则:即客户端与服务器之间最大可以传输(未经 ACK 确认的)数据量取 rwnd 和 cwnd 变量中的最小值。这条规则翻译到 Story 中就是:“小青蛙”与“小乌龟”之间的传输单位取 对方的接收能力单位和 管道传输能力中的最小值。

发送端不会通告 cwnd 变量,即发送端和接收端不会交换这个值,此时的 cwnd 变量是发送端的私有变量。那服务器和客户端怎么确定拥塞窗口大小的最优值呢?毕竟,网络状况随时都在变化。如果能通过算法来确定每个连接的窗口大小,而不用手工调整就最好了。

解决方案就是慢启动,即在分组被确认后增大窗口大小,慢慢地启动! 看如下示意图:

最初,cwnd 的值只有 1 个 TCP 段。1999 年 4 月,RFC 2581 将其增加到了 4 个 TCP 段。2013 年 4 月,RFC 6928 再次将其提高到 10 个 TCP 段。

新 TCP 连接传输的最大数据量取 rwnd 和 cwnd 中的最小值,服务器实际上可以向客户端发送 4 个 TCP 段(该数值参照的是 RFC 2581),然后就必须停下来等待确认。此后,每收到一个 ACK,慢启动算法就会告诉服务器可以将它的 cwnd 窗口增加 1 个 TCP 段。每次收到 ACK 后,都可以多发送两个新的分组。TCP 连接的这个阶段通常被称为“指数增长”阶 段(如上图),因为客户端和服务器都在向两者之间网络路径的有效带宽迅速靠拢。

为什么知道有个慢启动对我们构建浏览器应用这么重要呢?因为包括 HTTP 在内的很多应用层协议都运行在 TCP 之上,无论带宽多大,每个 TCP 连接都必须经过慢启动阶段。换句话说,我们不可能一上来就完全利用连接的最大带宽!

相反,我们要从一个相对较小的拥塞窗口开始,每次往返都令其翻倍(指数式增长)。

慢启动重启

除了调节新连接的传输速度,TCP 还实现了 SSR(Slow-Start Restart,慢启动重启)机制。这种机制会在连接空闲一定时间后重置连接的拥塞窗口。道理很简单, 在连接空闲的同时,网络状况也可能发生了变化,为了避免拥塞,理应将拥塞窗口重置回“安全的”默认值。

毫无疑问,SSR 对于那些会出现突发空闲的长周期 TCP 连接(比如 HTTP 的 keep-alive 连接)有很大的影响。

优化建议

禁用SSR

如上节所述,SSR 对于那些会出现突发空闲的长周期 TCP 连接(比如 HTTP 的 keep-alive 连接)有很大的影响。因此,我们建议在服务器上禁用 SSR。

增大初始堵塞窗口

慢启动导致客户端与服务器之间经过几百 ms 才能达到接近最大速度的问题,对于大型流式下载服务的影响倒不显著,因为慢启动的时间可以分摊到整个传输周期内消化掉。

可是,对于很多 HTTP 连接,特别是一些短暂、突发的连接而言,常常会出现还没有达到最大窗口请求就被终止的情况。换句话说,很多 Web 应用的性能经常受到服务器与客户端之间往返时间的制约。因为慢启动限制了可用的吞吐量,而这对于小文件传输非常不利。

这个时候,我们可以考虑增大TCP的初始拥塞窗口。

加大起始拥塞窗口可以让 TCP 在第一次往返就传输较多数据,而随后的速度提升也会很明显。对于突发性的短暂连接,这也是特别关键的一个优化。

总结

调优 TCP 性能可以让服务器和客户端之间达到最大吞吐量和最小延迟。结合之前的TCP 三次握手的文章,我们已经简单概要性地讲解了 TCP 协议上的一些优化细节。

当然,更快的性能优化是一个值得考虑的问题,而我们的应用程序如何建立或使用 TCP 连接也是另一个值得考虑的点:

• 再快也快不过什么也不用发送,能少发就少发(消除不必要的数据传输)。

• 我们不能让数据传输得更快,但可以让它们传输的距离更短(CDN)。


前端性能优化系列:

(一):从TCP的三次握手讲起

(二):针对TCP传输过程中的堵塞

(三):HTTP协议的优化

(四):图片优化

(五):浏览器缓存策略

(六):浏览器是如何工作的?

(七):webpack性能优化