关于请求被挂起页面加载缓慢问题的追查

原文链接:原文链接

缘起

有用户反馈内部MIS系统慢,页面加载耗时长。前端同学们开组会提及此事,如何解决慢的问题。

最致命的是:偶发!你不能准确知道它抽风的时间点,无法在想要追查问题的时候必现它。 这只是一方面,另外,慢的可能实在太多了,那么问题来了,是前端导致的还是后端的问题?

对慢的定义也有待商榷,多久算慢?如果这个页面加载大量数据耗时增加那我认为这是正常的。但这个时限超过了一个合理的自然值,就变得不那么正常了,比如四五十秒,一分多钟。

最奇葩的是,如此久的耗时居然不会报超时错误,而是拿到正确返回后将页面呈现了出来!

可能的原因

初步猜测

初步的猜测可能是后端迟迟未返回造成浏览器处于等待状态。这个猜测是很合乎逻辑的,至少能够很合理地解释Chrome Dev Tool 网络面板中我们看到的状态pending。 

但我们不能停留在猜测阶段,要用事实说话,数据才不会骗人。这也正是本文将要展开的。

下面是另外一些被提出来的可能性。

Angular

Angular首当其冲。为什么,因为这个问题出现在后台MIS系统中,且这些系统多用Angular开发。

Angular :怪我咯。

因为问题多出现在基于Angular的MIS系统中,并且Angular的性能一直是被诟病的,所以听到不少的声音将矛头指向Angular。这似乎没什么好庇护的。Angular在整个项目中的前端部分扮演了很重的角色。树大招风,理所当然。

这让我想起初次接触到这个问题时,那是在七月份,芙蓉的爱马仕平台用户反馈了慢的问题,报到前端,顺便看了下,一看Pending状态,觉得是后端未返回。只是情深缘浅当时也没有深入,同时洪堂大神负责去追查了。当时那个系统,很负责地说,没有用Angular。

所以这里可以为Angular正身,将其排除。

内部封装的commonResource

内部对Angular原生的resource进行了封装,做了些数据的转换处理。既然上面Angular都被正身了,那么这里的怀疑也是站不住脚的。

Chrome插件

经查,网上好多呼声有说是Adblock等与网络有关的Chrome插件。可我不使用它已经很多年,那玩意儿太重,后来找到了算法更高级体量更轻便的µBlock。关键是后者也在我使用一段时间后放弃了,因为个人觉悟提高了(此处逼格开始膨胀),免费内容是需要广告支撑的,如果你不希望付费变成强制的话。所以现在一直是处于未开这类插件的状态。那么在未开广告屏蔽插件的情况下重现了问题,可以排除这类插件的影响了。

关于插件,此刻我的Chrome里唯一还会接管Chrome网络的便是代理插件SwitchSharp, 升级之后这货叫Switchy哦卖喝(与时俱进的我当然使用的是后者,此处逼格已经爆表)。

Chrome独家?

因为内部MIS只兼容了Chrome开发,所以不会有在除了Chrome之外的浏览器上使用的场景,并且其他浏览器上面追查问题也是很痛苦的事情。这里仅在火狐里进行了少量尝试,未复现。同时接到反馈,Safari里也未复现。但也不能肯定就只有Chrome存在问题。似乎这个对于问题的解决还不那么重要,所以先不管。

杀毒软件

后面会看到,在追查错误号ERR_CONNECTION_RESET时引出了杀毒软件可能会导致Chrome工作不正常的情况,但这个可能也在稍后被排除人。

并且,我厂使用Mac的同学并没有安装杀软,依然是可以复现的。

重现

第一件事情便是重现。虽然是偶发,为了尽可能保存现场,还是想要手动将它刷出来。天不灭我,经过良久尝试,该问题被复现。于是各种截图,保存请求数据。这个时候还没有开启chrome://net-internals/#events页面来捕获事件日志。

为以后引用方便,这里留下版本信息:

OS: Windows 7 Ultimate

Chrome:Version 39.0.2171.95 m

这是请求Pending时的请求信息: 

这是请求成功返回后: 

可以看到Stalled了1分多钟。神奇的是竟然不报超时错误而是成功返回了。

同时保存了请求头,响应头,还将本次问题请求保存成了CURL等。现场已经留下,感觉Bug不会存活太久了。

接下来就是对比正常请求跟这次异常请求的不同,一轮比较下来,未发现多少异常。

常态与变态的对比

请求头对比:

请求头的对比已丢失,但除了时间外,其余无差别。

响应头对比:

返回结果对比:

上面的对比意义不大,但还是要做的,万一发现有价值的情报了呢。

一次失败的尝试

解决问题时,习惯性地看有没有人已经碰过到类似问题,这样做的好处很明显: 如果有,站在巨人的肩上轻松地牛逼着; 如果没有,这是个机会。

于是信心满满地出发了,因为根据一条互联网准则,70%的问题已经有人解决过了,那些没有被解决的要么是现有技术达不到,要么是未被人发现。所以能够搜索出问题答案的概率还是蛮大的。

经过旷日持久的搜索,有价值的参考寥寥无几。可能是问题本身太过奇葩,遇到的人太少;也有可能问题过于晦涩,无法表述;抑或我搜索的关键词不够精准。 倒也不是说一个都没找到,但一般涉及网络日志的情况就无人问津了,无人问津了!

比如这个,一年多前被人问的,现在还没有一个回答。

还比如这个 > Chrome stalls when making multiple requests to same resource?

是后来作为参考的,也是无人问津了……

甚至自己也去问了一个,依然无人问津问津……

神秘的CACHE LOCK

上面提到,Stackoverflow上找到一个问题,跟现在需要解决一有些类似点:

  • 偶发,并不是必然出现的。这里我们的问题也是偶发,很难复现,需要反复刷。
  • 也是请求被Pending了很久,从请求的时间线来看,体现在Stalled上。

这一刻,有一种感觉大概是这样的:

伟大的意大利的左后卫!他继承了意大利的光荣的传统。法切蒂、卡布里尼、马尔蒂尼在这一刻灵魂附体!格罗索一个人他代表了意大利足球悠久的历史和传统,在这一刻他不是一个人在战斗,他不是一个人!

突然看到了希望。该提问到没有给出什么建设性的意见,但它后面的追加编辑却给出了答案。过程是查看Chrome的网络日志,在事件里面发现有一个超时错误:

t=33627 [st= 5] HTTP_CACHE_ADD_TO_ENTRY [dt=20001] –> net_error = -409 (ERR_CACHE_LOCK_TIMEOUT)

耗时20秒之久!而且写得非常明显是ERR_CACHE_LOCK_TIMEOUT。根据提问者贴出来的链接,了解到Chrome有一个缓存锁的机制。

具体源于一个今年6月分实现的一个补丁,加入了这么个机制,而这个机制的引入又源于2010年的一个issue。具体信息可以通过这个这里查看,下面引用如下。

Basically here is the situation:

The site author has a long-lived XHR being used to stream a slow response from the server. This XHR response is cachable (it is just really slow). They kick off the XHR asynchronously, and as data slowly arrives on it, update the progressive load of the webpage. Cool.

Now what happens if you try to load this page in multiple tabs of Chrome is: The first page starts to load just fine, but the second one does nothing. What has happened, is the background XHR of the first page load has acquired an exclusive lock to the cache entry, and the background XHR of the second page is stalled at “Waiting for cache…” trying to get a reader access to the cache entry.

Since the first request can takes minutes, this is a problem.

eroman 同学指出了这么一个事实:

浏览器对一个资源发起请求前,会先检查本地缓存,此时这个请求对该资源对应的缓存的读写是独占的。那么问题来了,试想一下,当我新开一个标签尝试访问同一个资源的时候,这次请求也会去读取这个缓存,假设之前那次请求很慢,耗时很久,那么后来这次请求因为无法获取对该缓存的操作权限就一直处于等待状态。这样很不科学。于是有人建议优化一下。也就是上面所描述的那样。

随着问题的提出,还出了两种可能的实现方案。

(a) [Flexible but complicated] Allow cache readers WHILE writing is in progress. This way the first request could still have exclusive access to the cache entry, but the second request could be streamed the results as they get written to the cache entry. The end result is the second page load would mirror the progress of the first one.

(a) [Naive but simpler] Have a timeout on how long we will block readers waiting for a cache entry before giving up and bypassing the cache.

我猜上面第二个(a)应该是(b)。简单说第一种优化方案更加复杂但科学。之前的请求对缓存仍然是独占的,但随着前一次请求不断对缓存进行更新,可以把已经更新的部分拿给后面的请求读取,这样就不会完全阻塞后面的请求了。

第二种方案则更加简单暴力。给后来的请求设定一个读取缓存超时的时限,如果超过了这个时限,我认为缓存不可用或者本地没有缓存,忽略这一步直接发请求。

于是Chromium的开发者们选择了后者简单的实现。也就是Issue 345643003: Http cache: Implement a timeout for the cache lock 这个提交里的实现。

这个提交的描述如下:

The cache has a single writer / multiple reader lock to avoid downloading the same resource n times. However, it is possible to block many tabs on the same resource, for instance behind an auth dialog.

This CL implements a 20 seconds timeout so that the scenario described in the bug results in multiple authentication dialogs (one per blocked tab) so the user can know what to do. It will also help with other cases when the single writer blocks for a long time.

The timeout is somewhat arbitrary but it should allow medium size resources to be downloaded before starting another request for the same item. The general solution of detecting progress and allow readers to start before the writer finishes should be implemented on another CL.

于是就产生了上面题主遇到的情况。

所以他的解决方法就很明朗了,对请求加个时间戳让其变得唯一,或者服务器响应头设置为无缓存。Both will work!

那么我们的问题也会是这样的么?我幻想由于某种未知的原因造成之前的请求不正常(虽然网络面板里没有数据证明这样的阻塞请求在问题请求之前存在),然后我们的MIS里打开页面时读取不到缓存,卡了,一会儿缓存好了,正常了,于是在等待了几十秒后请求成功发出去了。

似乎不太可能。因为恰好内部MIS系统的响应头里已经加了缓存控制了 Cache-Control: no-cache

以下是一次问题请求的响应头:

HTTP/1.1 200 OK
Date: Wed, 31 Dec 2014 11:47:21 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Pragma: no-cache
Cache-Control: no-cache
tracecode: 28410188240979065866123119
tracecode: 28410188240506537994123119
Server: Apache

并且开多个标签也是无法进行有效重现的。

因此可以排除缓存的干扰。那么似乎这里的缓存锁并不是导致问题的原因,只能另寻他路。不得不说,高兴过后有点失望。

八卦时间

可喜的是,在细细口味了上面缓存机制引入的过程后,真是耐人寻味。这里不妨八卦一下。相信你也注意到了,上面提到,该缓存问题的提出是在2010年,确切地说是Jun 8, 2010。是的,2010年6月8日由eroman 同学提出。但最后针对该问题进行修复的代码提交却是在今年6月份,2014年6月24日,提交时间摆在那里我会乱说?

于是好奇为什么会拖了这么久,遂跟了一下该问题下面的回复看看发生了什么。简直惊呆了。

  • 同月14号,有了首次对这个问题的回复,那是将该问题指派给了rvargas同学。

  • 一个月过去了,也就是7月15号,rvargas同学指出了与该问题关联的另外一个issue「issue 6697

  • 接下来是8月5日,rvargas同学为该问题贴上了标签-Mstone-7 Mstone-8 ,表明将会在里程碑7或者8里面进行修复。但在后面的10月7日,这个日程又被推到了-Mstone-8 Mstone-9 

  • 再接下来11月5日,有人表示以目前的速度及bug数量,还没有时间来修复它,重点在处理优先级为p1的问题上。于是此问题又成功被顺延了,来到-mstone-9 Mstone-10 ,同时优级降为p2。Chromium人手也不够啊,看来。

  • 时间来到12月9日,因为优先级为p2的issue如果没有被标为开始状态的话又自动推到下一个里程碑了,于是顺利来到 -Mstone-10 MovedFrom-10 Mstone-11 。次年2月来到-Mstone-11 Mstone-12 。完成了一次跨年!

…………

  • 上面省略N步。如此反复,最后一次被推到了-Mstone-16 ,那是在2011年10月12日。

  • 时间一晃来到2013年,这一年很平静,前面的几个月都没有人对此问题进行回复。直到11月27日,有人看不下去了,评论道:

This bug has been pushed to the next mstone forever…and is blocking more bugs (e.g https://code.google.com/p/chromium/issues/detail?id=31014)and use-cases same video in 2 tags on one page, and adaptive bit rate html5 video streaming whenever that will kick in. Any chance this will be prioritized?

由于这个bug的无限后延也阻塞了另外一些同类问题,看来是时候解决了。这不,最初的owner 当天就进行了回复:

ecently there was someone looking at giving it another try… I’d have to see if there was any progress there.

If not, I may try to fix it in Q1.

最后一句亮瞎。敢情这之前owner就没有想过要去真正解决似的,因为有其他人在看这个问题了,所以就没管了,如果Q1还没人解决的话,我会出手的!嗯,就是这个意思。

…………

最后,也就是上文提到的,2014年6月,还是rvargas同学对这个问题进行了修复,实现了对缓存读取20秒超时的控制。

该问题就是这样从2010来到2014的。我怀疑Chrome是如何成为版本帝的。

阶段总结

仅有的希望到此似乎都没有了。不过前面的努力也不是没有作何收获,至少我得到了以下有价值的信息:

  • 谷歌的神坛光环不再那么耀眼,他们的产品也是有大堆Bug需要处理的
  • Chrome 处理issue的效率,当然不排除这种大型项目bug数量跟人力完全不匹配的情况
  • 受上面Stackoverflow问题的启发,接下来我将重点转移到了针对出问题请求的日志分析上,并且取得了突破

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值