Chromium的连接管理

来源:互联网 发布:青岛飞拉利和淘宝比 编辑:程序博客网 时间:2024/05/17 20:33
连接延迟和并行性是影响网络应用的网络性能的重要因素。因此,Chromium的工程师花了相当多的时间研究怎样最好的管理我们的连接 -- 应该打开多少,打开多久,等等。这里,我将说说我们的设计和实现基于的各种考量。

连接延迟

发起一个新的连接的代价是昂贵的。首先,Chromium必须解析主机名,这已经是一个费时的过程。然后,它还得进行各种握手。即TCP握手,可能还有SSL握手。在下面的图中,你会看到,这两者都是耗时的操作。注意这里收集的样本表示的是TCP connect()s 和SSL握手,这些数据收集与2013年2月的某一天,来自于选择参加Google Chrome改进计划的用户。


TCP连接延迟的累积分布


如图所示,新的TCP连接,虽然比完整的SSL连接快不少,通常也需要几十到数百毫秒。因此通过更好的连接管理尽可能多的隐藏这种延迟对于降低用户的感知延迟是很重要的。

Socket 延后绑定(“late binding”)

2009年我刚开始为Chromium工作时,团队刚刚重写完跨平台的网络堆栈,终于有时间开始进行优化。起初,我们的网络事务类做的事情很简单,在内部维护一个完整的状态机,根据状态机顺序执行 --  代理解析,主机解析,TCP连接,SSL握手等等。我们把这个初始的处理方式叫做“早期绑定" ("early binding"),HTTP事务很早就绑定到一个目标socket上。显然,这不是理想的处理方式。因为当HTTP事务被绑定到一个socket并等待TCP握手完成的时候,可能另一个socket变得可用了(可能是一个新连接完成的socket或者空闲下来的持久HTTP连接)。而且,由于早期绑定和网络的不可靠性(包丢失、重组、包损坏),高优先级的HTTP事务可能在低优先级的事务之后的获得一个已连接的socket。我在团队的启动项目是使用socket延后绑定来替换这种处理方式,以及介绍socket请求和连接工作的概念。HTTP事务产生请求,并放入优先级队列中,等待连接工作完成或一个持久连接释放,然后完成请求。我们一开始为TCP sockets启动该功能时,我们看到,由于改善了连接重用,每个HTTP事务的响应时间减少了6.5%

延后绑定的例子,连接1用于请求bar.html,然后复用于a.png。连接2和3启动,但3先连接上了,并且绑定到b.png。在2完成连接之前,1上的a.png已经完成,因此连接1被重用于c.png。在这个例子中,只用到了连接1和连接3,连接2没有用到,处于空闲状态。
ps: 延后绑定能够提供更大的灵活性,避免发生低优先级的事务比高优先级的事务先得到服务

利用socket延后绑定带来更多性能优化

推迟HTTP事务的绑定直到有一个实际的socket可用,这在决定哪个连接工作用于完成socket请求方面给了我们重要的灵活性。这种灵活性使我们能够在许多重要方面改善上述图示中曲线中部和长尾上的实例。

TCP后备连接工作(connect jobs)

检查上述连接延迟表,你会发现Windows的连接延迟曲线比较平滑。3秒处的震荡与Windows TCP SYN重传超时时间有关。3秒,对某些应用可能微不足道,但对于用户紧盯着屏幕等待网页加载的交互应用来说却是无法忍受的。我们的数据显示大约有1%的TCP connect()时间落入3秒这个桶中。我们这里的解决方案是,引入一个后备连接工作,在第一个启动250ms之后启动。这种very hacky solution通过快速重试来解决过长的TCP SYN重传超时问题。注意每台目标主机我们只启用了一个后备连接。实现这种处理方式之后,我们的socket请求图示显示由于TCP层的SYN传输计时器导致的3s处的震荡显著降低了。作为一个对长尾延迟和减少震荡充满热情的人,这种改变让我感觉很爽,当Firefox跟进了我们的做法时(followed suit),我很是激动。

从上图,你可以看到socket连接的DNS TCP时间与已连接socket的请求时间的对照。后备工作,只有助于TCP SYN重传超时,但对于DNS重传超时没有影响,注意到这一点是很有用的。这是因为我们没有为每个host调用多次getaddrinfo,只调用了一次。在我们即将采用的DNS stub resolver中,我们可以自己控制DNS重传超时。后备连接工作对于减少TCP 重传超时相关的震荡是很有效的。

一些人问我们,为什么不采用IE9 风格( IE9 style approach )的并行打开两个socket的方法而采用这种方法。那个方法确实不错,并且像他们说的那样,有一些优点。但是作为一个SYN包丢失的补偿方案,这恐怕就是一个拙劣的技术了。这个思路确实有我们正在考虑的一些优点,但对Chromium益处不大,因为Chromium还实现了socket预连接。
ps: 后备工作作为一种解决TCP SYN重传超时过长的解决方案

socket预连接

