記一次Vue全頁面SSR深坑之旅 - 微弱的內存/CPU泄漏

若是我跟你說,我面試來這家的時候,面試題就是這個問題你會做何感想?估計通常人是不會進坑的。然而,我進來了。由於我以爲這種技術問題很好玩。僅此而已。不然工做會很無聊。前端

前言

  • 其實你沒啥必要解決這個bug,由於國內不少公司每週一個版本,因此壓根兒就察覺不到這個bug的存在。
  • 其實你大可沒必要解決這個bug,由於你寫一個定時自動重啓腳本,在一個夜深人靜的夜晚默默執行重啓之。
  • 其實你不用非得解決這個bug,由於百度也開始支持spa系統seo,你還在那裏累死累活搞蹩腳的ssr幹嗎。

若是你像我同樣以爲無聊,那麼就往下看吧。對不對不知道。反正在我本地測試,大部分的問題都已經KO了。vue

緣起一次換工做

面試的時候面試官提出這樣一個問題,他們的系統出現了一個奇怪的現象,基於Vue的SSR系統出現了CPU緩慢上升,不得不隔段時間重啓一次。問我解決思路是什麼?node

嗯?CPU上升?是否有內存泄漏?是否每一個請求都返回了?是否有阻塞的IO操做?若是是Express是否都執行了返回?緩慢上升,是什麼樣的幅度?QPS是多少?服務器負載是否合理?ios

而後我順利的拿到了Offer,入職後給個人第二個任務就是解決這個技術問題。看到這裏是否是以爲我被套路了?哈哈哈哈,可是我就是喜歡這種挑戰。很好玩,不然工做會很無聊。不過對於這種技術調查很難短期出現成果物,對於我也是很危險的一件事情。並且,嗯。。。。。。也有卸磨殺驢的可能。誰知道呢。反正這是一件很好玩的事情。管他呢。git

問題是否真的如描述那樣?

在解決一個技術難題的時候,咱們每每獲得的是遇到問題的人描述的表現,而實際問題的表現並不必定如描述者所說。程序員

遇到性能問題,咱們要充分了解問題的本質是什麼?僅僅是CPU緩慢上漲?現代的SPA框架都有嚴重消耗CPU的問題,是否是服務器集羣能力不足?是否伴隨內存泄漏?是否有掛起的請求沒有返回?這些疑問在個人腦海翻來覆去。github

直到我看到系統,看到源碼,登上了服務器,看到了各類服務器監控數據的時候,好傢伙。有點意思,讓我越加亢奮。面試

問題:ajax

  • CPU週期性上升,偶有降低,可是整體趨勢是上升。週期在2周左右到達80%以上的佔用率。
  • 內存天天會有一小部分的泄漏,很是少。也會有釋放。整體趨勢是天天在500M左右。
  • 天天訪問量在有活動的時候會有大範圍波動,可是總體比較平穩,不過日誌系統只保留最近7天日誌,形成從日誌分析緣由有點困難。出問題的那幾天數據已經沒了。
  • 後端系統,在代碼層面,若是沒有重大代碼邏輯問題,代碼優化帶來的性能提高是有限的。

第一個彎路

光從訪問日誌和描述者描述問題來看,在CPU居高不下的那幾天剛好有訪問高峯。並且從訪問量下降的時候CPU使用率也是有明顯下降的。因而根據那幾天CPU高峯時段的用戶流量來判斷應該是服務器負載不足,沒有頂住流量高峯。 因而拿着這個調查結果去找Leader。Leader也接受了。畢竟從數據層面是說的通的。並且在這面諮詢了運維同事,他們也以爲是這樣。並且當時確實有一個很大的流量高峯持續了幾個小時。vue-router

可是,在接下來的幾天觀察發現,流量沒有那麼巨大的時候,依然會有緩慢的上升趨勢,只是比流量高峯時段上漲的慢一些。所以第一次的調查結果宣佈不對。

第二個彎路

根據經驗分析形成CPU緩慢上漲而不能明顯降低現象大可能是由於有代碼片斷被掛起,沒法釋放。對於Nodejs來講無非就是幾種:1 setTimeout,2 阻塞IO,3 express沒調用res.end()結束請求。

