您的位置:首页 > 其它

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

2015-06-16 07:20 447 查看
缘起

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

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

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

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

  可能的原因

  初步猜测

  初步的猜测可能是后端迟迟未返回造成浏览器处于等待状态。这个猜测是很合乎逻辑的,至少能够很合理地解释Chrome Dev Tool 网络面板中我们看到的状态
pending
。 View Code
  首先,日志显示的总耗时与上面网络面板截图的总耗时是吻合的,都是42.74秒,说明我们定位正确。


以下时间均以毫秒计


  日志第一列为时间线,自请求发起时算。第二列为每步操作所逝去的时间,时间差的概念,与第三列里面的
dt
不同,它会积累前面的耗时。第三列为具体的事件,以及相应事件的耗时
dt
,此耗时为绝对耗时。

  
+
号对应事件开始,
-
号对应事件结束,也就是说他们必然成对出现。住里是展开后更加详细的子事件。直到不能再细分。

  如果说一开始接触到这个日志时手足无措的话,我们来看一下正常情况下的日志是怎样的,有对比才有发现。

  以下随便摘取一次正常请求的日志,如下:

View Code
  针对上面正常的请求,我们主要关注两部分,如下面的截图:

发送请求头
+HTTP_TRANSACTION_SEND_REQUEST [dt=1]


读取响应头
+HTTP_TRANSACTION_READ_HEADERS [dt=161]




  这是正常的情况下,没有什么问题。并且日志里可以清晰地看到发送的请求头是什么,然后解析出来的响应头是什么。这跟在网络面板看到的是一致的。

  再回到出问题的请求日志上来,同样我们只关注这两部分。如下面的截图:



  与正常相比,最后一次发送请求和读取响应头无异常,时间就多在了前面还有再次发送和请求的过程,细看时间都花在了以下两个事件中:

HTTP_STREAM_PARSER_READ_HEADERS [dt=21301]


HTTP_STREAM_PARSER_READ_HEADERS [dt=21304]


  该事件的名称已经自我解读,意思是解析读取的响应头。但问题是紧接着下面报错了,

--> net_error = -101 (ERR_CONNECTION_RESET)

  读取响应头时发生了链接重置的错误,有理由认为本次链接是不成功的,没拿到正确的响应头,于是解析不成功。时间都花在了这里,足足21秒之久,两个21秒造就了上面看到的
Stalled
了42秒之久。

  问题似乎已经很明朗了。链接被重置。

  在第三次尝试的时候正常了,于是正确返回,我们才看到了被解析的响应头被展示在了下面。也就是说在出问题的时候要么响应头未拿到,要么响应头非法导致解析不成功。而原因就是链接被重置。

  那么接下来的工作就是对
ERR_CONNECTION_RESET
这个错误的追查了。

  官方关于
ERR_CONNECTION_RESET
错误的解释


  未找到官方相应的资料,Chrome官网上唯一关于此错误的描述是在安装Chrome时出现Error 101。我估计文档的撰写人员没想到谁会这么蛋疼想要看这些生涩的东西,除了开发者。既然你都是开发者了,那为什么不去看Chromium的源码。

  好吧,唯一的途径似乎只能从源码中寻找了。作为只精JS的前端人员,现在要从C,C++代码中找答案了。估计追完这个问题,我会尝试为Chromium贡献代码。

  慢着,在这之前,还是搜到一些关于这个错误的信息的。但似乎都不怎么靠谱。

  比如这里提到,是因为ISP网络问题,实在无太可能。还有这是神马居然一个硬件网站但提到了这个错误,并且怀疑是杀软导致Chrome出问题,但杀软已经在上文被我们排除了。

  Chromium 源码

  那么这个错误究竟是什么。能不能找到点靠谱的解释。当然能,让我们进入到Chromium的源码中去。

  ERRCONNECTIONRESET被唤起的地方

  在Chromium的源码中搜索该常量名,确实出现很多结果。联系到我们查看日志发现问题的上下文,是在解析响应头报的。所以我们定位到