我们可以预测性的预先建立socket连接,如果我们认为它们将会被用到。基本上,我们是简单的利用现有的Predictor组件(也用它来增强DNS预解析),简单的来说就是,当Predictor有了一个相当高的确信会发生的预测时,就会发起一个预连接。这样,当WebKit想要初始化资源请求时,socket连接可能已经开始甚至完成了。Mike证实,这可以降低7%-9%的网页加载时间(results in huge (7-9%) improvements in PLT in his synthetic tests)。后来,当我们重构SSL连接以利用延后绑定socket池组件。我们输出了一个通用的预连接接口,可以让我们预热所有类型的连接(执行TCP握手、HTTP连接以及SSL握手,还可以同时预热多个连接。我们的Predictor让我们可以基于以往的浏览进行预测,给定一个网络拓扑,我们想要的最大并发事务的数量是多少?利用通用socket池组件来完成一个socket连接.
ps:预连接,如果处理好,能够大大改善平均网页加载时间。

改善连接重用

改善代码以便更好的利用可用连接的同时,我们也花时间研究如何提升连接的重用性,以及优化重用连接的选择。

socket应该打开多久

我们研究的提升连接重用性的主要机制是调整连接的空闲时间(ps: 空闲时间短,一个打开的连接就会早点关闭,反之亦然)。这个值是比较敏感的。太长,很可能一些中间件或原始服务器关闭了连接,而且可能发送也可能不发送一个TCP FIN(或者在来的路上),结果,浪费了一次尝试,使用该连接仅仅是花费一个RTT等到一个TCP RST。太短,不利于连接的重用。socket延后绑定的世界更复杂,因为有时候资源请求会被取消,socket池中的预连接处于空闲状态得不到使用。前面我们提到,一个socket只有在重用间隔期才会是空闲的,而现在一个socket可能在第一次被使用前就处于空闲状态。已经证明中间件和原始服务器对于未用连接和使用过的连接采用不同的空闲超时(最大连接维持时间,keep-alive时间)。此外,对于未使用过的连接,我们得更保守,因为可能在第一次使用时就得到一个TCP RST,很多时候,RST表示服务器/网络出了问题,而不是连接空闲超时。考虑到这一点,我们收集了sockets的使用类型,以及socket在使用/重用前的空闲时间的统计图,以更好的引导我们选择合适的socket timeouts.

上图显示了我们在使用或重用socket之前的等待时间。注意Chromium目前的超时设置是:未用过的sockets 10-20秒,使用过的sockets 6分钟。

ps: 由上图可知,我们大部分时候是在等待一个已使用过的socket空闲下来

我们总共应该打开多少个socket

除了尝试优化对单个socket的重用,我们也调查看是否可以通过增加可用的socket数量以更好的满足潜在的重用。最初,我们没有限制socket的数量,IE也是这么做,但后来我们发现这样做会有问题(broken third party software),最终我们不得不修改我们的规则,降低最大的socket数量限制。我们知道,这会降低sockets的重用性,影响到性能,但不这样做,一些用户会崩溃,我们也不知道还有什么更好的方法。最后,因为最大socket限制导致了很多的bug,我们又修正了我们的策略,做出决定,第三方应该修正他们的代码,并把我们的最大限制提升到256,这在可预见的将来应该是足够了。

应该使用哪一空闲的socket

关于空闲sockets的重用我们应该记住一件事,不是所有的sockets都是平等的。如果sockets空闲了很久,很可能中间件/服务器已经终止了该链接,如果一些sockets已经读了很多数据,还可能内核已经发生了拥塞窗口超时,结果拥塞窗口变得很大。对HTTP堆栈进行贪婪优化的问题是不确定是否最终会改善用户的感知延迟。我们或多或少能够为HTTP堆栈内已知的HTTP事务做一些优化,但一毫秒之后,WebKit可能请求一个更高优先级的资源。我们可能为当前已知的最高优先级的HTTP事务分配“最热门”的socket,但是,如果随后一个更好优先级的HTTP事务立即到来,这就会是整体性能恶化。我们没有预测未来的水晶球,所以要做到最好的把空闲socket分配给HTTP事务是很困难的。实践出真知,我们还是决定通过实验来分析。不过很遗憾,实验结果是不确定的:“暂且不说结果,决定我调查的四个维度(Time to first byte, time from request to last byte, PLT,  and net error rates)的主要因素是那一天我正在看什么。我们没有看到任何明确是更好的方法,所以,为了简单起见,我们偏向于选择最近使用过的socket。


并行度多少合适

简单、天真的推理是并行越多越好,那为什么我们不尽可能多的并行化我们的HTTP事务呢?简而言之就是竞争。有各种不同的竞争,但主要是对带宽的竞争。更大的并行度可以带来更好的链路利用率,但常常在web浏览场景中引入了竞争,结果可能反而增加网页加载时间。给定一个网站,浏览器很难知道什么程度的并行化是最优的,因为这和我们不了解(在桌面端,我们能够大概估计,但在移动端,就很难这么做,因为波动很大)的网络特性(RTT,带宽等等)有关。最终,至少是现在,我们基本上是坚持为基于TCP的HTTP选择一个静态的并行值,所有用户使用同样的值。因为Chromium网络栈不了解网页浏览过程,所以要在每个主机的连接限制之外控制并行性历来很难。但现在我们正在开发我们自己的Chromium端ResourceScheduler,它可以在跨越多个渲染进程处理资源调度。我们的现场试验主要聚焦于studying connection per host limits。
ps: 并行度的问题在于应该与主机保持多少个连接合适?

探索单个主机的连接限制

为了找出对于真实网站的最有主机连接限制,我们在 live experiments做了 live experiments,实验了各种不同的值(4,5,6,7,8,9,16)。我们主要分析两个主要维度:Time to First Byte(TTFB)和page load time(PLT)。4和16;立即被否决了,因为它们的性能实在很糟。我们聚焦于实验流行的5-9。在这些选项中,5和9表现最糟糕,6-8表现相当。7和8在TTFB和PLT上几乎一样。8个连接比6个连接在TTFB上有0 - 2%的改善。在PLT方面,在90%的情况下,8连接比6连接有大约0.2% - 0.8%的改进,在另外20%的情况下, 8连接却比6连接退化了0.7 - 1.3%。总结我们的讨论(summary of our discussion ):”6是一个比较好的值。7个连接在有些情况下表现较好点,但还不足于让我们选择它。“

打开太多的TCP连接会导致拥塞

随着并行性的提高,我们看到另一个问题,TCP拥塞避免被绕过了,暂且不说竞争。正常情况下,TCP通过慢启动来避免拥塞,即使用一个保守的初始拥塞窗口(initcwnd),然后逐渐扩大。还要注意,一个客户端同时打开的连接越多,有效的initcwnd就越大。此外,较新的linux内核版本已经把初始拥塞窗口增加到10。这意味着,如果同时打开太多与服务器的连接,很可能立即引起拥塞和包丢失,反而增加了网页加载时间。我们前面提到,在实际访问一些主要的网站时,这的确会发生。我们不厌其烦的说到,解决方法是部署一个能够在单个连接上进行优先级多路复用的协议,例如SPDY(HTTP/2),以使网站不需要通过打开更多的TCP连接也可以获得更大的并行性。
移动连接管理
自从Android浏览器开始嵌入Chromium网络堆栈,核心网络团队已经帮助我们的移动团队对移动浏览器的网络做了很多调整。

减少不必要的广播使用

当我问Android browser团队如何处理这个问题时,他们告诉我在这方面他们也有很大压力,他们的处理方式是在单次使用后关闭所有的HTTP连接以试图节省更多电源。理由如下:
  • 在服务器规定的timeout之后,服务器将关闭连接,发送一个TCP FIN给Android 设备,唤醒radio来关闭socket。
  • 在Chromium空闲sockets超时后,我们关闭sockets,这会从Android设备发送一个FIN到服务器,这同样会唤醒radio来发送一个FIN。
  • 唤醒radio是代价高昂的,radio基于定时器来关闭电源,Souders对此做了详细解释(Souders explains in detail)。
这个建议的解决方案的问题是,在完成一个HTTP事务之后就关闭socket阻止了重用,减慢了网页加载,最终可能导致radio活动更久。我还不太清楚怎么样正确解决这个问题,但我有点怀疑使用一次就关闭的做法最终可能更耗电。为了减轻这个问题,我们能做的一件事情是只发送FIN不唤醒radio(not to ever wake up the radio just to send a FIN)。目前我们在大多数平台上就是这么做的。然而,这只是解决了客户端这边的问题。这是另一个SPDY能够帮助改善的例子之一。通过减少连接的数量,SPDY减少了服务器的FINs数量,从而减少了服务器FINs唤醒/激活客户端radio的次数。

结论

连接管理是Chromium网络堆栈的一部分,它对性能有巨大的影响,所以我们花了大量的时间来优化它。大部分的工作是重构初始的HTTP堆栈。在之前的版本中,所有HTTP事务彼此独立,使用一个串行状态机,事务只会在单个网络事件上阻塞。重构之后,HTTP堆栈变得更复杂,一个HTTP事务发起的网络事件可能会推进了另一个HTTP事务。所有的HTTP事务共享总体的上下文,共同处理不同层发起的工作,并按优先顺序执行请求。我们也花了很多时间来试验HTTP堆栈中不同的常量,试图找出对大多数用户来说最好的静态编译值。然而,我们知道我们挑选的任何单个的常量对很多用户/网络来说不是最优的(尽管对大多数用户是)。这是我们极力提倡SPDY的原因之一,它提供很多更好的语义,使我们不必过于依赖这些常量。通过利用SPDY的基于单个连接的优先级多路复用,我们可以鱼和熊掌兼得,要不然你不得不在链接利用/并行和竞争之间做出权衡,还得担心太多连接导致绕过TCP的拥塞避免。尽管我们已经在Chromium的网络堆栈领域花了相当多的时间,但仍有很多工作要做。我们真的是只涉及到移动优化的皮毛。就想Mike经常说的,SSL是未优化的前沿阵地。此外,涌现了像TCP快速打开这样的新技术,我们正在考虑如何合理利用 take advantage of a zero roundtrip connect。事情那么多,时间那么少。

原文:https://insouciant.org/tech/connection-management-in-chromium/
0 0