開始作代碼code review,發現整個項目都是基於官方vue-hackernews2.0來構建的。從代碼上面問題不大。那麼多是阻塞IO?

因而找運維同窗get到如何查看活動網絡連接,對本地環境進行壓測。而後中止半小時之後查看連接狀況(由於操做系統爲了優化io使用並不會在你操做結束後立刻釋放連接,因此要等待一會)。

壓測後結果非常震驚,因爲測試環境後端接口性能極差,致使超多請求被掛起。而這個時候被阻塞的socket連接也很是多,內存飆升,CPU一直沒有明顯降低。哈哈哈問題找到了(高興太早了)。

因而去找運維協商是否有手段在服務器上設置斷開長期無響應的連接。運維很無奈。。。。。。

好吧。還得本身來。爲何會掛起這麼多連接?查閱資料,發現有這麼一個現象存在:在服務器超載的狀況下,因爲沒法作出響應,客戶端的socket就會被掛起一直處於connection狀態。

我去問了項目開發負責人,說他們設置了超時處理,並不會引發這種情況。。。。。。

可是我在log日誌明明看到了不少200s以上才返回的請求。。。。。。說明咱們代碼設置的超時並無起做用。因而我須要找到足夠的證據來講服他。

有時候咱們在溝通的時候,對方並不信任你觀點,實際上是源於你的證據不充分,那麼這個時候,你就須要找到具備足夠說服力的證據來證實你的觀點。

因而深挖Nodejs文檔,跟項目代碼,發現axios的這塊實現有問題:

if (config.timeout) {
      timer = setTimeout(function handleRequestTimeout() {
        req.abort();
        reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req));
      }
    }
複製代碼

這裏代碼看起來是沒任何問題的,這是在前端處理中一個很典型的超時處理解決方式。

因爲Nodejs中,io的連接會阻塞timer處理,所以這個setTimeout並不會按時觸發,也就有了10s以上才返回的狀況。

貌似問題解決了,巨大的流量和阻塞的connection致使請求堆積,服務器處理不過來,CPU也就下不來了。

在Nodejs官方文檔中提到:

If req.abort() is called before the connection succeeds, the following events will be emitted in the following order:

- socket
- (req.abort() called here)
- abort
- close
- error with an error with message Error: socket hang up and code ECONNRESET
複製代碼

因而我給axios提了PR,解決辦法就是利用socket中對於connect的超時處理來代替會在Nodejs中被阻塞的setTimeout來處理超時請求。這個問題在node-request中也存在。並且通過本地大量測試,發如今高負載下CPU和內存都在正常範圍內了。覺得一切都OK了。

if (config.timeout) {
      // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
      // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
      // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
      // And then these socket which be hang up will devoring CPU little by little.
      // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.
      req.setTimeout(config.timeout, function handleRequestTimeout() {
        req.abort();
        reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req));	
      }
    }
複製代碼

然而。。。。。。我又錯了。

有一天我忘記了關電腦,本地壓測的環境還在跑,次日驚奇的發現,全部被掛起的socket資源都被釋放了。可是內存,CPU依然沒有被回收。關於這一點我求證了運維同事,確實操做系統會自動處理掉這些長時間不活動的連接。雖然我經過修改axios源碼的方式解決了問題,可是貌似問題的本質緣由並無找對。

一次偶然發現vue-router中的「騷」處理

實在沒有頭緒,廢了幾天勁貌似都沒有抓到問題的根本緣由,雖然誤打誤撞解決了問題。可是這種解決問題方式對因而否可以根除問題會有必定的不肯定性。

利用inspect反覆的去分析系統的內存,因爲線上流量很是巨大,可是內存和CPU的泄漏很小,而本地難以復現這麼大的訪問量,因此本地復現很是難,加上JS的GC方式,在調查上難度很大。只能一個請求一個請求後反覆對比內存鏡像查找哪怕一絲絲線索。

而對於CPU,那更是難以跟蹤,線上天天CPU增加在每小時0.02左右。也就意味着平均一次請求對於CPU泄漏的影響微乎其微,而一旦進行大規模的請求測試,對於內存的跟蹤就不許確了。

可能這個時候就是年齡大的程序員的優點了,能夠沉得住氣,耐得住性子去查找問題的。有的時候解決一個技術問題並不須要你有多麼強的技術,解決問題的方式,以及耐心纔是主要的。

在一次偶然發現,發起一個請求後,內存鏡像中老是會出現一個timer。而後下一次抓取內存鏡像又釋放了一個timer。What the fxxk?什麼鬼。

而這個timer卻沒有什麼明顯信息去告訴我是在哪裏被建立的。再一次陷入崩潰。

難道這就是那個形成內存泄漏的根源?timer佔用資源很是小,並且是異步,並不會阻塞系統,因此並不會像死循環那樣致使CPU長期處於高位運行。貌似,這個timer纔是問題的根源。

好在Nodejs的全部api接口都是js實現的,因而直接在setTimeout裏面打斷點跟蹤代碼。。。。。。果真是大力出奇跡。發現了vue-router中的騷操做

function poll ( cb, // somehow flow cannot infer this is a function instances, key, isValid ) {
  if (instances[key]) {
    cb(instances[key]);
  } else if (isValid()) {
    setTimeout(function () {
      console.log('vue-router poll');
      poll(cb, instances, key, isValid);
    }, 16);
  }
}
複製代碼

是的,沒錯,這是一個死循環的timer。instances是什麼?經過代碼應該是對應的異步組件實例,而key是對應的組件在實例數組中的鍵值。而退出條件只有2個:1 異步組件加載完成,2 路由發生改變。

可是在ssr的場景下,路由發生改變在每個請求的過程當中是不會發生的。所以退出條件就只剩下了異步組件加載完成。可是處於某種緣由,它沒加載成功。致使這個timer就陷入了死循環。並且前提是須要在組件裏面實現了beforeRouteEnter這個守衛函數。

因爲vue-router代碼的實現太騷了。只能求助萬能的github。發現了這個issue

和個人狀況徹底吻合。可是對於member的回覆有一些心寒。經過題主的簡單設置已經能夠完美的復現問題了。團隊卻直接以「A boiled down repro instead of a whole app would help to identify the problem, thanks」爲由給close了。。。。。。

而更加可氣的是:

> A boiled down repro instead of a whole app would help to identify the problem, thanks

if you have an infinite loop, it's probably next not being called without arguments  《= 覺得咱們都是傻子嗎?不知道調next?
複製代碼

好吧。看來既然上了賊船就只能靠本身了。我和題主溝通後開始嘗試解決問題。可是通過幾天努力題主已經放棄了。而我。。。。。。也選擇了放棄(別把我看那麼高大上,說實話,看了幾天vue-router源碼。真的沒有找到好的解決辦法,主要是會修改不少東西。)。

解決方案

在vue-ssr中形成內存和cpu泄漏的緣由目前我所調查的結果就是這麼兩個緣由:

  1. 掛起的socket形成暫時性的堵塞
  2. vue-router中的timer在某些狀況下會陷入死循環
  3. 大量的模板編譯,內存中會存留大量被字符串佔用的內存

那麼如何解決呢?

  • 移除component中對於beforeRouteEnter的處理。將這裏的處理移到其餘地方,從vue-router代碼層面分析是能夠避免陷入timer的死循環的。
  • 在nodejs中替換掉setTimeout的方式去處理服務器端請求超時,改用http.request的timeout事件handle來處理。防止io阻塞timer處理。
  • 若是不是對seo要求太高,採用骨架頁渲染的方式,向客戶端渲染出骨架頁,而後由前端直接發起ajax請求拉取服務器數據。避免在nodejs端執行服務端請求因爲服務端後臺沒法響應形成堵塞致使部分連接被掛起。(nodejs的事件循環和瀏覽器是不一樣的,雖然都是基於V8引擎。這也是大部分國內互聯網公司在vue-ssr這塊的廣泛應用方式)

也許還有

我對vue-ssr只研究了2周,若是以上有疑問歡迎及時提醒我進行改正。

相關文章
相關標籤/搜索