http_stream_parser.cc
文件,同时注意到有一个文件叫
net_errors_win.cc
,所以猜测他是定义所有错误常量用的,也顺便打开之。

  经过观察
src/net/base/net_errors_win.cc
其路径和代码得知其中多为系统级别的错误,似乎跟我们的问题不是很关联,忽略该文件。



  
http_stream_parser.cc
文件中,
ERR_CONNECTION_RESET
仅出现一次。这给我们定位带来了极大的便利。

  [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);
}


  这里定义了一个
ShouldTryReadingOnUploadError
的方法,注释耐人寻味,这个时候,这样的情景,能否正确解读注释成为了比读懂代码更重要(这是我在看JS代码时永远无法体味到的感觉),下面尽可能对它进行理解:


在尝试发送一个请求体的时候,让服务器尝试发送一个带错误的响应体,如果我们接收到了该错误则返回
true



  我承认被上面的复杂从句打败!

  那么我们来看这个方法被调用的场景。

  现在我们点击上面的
ShouldTryReadingOnUploadError
方法,代码下方出现调用了该方法的地方,一共有两处。



  分别点击进行查看。

  459行DoSendHeadersComplete方法里进行了调用:

int HttpStreamParser::DoSendHeadersComplete(int result) {
if (result < 0) {
// In the unlikely case that the headers and body were merged, all the
// the headers were sent, but not all of the body way, and |result| is
// an error that this should try reading after, stash the error for now and
// act like the request was successfully sent.
if (request_headers_->BytesConsumed() >= request_headers_length_ &&
ShouldTryReadingOnUploadError(result)) {
upload_error_ = result;
return OK;
}
return result;
}



虽然不太可能,但也不排除头部和请求体合并的情况,当所有头部发送完毕,请求体不一定,此时
result
便是需要稍后处理的一种错误,这里暂且先返回
OK



  516行另一个DoSendBodyComplete方法里进行了调用:

int HttpStreamParser::DoSendBodyComplete(int result) {
if (result < 0) {
// If |result| is an error that this should try reading after, stash the
// error for now and act like the request was successfully sent.
if (ShouldTryReadingOnUploadError(result)) {
upload_error_ = result;
return OK;
}
return result;
}



跟上面类似,如果
result
出错,稍后处理,先返回正常


  这也与我们在日志中看到的情况相符,在前面再次错误后,这次请求并没有终止结束,而是尝试到了第三次并且以成功结束的。

  但不管怎样,从这两个方法,一个
DoSendHeadersComplete
, 另一个
DoSendBodyComplete
,身上能体现出请求确实已经发出去。

  TCP RST

  另外,在
net_error_list.h
这个文件的109行,可以准确找到我们在日志中得到的101号错误。它的定义如下:

// A connection was reset (corresponding to a TCP RST).NET_ERROR(CONNECTION_RESET, -101)

  从括号中的进一步解释可以知道,它代表TCP连接重置。

  TCP

  那么问题来了,什么是TCP连接重置?什么会引发TCP连接重置。从这篇文章中有比较详细的解答。

  想要完全解释,本文似乎是不可能的了。但根据上面的文章,这里可以简单转述一下。

  什么是TCP连接

  它是一种协议。当网络上一个节点想与另一个节点通信时,双方需要选建立连接。而这个连接过程需要大家都懂的一种约定,TCP就是事先定好的一种约定,于是我们采用它吧,于是其中一个节点按照这个约定发起一建立连接的请求,另一节点收到后,根据该约定,便能读懂这个请求里各字段的意思:哦,丫这是想约我呢。

  三次握手

  继续上面的例子。A想与B通信,并且使用TCP。

  首先A发起一个报文,其中包含自己的地址,想要连接的目标地址,自己用来连接的端口及目标机器的端口,etc.

  B收到邀约,并且愿意付约。此刻B需要回传一个报文,告诉A我愿意跟你连接。

  A收到B的肯定应答,到此A与B经历了三次通信或者说是握手,双方都没有异议,连接建立。

  而连接断开的过程也颇为类似。双方中的一方比如说A先发起一个断开连接的报文FIN,B收到并确认,然后回传一个可以断开的报文FIN给A。此刻A收到并确认。此刻双方都确认后,连接可以安全断开,但还会保持一个等待断开的状态,大概持续4分钟,用于之前连接通路上未传输完成的数据进行善后。

  什么是重置

  上面提到了4分钟的等待时间,而重置RESET便是立即断开连接的手段。

  发生重置的情况

  到此重置的作用已然明了。也就是说,重置甚至算不上一个错误,它是TCP连接中的一种正常情况。但什么时候会发生重置,如何引起的。

  上文列出了三种情况。

  SMB Reset

  简单举例来说,服务器提供了两个端口445,139进行服务,客户端同时去请求与这两个端口连接,服务器返回了两个端口可以被连接,此刻客户端择优选择一个进行连接,而重置另一个。

  Ack, Reset

  报文重置发生主要有以下情况:- 服务器没有监听被请求的端口,无法建立连接- 服务器此刻无法比如没有充裕的资源用来连接连接

  TCP Reset due to no response

  由于没有响应而被重置。当发起连接的一方连续发送6次请求未得到回应,此刻默认他们之间已经通过三次握手建立了连接并且通信有问题,发起的一方将连接重置。

  Application Reset

  除了上面的情况,找不到TCP内部自己发送的重置,则归为了这一类。程序内将连接重置。此种情况包含了所有你想得到想不到将连接断开的情况。有可能是程序内部逻辑重置的,所以不能完全认为此时发生了错误。

  值得注意的是,上面列出的情况服务器的不确定性导致连接重置的可能性要合理些。Chrome 主动发起URL请求不太可能自己又重置掉,并且没有理由重置掉后又去重连。

  进一步解读日志文件

  上面Chromium源码部分的求证多少带有猜测成分。不妥。

  因为没找到关于Chrome net-internal 日志的官方文档什么的,自身去解读始终是有局限的。不如提个ISSUE让Chromium开发人员来搭一把手吧。遂向Chromium提交ISSUE,请戳此查看,虽然我不认为现在遇到的这个问题跟Chrome有关并且属于Chrome的Bug,目的仅仅是看他们能否帮忙给出合理的日志解读来定位问题。

  三天后(有点热泪盈眶),有同学回复,将日志所体现的问题诊断得似乎很有道理,可信。


1) We have a bunch of connections to qa.tieba.baidu.com, all were used successfully, and are now idle.2) They all silently die for some reason, without us ever being informed. My guess is your personal router times out the connection, but this could also be your ISP, the destination server, or ever a real network outage (A short one) that prevents us from getting the connection closed message.3) There's a new request to qa.tieba.baidu.com. We try to reuse a socket. After 21 seconds, we get the server's RST message ("I don't have a connection to you."). Since it was a stale socket, we're aware this sometimes happens, so we just retry...And get the next idle socket in the list, which, after 21 seconds, gives us the same reset message. We try again, for the same reason. This time we don't have another stale socket to try, so we use a fresh one. The request succeeds.

