41 關於請求被掛起頁面加載緩慢問題的追查

緣起html

  有用戶反饋內部MIS系統慢,頁面加載耗時長。前端同窗們開組會說起此事,如何解決慢的問題。前端

  最致命的是:偶發!你不能準確知道它抽風的時間點,沒法在想要追查問題的時候必現它。這只是一方面,另外,慢的可能實在太多了,那麼問題來了,是前端致使的仍是後端的問題?html5

  對慢的定義也有待商榷,多久算慢?若是這個頁面加載大量數據耗時增長那我認爲這是正常的。但這個時限超過了一個合理的天然值,就變得不那麼正常了,好比四五十秒,一分多鐘。git

  最奇葩的是,如此久的耗時竟然不會報超時錯誤,而是拿到正確返回後將頁面呈現了出來!github

  可能的緣由web

  初步猜想ajax

  初步的猜想多是後端遲遲未返回形成瀏覽器處於等待狀態。這個猜想是很合乎邏輯的,至少可以很合理地解釋Chrome Dev Tool 網絡面板中咱們看到的狀態pending算法

  但咱們不能停留在猜想階段,要用事實說話,數據纔不會騙人。這也正是本文將要展開的。chrome

  下面是另一些被提出來的可能性。json

  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] HTTPCACHEADDTOENTRY [dt=20001] --> neterror = -409 (ERRCACHELOCKTIMEOUT)

  耗時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.ghttps://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問題的啓發,接下來我將重點轉移到了針對出問題請求的日誌分析上,而且取得了突破

  開始新的征程

  雖然上面的努力沒能定位到問題,但做爲此次對解決此次問題的嘗試,仍是將它記錄了下來,估且稱做「舊的回憶」吧。

  下面開始「新的征程」。

  再次重現

  此次受到上面的啓發,開啓chrome://net-internals/#events頁面來捕獲事件日誌。看是否有錯誤或異常發生。

  再次通過曠日持久的機械操做,重現了!此次,日誌在手,天下我有。感受Bug不會存活多久了。

  Chrome Dev Tools 網絡面板截圖:

  由上面的截圖看到,本次出問題的請求總耗時42.74秒。

  問題請求的時間線信息截圖:

  能夠預見,經過捕獲的日誌徹底能夠看到Stalled那麼久都發生了些什麼鬼。

  話很少說,切換到事件捕獲頁面,定位到出問題的請求,查看其詳情。同時將該日誌導出,永久保存!做爲記念,也方便之後再次導入查看。有興趣的同窗能夠訪問下方下載後進行導入,就能夠清晰地查看到現場了,就好像你親歷了整個犯罪現場同樣。

  日誌還原

  • 下載該日誌文件

  • 在Chrome新開一個標籤輸入chrome://net-internals/#events

  • 切換到Import,選擇剛纔下載的JSON文件進行導入

  • 切換到Events,定位到http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593 這個請求

  此刻右邊出現的即是該問題請求的詳細日誌。

  日誌解讀

  下面不妨把日誌文件貼出來先:

  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/

相關文章
相關標籤/搜索