缘起 有用户反馈内部MIS系统慢,页面加载耗时长。前端同学们开组会提及此事,如何解决慢的问题。 最致命的是:偶发!你不能准确知道它抽风的时间点,无法在想要追查问题的时候必现它。这只是一方面,另外,慢的可能实在太多了,那么问题来了,是前端导致的还是后端的问题? 对慢的定义也有待商榷,多久算慢?如果这个页面加载大量数据耗时增加那我认为这是正常的。但这个时限超过了一个合理的自然值,就变得不那么正常了,比如四五十秒,一分多钟。 最奇葩的是,如此久的耗时居然不会报超时错误,而是拿到正确返回后将页面呈现了出来! 可能的原因 初步猜测 初步的猜测可能是后端迟迟未返回造成浏览器处于等待状态。这个猜测是很合乎逻辑的,至少能够很合理地解释Chrome Dev Tool 网络面板中我们看到的状态 但我们不能停留在猜测阶段,要用事实说话,数据才不会骗人。这也正是本文将要展开的。 下面是另外一些被提出来的可能性。 Angular Angular首当其冲。为什么,因为这个问题出现在后台MIS系统中,且这些系统多用Angular开发。 Angular :怪我咯。 因为问题多出现在基于Angular的MIS系统中,并且Angular的性能一直是被诟病的,所以听到不少的声音将矛头指向Angular。这似乎没什么好庇护的。Angular在整个项目中的前端部分扮演了很重的角色。树大招风,理所当然。 这让我想起初次接触到这个问题时,那是在七月份,芙蓉的爱马仕平台用户反馈了慢的问题,报到前端,顺便看了下,一看 所以这里可以为Angular正身,将其排除。 内部封装的 内部对Angular原生的 Chrome插件 经查,网上好多呼声有说是Adblock等与网络有关的Chrome插件。可我不使用它已经很多年,那玩意儿太重,后来找到了算法更高级体量更轻便的µBlock。关键是后者也在我使用一段时间后放弃了,因为个人觉悟提高了(此处逼格开始膨胀),免费内容是需要广告支撑的,如果你不希望付费变成强制的话。所以现在一直是处于未开这类插件的状态。那么在未开广告屏蔽插件的情况下重现了问题,可以排除这类插件的影响了。 关于插件,此刻我的Chrome里唯一还会接管Chrome网络的便是代理插件SwitchSharp, 升级之后这货叫Switchy哦卖喝(与时俱进的我当然使用的是后者,此处逼格已经爆表)。 Chrome独家? 因为内部MIS只兼容了Chrome开发,所以不会有在除了Chrome之外的浏览器上使用的场景,并且其他浏览器上面追查问题也是很痛苦的事情。这里仅在火狐里进行了少量尝试,未复现。同时接到反馈,Safari里也未复现。但也不能肯定就只有Chrome存在问题。似乎这个对于问题的解决还不那么重要,所以先不管。 杀毒软件 后面会看到,在追查错误号 并且,我厂使用Mac的同学并没有安装杀软,依然是可以复现的。 重现 第一件事情便是重现。虽然是偶发,为了尽可能保存现场,还是想要手动将它刷出来。天不灭我,经过良久尝试,该问题被复现。于是各种截图,保存请求数据。这个时候还没有开启 为以后引用方便,这里留下版本信息:
这是请求 这是请求成功返回后: 可以看到 同时保存了请求头,响应头,还将本次问题请求保存成了CURL等。现场已经留下,感觉Bug不会存活太久了。 接下来就是对比正常请求跟这次异常请求的不同,一轮比较下来,未发现多少异常。 常态与变态的对比 请求头对比: 请求头的对比已丢失,但除了时间外,其余无差别。 响应头对比: 返回结果对比: 上面的对比意义不大,但还是要做的,万一发现有价值的情报了呢。 一次失败的尝试 解决问题时,习惯性地看有没有人已经碰过到类似问题,这样做的好处很明显:如果有,站在巨人的肩上轻松地牛逼着;如果没有,这是个机会。 于是信心满满地出发了,因为根据一条互联网准则,70%的问题已经有人解决过了,那些没有被解决的要么是现有技术达不到,要么是未被人发现。所以能够搜索出问题答案的概率还是蛮大的。 经过旷日持久的搜索,有价值的参考寥寥无几。可能是问题本身太过奇葩,遇到的人太少;也有可能问题过于晦涩,无法表述;抑或我搜索的关键词不够精准。倒也不是说一个都没找到,但一般涉及网络日志的情况就无人问津了,无人问津了! 比如这个,一年多前被人问的,现在还没有一个回答。 还比如这个
是后来作为参考的,也是无人问津了…… 甚至自己也去问了一个,依然无人问津问津…… 神秘的CACHE LOCK 上面提到,Stackoverflow上找到一个问题,跟现在需要解决一有些类似点:
这一刻,有一种感觉大概是这样的:
突然看到了希望。该提问到没有给出什么建设性的意见,但它后面的追加编辑却给出了答案。过程是查看Chrome的网络日志,在事件里面发现有一个超时错误:
耗时20秒之久!而且写得非常明显是 具体源于一个今年6月分实现的一个补丁,加入了这么个机制,而这个机制的引入又源于2010年的一个issue。具体信息可以通过这个这里查看,下面引用如下。
eroman 同学指出了这么一个事实: 浏览器对一个资源发起请求前,会先检查本地缓存,此时这个请求对该资源对应的缓存的读写是独占的。那么问题来了,试想一下,当我新开一个标签尝试访问同一个资源的时候,这次请求也会去读取这个缓存,假设之前那次请求很慢,耗时很久,那么后来这次请求因为无法获取对该缓存的操作权限就一直处于等待状态。这样很不科学。于是有人建议优化一下。也就是上面所描述的那样。 随着问题的提出,还出了两种可能的实现方案。
我猜上面第二个 第二种方案则更加简单暴力。给后来的请求设定一个读取缓存超时的时限,如果超过了这个时限,我认为缓存不可用或者本地没有缓存,忽略这一步直接发请求。 于是Chromium的开发者们选择了后者简单的实现。也就是Issue 345643003: Http cache: Implement a timeout for the cache lock 这个提交里的实现。 这个提交的描述如下:
于是就产生了上面题主遇到的情况。 所以他的解决方法就很明朗了,对请求加个时间戳让其变得唯一,或者服务器响应头设置为无缓存。Both will work! 那么我们的问题也会是这样的么?我幻想由于某种未知的原因造成之前的请求不正常(虽然网络面板里没有数据证明这样的阻塞请求在问题请求之前存在),然后我们的MIS里打开页面时读取不到缓存,卡了,一会儿缓存好了,正常了,于是在等待了几十秒后请求成功发出去了。 似乎不太可能。因为恰好内部MIS系统的响应头里已经加了缓存控制了 以下是一次问题请求的响应头: 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年,确切地说是 于是好奇为什么会拖了这么久,遂跟了一下该问题下面的回复看看发生了什么。简直惊呆了。
…………
由于这个bug的无限后延也阻塞了另外一些同类问题,看来是时候解决了。这不,最初的owner 当天就进行了回复:
最后一句亮瞎。敢情这之前owner就没有想过要去真正解决似的,因为有其他人在看这个问题了,所以就没管了,如果Q1还没人解决的话,我会出手的!嗯,就是这个意思。 ………… 最后,也就是上文提到的,2014年6月,还是rvargas同学对这个问题进行了修复,实现了对缓存读取20秒超时的控制。 该问题就是这样从2010来到2014的。我怀疑Chrome是如何成为版本帝的。 阶段总结 仅有的希望到此似乎都没有了。不过前面的努力也不是没有作何收获,至少我得到了以下有价值的信息:
开始新的征程 虽然上面的努力没能定位到问题,但作为这次对解决这次问题的尝试,还是将它记录了下来,估且称作「旧的回忆」吧。 下面开始「新的征程」。 再次重现 这次受到上面的启发,开启 再次经过旷日持久的机械操作,重现了!这次,日志在手,天下我有。感觉Bug不会存活多久了。 Chrome Dev Tools 网络面板截图: 由上面的截图看到,本次出问题的请求总耗时42.74秒。 问题请求的时间线信息截图: 可以预见,通过捕获的日志完全可以看到 话不多说,切换到事件捕获页面,定位到出问题的请求,查看其详情。同时将该日志导出,永久保存!作为纪念,也方便以后再次导入查看。有兴趣的同学可以访问下方下载后进行导入,就可以清晰地查看到现场了,就好像你亲历了整个犯罪现场一样。 日志还原
此刻右边出现的便是该问题请求的详细日志。 日志解读 下面不妨把日志文件贴出来先: ![]() 193486: URL_REQUEST http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593 Start Time: 2015-01-02 17:51:05.323 t= 1 [st= 0] +REQUEST_ALIVE [dt=42741] t= 1 [st= 0] URL_REQUEST_DELEGATE [dt=0] t= 1 [st= 0] +URL_REQUEST_START_JOB [dt=42740] --> load_flags = 339804160 (BYPASS_DATA_REDUCTION_PROXY | MAYBE_USER_GESTURE | REPORT_RAW_HEADERS | VERIFY_EV_CERT) --> method = "GET" --> priority = "LOW" --> url = "http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593" t= 2 [st= 1] URL_REQUEST_DELEGATE [dt=0] t= 2 [st= 1] HTTP_CACHE_GET_BACKEND [dt=0] t= 2 [st= 1] HTTP_CACHE_OPEN_ENTRY [dt=0] t= 2 [st= 1] HTTP_CACHE_ADD_TO_ENTRY [dt=0] t= 2 [st= 1] HTTP_CACHE_READ_INFO [dt=0] t= 2 [st= 1] URL_REQUEST_DELEGATE [dt=0] t= 2 [st= 1] +HTTP_STREAM_REQUEST [dt=2] t= 4 [st= 3] HTTP_STREAM_REQUEST_BOUND_TO_JOB --> source_dependency = 193488 (HTTP_STREAM_JOB) t= 4 [st= 3] -HTTP_STREAM_REQUEST t= 4 [st= 3] +HTTP_TRANSACTION_SEND_REQUEST [dt=0] t= 4 [st= 3] HTTP_TRANSACTION_SEND_REQUEST_HEADERS --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1 Host: qa.tieba.baidu.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 Referer: http://qa.tieba.baidu.com/project/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Cookie: [268 bytes were stripped] t= 4 [st= 3] -HTTP_TRANSACTION_SEND_REQUEST t= 4 [st= 3] +HTTP_TRANSACTION_READ_HEADERS [dt=21301] t= 4 [st= 3] HTTP_STREAM_PARSER_READ_HEADERS [dt=21301] --> net_error = -101 (ERR_CONNECTION_RESET) t=21305 [st=21304] HTTP_TRANSACTION_RESTART_AFTER_ERROR --> net_error = -101 (ERR_CONNECTION_RESET) t=21305 [st=21304] -HTTP_TRANSACTION_READ_HEADERS t=21305 [st=21304] +HTTP_STREAM_REQUEST [dt=3] t=21307 [st=21306] HTTP_STREAM_REQUEST_BOUND_TO_JOB --> source_dependency = 193494 (HTTP_STREAM_JOB) t=21308 [st=21307] -HTTP_STREAM_REQUEST t=21308 [st=21307] +HTTP_TRANSACTION_SEND_REQUEST [dt=3] t=21308 [st=21307] HTTP_TRANSACTION_SEND_REQUEST_HEADERS --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1 Host: qa.tieba.baidu.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 Referer: http://qa.tieba.baidu.com/project/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Cookie: [268 bytes were stripped] t=21311 [st=21310] -HTTP_TRANSACTION_SEND_REQUEST t=21311 [st=21310] +HTTP_TRANSACTION_READ_HEADERS [dt=21304] t=21311 [st=21310] HTTP_STREAM_PARSER_READ_HEADERS [dt=21304] --> net_error = -101 (ERR_CONNECTION_RESET) t=42615 [st=42614] HTTP_TRANSACTION_RESTART_AFTER_ERROR --> net_error = -101 (ERR_CONNECTION_RESET) t=42615 [st=42614] -HTTP_TRANSACTION_READ_HEADERS t=42615 [st=42614] +HTTP_STREAM_REQUEST [dt=12] t=42627 [st=42626] HTTP_STREAM_REQUEST_BOUND_TO_JOB --> source_dependency = 193498 (HTTP_STREAM_JOB) t=42627 [st=42626] -HTTP_STREAM_REQUEST t=42627 [st=42626] +HTTP_TRANSACTION_SEND_REQUEST [dt=2] t=42627 [st=42626] HTTP_TRANSACTION_SEND_REQUEST_HEADERS --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1 Host: qa.tieba.baidu.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 Referer: http://qa.tieba.baidu.com/project/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Cookie: [268 bytes were stripped] t=42629 [st=42628] -HTTP_TRANSACTION_SEND_REQUEST t=42629 [st=42628] +HTTP_TRANSACTION_READ_HEADERS [dt=112] t=42629 [st=42628] HTTP_STREAM_PARSER_READ_HEADERS [dt=112] t=42741 [st=42740] HTTP_TRANSACTION_READ_RESPONSE_HEADERS --> HTTP/1.1 200 OK Date: Fri, 02 Jan 2015 09:51:48 GMT Content-Type: application/json; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Cache-Control: no-cache tracecode: 31079600320335034634010217 tracecode: 31079600320537995786010217 Server: Apache t=42741 [st=42740] -HTTP_TRANSACTION_READ_HEADERS t=42741 [st=42740] HTTP_CACHE_WRITE_INFO [dt=0] t=42741 [st=42740] HTTP_CACHE_WRITE_DATA [dt=0] t=42741 [st=42740] HTTP_CACHE_WRITE_INFO [dt=0] t=42741 [st=42740] URL_REQUEST_DELEGATE [dt=0] t=42741 [st=42740] -URL_REQUEST_START_JOB t=42741 [st=42740] URL_REQUEST_DELEGATE [dt=0] t=42741 [st=42740] HTTP_TRANSACTION_READ_BODY [dt=0] t=42741 [st=42740] HTTP_CACHE_WRITE_DATA [dt=0] t=42741 [st=42740] HTTP_TRANSACTION_READ_BODY [dt=0] t=42741 [st=42740] HTTP_CACHE_WRITE_DATA [dt=0] t=42742 [st=42741] -REQUEST_ALIVE 首先,日志显示的总耗时与上面网络面板截图的总耗时是吻合的,都是42.74秒,说明我们定位正确。
日志第一列为时间线,自请求发起时算。第二列为每步操作所逝去的时间,时间差的概念,与第三列里面的 如果说一开始接触到这个日志时手足无措的话,我们来看一下正常情况下的日志是怎样的,有对比才有发现。 以下随便摘取一次正常请求的日志,如下: ![]() 384462: URL_REQUEST http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593 Start Time: 2015-01-03 20:23:54.698 t=1556 [st= 0] +REQUEST_ALIVE [dt=172] t=1556 [st= 0] URL_REQUEST_DELEGATE [dt=0] t=1556 [st= 0] +URL_REQUEST_START_JOB [dt=171] --> load_flags = 335609856 (BYPASS_DATA_REDUCTION_PROXY | MAYBE_USER_GESTURE | VERIFY_EV_CERT) --> method = "GET" --> priority = "LOW" --> url = "http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593" t=1557 [st= 1] +URL_REQUEST_DELEGATE [dt=4] t=1557 [st= 1] DELEGATE_INFO [dt=4] --> delegate_info = "extension Tampermonkey" t=1561 [st= 5] -URL_REQUEST_DELEGATE t=1561 [st= 5] HTTP_CACHE_GET_BACKEND [dt=0] t=1561 [st= 5] HTTP_CACHE_OPEN_ENTRY [dt=1] --> net_error = -2 (ERR_FAILED) t=1562 [st= 6] HTTP_CACHE_CREATE_ENTRY [dt=0] t=1562 [st= 6] HTTP_CACHE_ADD_TO_ENTRY [dt=0] t=1562 [st= 6] URL_REQUEST_DELEGATE [dt=0] t=1562 [st= 6] +HTTP_STREAM_REQUEST [dt=2] t=1564 [st= 8] HTTP_STREAM_REQUEST_BOUND_TO_JOB --> source_dependency = 384467 (HTTP_STREAM_JOB) t=1564 [st= 8] -HTTP_STREAM_REQUEST t=1564 [st= 8] +HTTP_TRANSACTION_SEND_REQUEST [dt=1] t=1564 [st= 8] HTTP_TRANSACTION_SEND_REQUEST_HEADERS --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1 Host: qa.tieba.baidu.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 Referer: http://qa.tieba.baidu.com/project/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Cookie: [2642 bytes were stripped] t=1565 [st= 9] -HTTP_TRANSACTION_SEND_REQUEST t=1565 [st= 9] +HTTP_TRANSACTION_READ_HEADERS [dt=161] t=1565 [st= 9] HTTP_STREAM_PARSER_READ_HEADERS [dt=160] t=1725 [st=169] HTTP_TRANSACTION_READ_RESPONSE_HEADERS --> HTTP/1.1 200 OK Date: Sat, 03 Jan 2015 12:23:54 GMT Content-Type: application/json; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Cache-Control: no-cache tracecode: 14346880480340800522010320 tracecode: 14346880480253893130010320 Server: Apache t=1726 [st=170] -HTTP_TRANSACTION_READ_HEADERS t=1726 [st=170] HTTP_CACHE_WRITE_INFO [dt=0] t=1726 [st=170] HTTP_CACHE_WRITE_DATA [dt=0] t=1726 [st=170] HTTP_CACHE_WRITE_INFO [dt=0] t=1726 [st=170] +URL_REQUEST_DELEGATE [dt=1] t=1726 [st=170] DELEGATE_INFO [dt=1] --> delegate_info = "extension Tampermonkey" t=1727 [st=171] -URL_REQUEST_DELEGATE t=1727 [st=171] -URL_REQUEST_START_JOB t=1727 [st=171] URL_REQUEST_DELEGATE [dt=0] t=1727 [st=171] HTTP_TRANSACTION_READ_BODY [dt=0] t=1727 [st=171] HTTP_CACHE_WRITE_DATA [dt=1] t=1728 [st=172] HTTP_TRANSACTION_READ_BODY [dt=0] t=1728 [st=172] HTTP_CACHE_WRITE_DATA [dt=0] t=1728 [st=172] -REQUEST_ALIVE 针对上面正常的请求,我们主要关注两部分,如下面的截图:
这是正常的情况下,没有什么问题。并且日志里可以清晰地看到发送的请求头是什么,然后解析出来的响应头是什么。这跟在网络面板看到的是一致的。 再回到出问题的请求日志上来,同样我们只关注这两部分。如下面的截图: 与正常相比,最后一次发送请求和读取响应头无异常,时间就多在了前面还有再次发送和请求的过程,细看时间都花在了以下两个事件中:
该事件的名称已经自我解读,意思是解析读取的响应头。但问题是紧接着下面报错了,
读取响应头时发生了链接重置的错误,有理由认为本次链接是不成功的,没拿到正确的响应头,于是解析不成功。时间都花在了这里,足足21秒之久,两个21秒造就了上面看到的 问题似乎已经很明朗了。链接被重置。 在第三次尝试的时候正常了,于是正确返回,我们才看到了被解析的响应头被展示在了下面。也就是说在出问题的时候要么响应头未拿到,要么响应头非法导致解析不成功。而原因就是链接被重置。 那么接下来的工作就是对 官方关于 未找到官方相应的资料,Chrome官网上唯一关于此错误的描述是在安装Chrome时出现Error 101。我估计文档的撰写人员没想到谁会这么蛋疼想要看这些生涩的东西,除了开发者。既然你都是开发者了,那为什么不去看Chromium的源码。 好吧,唯一的途径似乎只能从源码中寻找了。作为只精JS的前端人员,现在要从C,C++代码中找答案了。估计追完这个问题,我会尝试为Chromium贡献代码。 慢着,在这之前,还是搜到一些关于这个错误的信息的。但似乎都不怎么靠谱。 比如这里提到,是因为ISP网络问题,实在无太可能。还有这是神马居然一个硬件网站但提到了这个错误,并且怀疑是杀软导致Chrome出问题,但杀软已经在上文被我们排除了。 Chromium 源码 那么这个错误究竟是什么。能不能找到点靠谱的解释。当然能,让我们进入到Chromium的源码中去。 ERRCONNECTIONRESET被唤起的地方 在Chromium的源码中搜索该常量名,确实出现很多结果。联系到我们查看日志发现问题的上下文,是在解析响应头报的。所以我们定位到 经过观察 [chromium]//src/net/base/neterrorswin.cc: // Returns true if |error_code| is an error for which we give the server a // chance to send a body containing error information, if the error was received // while trying to upload a request body. bool ShouldTryReadingOnUploadError(int error_code) { return (error_code == ERR_CONNECTION_RESET); } 这里定义了一个
我承认被上面的复杂从句打败! 那么我们来看这个方法被调用的场景。 现在我们点击上面的 分别点击进行查看。 459行DoSendHea |