The real problem here is something is taking 21 seconds to send us the RST messages, despite the fact that a roundtrip to the server you're talking to only takes about 100 milliseconds.



「之前有过很多成功的连接」,确实,因为出现加载缓慢的情况是偶发的,这之前有过很多正常的不卡的请求存在过。这里没有异议。

「他们都以未知的原因被断掉了」,因为不是正常地断开连接,所以客户端也就是浏览器不知道当前与服务器的TCP连接已经断开,傻傻地保留着与服务器连接的socket,注意,此时已经发生信息的不对等了,这是问题的根源。至于什么原因,给出了可能的原因:路由器认为连接超时将其断掉,同时不排除ISP(互联网服务提供商)的原因,服务器暂时的停运抽风等。不管怎样,客户端浏览器没有收到连接断开的信息。

在上面的基础上,我们去发起一次新的请求。此时浏览器希望重用之前的连接以节省资源,用之前的一个socket去发起连接。21秒后收到服务器返回的重置信息(意思是服务器告诉浏览器:我和你之间没有连接),没关系,上面提到,我们有很多可以重用的连接,于是浏览器重新从可用的连接里面又选择了一个去进行连接,不幸的是,同样的情况再次发生,21秒后收到服务器的重置信息。这体现在日志上就是第二次重试失败。到第三次,因为前面浏览器认为可以重用的连接现在都被正确地标为断开了,没有新的可用,于是这次浏览器发起了全新的请求,成功了!

  总结出来,两个问题:

为什么之前成功的连接不正常的断开了?服务器配置或者网络原因?

是什么让浏览器21秒后才收到重置信息?服务器作出反应过慢还是网络原因?

  Chrome Dev Tool 中时间线各阶段代表的意义

  另附注一下Chrome Dev Tool 中请求的时间线各阶段代表的意义。以下内容扒自Chrome 开发者文档页,然后我将它本地化了一下下。



  Stalled/Blocking

  在请求能够被发出去前的等等时间。包含了用于处理代理的时间。另外,如果有已经建立好的连接,那么这个时间还包括等待已建立连接被复用的时间,这个遵循Chrome对同一源最大6个TCP连接的规则。

  「拿我们的情况来说,上面出错所有的耗时也是算在了这部分里面。网络面板中显示的其余时间比如DNS查找,连接建立等都是属于最后那次成功请求的了」

  Proxy Negotiation

  处理代理的时间。

  DNS Lookup

  查找DNS的时间。页面上每个新的域都需要一次完整的寻路来完成DNS查找。

  Initial Connection / Connecting

  用于建立链接的时间,包括TCP握手及多次尝试握手,还有处理SSL。

  SSL

  完成SSL握手的时间。

  Request Sent / Sending

  发起请求的时间,通常小到可以忽略。

  Waiting (TTFB)

  等待响应的时间,具体来说是等待返回首个字节的时间。包含了与服务器之间一个来回响应的时间和等待首个字节被返回的时间。

  Content Download / Downloading

  用于下载响应的时间

  结论

  我相信很多同学是直接跳到这里来了的。事实上我给不出什么解决方案,但能排除前端代码引起问题的可能性。

  具体来说,能够得到的结论有以下几点:

请求成功构造,失败情况下也可以看到正常的请求头被打印出来了的

可以肯定的是在与服务器建立连接时被Shut down了,参考上面关于连接重置的部分会更有意义一些

参考上面「进一步解读日志文件」部分,来自Chromium开发者的回复中对日志文件的解读更加合理些,浏览器与服务器的连接不正常断开是导致问题的根源,以至于影响了后面对连接的重用

21秒的等待仍然是个未知数,不知道有何不可抗拒的外力促使浏览器21秒后才收到服务器的重置信息。此处浏览器与服务器的失联原因有待查证

  最后寄希望于RD同学跟进,协助排查服务器连接及后端代码的部分。FE同学会保持持续关注。

  参考及引用

  #1 Chrome stalls when making multiple requests to same resource?

  #2 What does “pending” mean for request in Chrome Developer Window?

  #3 Evaluating network performance / Resource network timing

  #4 Provisional headers are shown

  #5 “CAUTION: provisional headers are shown” in Chrome debugger

  #6 Chrome 里的请求报错 "CAUTION: Provisional headers are shown" 是什么意思?

  #7 Issue 345643003: Http cache: Implement a timeout for the cache lock

  #8 Issue 46104: Pages can get blocked in "Waiting for Cache" for a very long time

  #9 Providing Network Details for bug reports

  #10 从FE的角度上再看输入url后都发生了什么

  #11 ERRCONNECTIONRESET 的Chromium 源码

  #12 Chromium Network Stack

  #13 Where do resets come from? (No, the stork does not bring them.)

  #14 Issue 447463: Chrome-network: Long delay before RST message on stale sockets results in slow page loads)

原文地址:http://kb.cnblogs.com/page/513237/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: