天天看點

.Net程式記憶體異常解析

一、概要

大概在今年三月份的時候突然被緊急調到另外一個項目組解決線上記憶體異常問題。經過兩周的玩命奮戰終于解決了這個問題這裡把心路曆程及思路分享給大家。希望可以幫助到各位或現在正遇到這樣事情的小夥伴提供一些思路。

二、場景

當部門老大找到我的時候,給我描述了這樣一段話。

“目前服務出現了送出記憶體異常的問題,目前分析出來可能是日志元件有大量的日志消息堆積把記憶體占滿導緻服務崩潰了。在國内某地區客戶的伺服器上15000台物聯網裝置不能正常工作這個問題非常緊急需要馬上解決。”

問題描述至此,沒有其他可用資訊。這時候我先崩潰了...但是任務找到你不能說不行。萬一解決了這種重大事故還能在部門老大面前秀一把。

三、思路

(1)分析

Part1,分析日志堆積原因

  1. 拿到伺服器位址去翻出日志檔案,檢視日志内容;内容基本上都是一些報錯情況xxx對象為null,對象轉換失敗。
  2. 日志元件的實作也比較糟糕Log對象在每個調用的類裡都會重新new

解決方案:

  1. 修複對象為null的問題并加上空值判斷,大概的原因就是json值轉換的時候傳入的值是null那麼就引起這兩塊的連鎖反應。非常值得注意的一點是通常json對象轉換的地方都會加入try塊去捕獲異常在程式裡try的捕捉是會對.net程式造成性能影響的是以能用判斷規避的盡量不要去觸發try機制,程式性能被拖下去其他方面的處理就會變相的削減處理速度變慢那麼資料堆積好像就解釋的通了。
  2. 将日志元件重構為單例且線程安全的實作,寫入日志的資料結構體是class這裡改成struct,考慮的因素是引用類型會存在引用問題再就是考慮的值類型和引用類型在記憶體中占用的大小是不一樣的,而且值類型和引用類型在處理速度上值類型更快。

以為這樣就結束了嗎?不,當程式改好之後放在測試伺服器上跑第二天早上測試部的小姐姐就找到我說異常報錯情況是好了,但是記憶體洩漏還是沒解決。

Part2,查找記憶體洩漏的根本原因

看來Part1的操作僅僅隻是修複了一個小bug而已,并不是我所想的那麼簡單,在日志的檢視中還發現log日志中出現“tcp服務拒絕連接配接XXX異常”。當我看到這些的時候心情糟糕透了....

1.一早我就用Profile把服務程式跑了一遍發現了

  • (1)有幾個消息隊列占用非常大,查閱代碼之後發現服務端程式會和15000台物聯網裝置進行互動的所有資料都會先堆積到這個隊列裡如果這個隊列滿了(Queue上限被設定2w)會new新的Queue然後把溢出的部分轉到新的Queue裡,最可怕的是從隊列裡取資料的還是單線程處理。
.Net程式記憶體異常解析
  • (2)還會有很多磁盤I/O的操作會存儲在應用伺服器本機上例如socket通訊的封包和需要轉發的内容等等都會進行寫入操作。
  • (3)逐漸調試的時候發現大部分的方法實作都是同步方法,而且架構版本居然是.net freamwork4。

(1)

  • 【移除new新隊列的機制、删除Main Queue的上限設定改為多線程處理Queue;一切資料堆積的本質就是資料處理不過來是以開辟再多的記憶體空間都是慢性死亡而已。】
  • 【走訪物聯網硬體部門,詢問物聯網裝置發送資料頻率、裝置數、單台裝置發送單條資料的大小是多少KB;為什麼需要了解?這些第一點在程式内記錄日志然後統計成走勢圖能直接觀察隊列内部的變化開會的時候能給上司具有說服力的證據能看到資料量什麼時候陡增、資料大小等;第二點因為這些封包資料需要存在應用伺服器本地那麼這時候就能計算出寫入的資料量有沒有超出普通硬碟的寫入I/O瓶頸以及網絡帶寬的占用。】
  • 【走訪物聯網硬體部門2,詢問物聯網裝置socket傳輸資料時是否有走正常“tcp揮手”流程;為什麼?因為socket tcp通訊中,是雙工通道那麼其中有一端突然斷開,另一端會進入“wait”狀态不會及時回收tcp連接配接資源,大家試想一下如果15000台裝置高頻短連接配接去操作那麼服務端連接配接隊列資源很有可能吃不消。這個時候就需要服務端主動斷開“失效”連接配接及時回收資源“拆除雙工通道”以及調整socket連接配接隊列大小。】
.Net程式記憶體異常解析

(2)磁盤寫入封包資訊這塊,就要用三寸不爛之舌說動項目經理把這塊砍掉以節約CPU性能以及減少磁盤I/O,大夥試想一下每次socket通訊進行收發的時候都要去操作一下I/O那是多麼恐怖的一件事情;最後溝通結果那個組的項目經理同意砍掉部分子產品磁盤寫入功能,那麼問題來了剩下的怎麼辦如何将優勢進一步擴大?這時候繼續查閱項目代碼,結果發現socket通訊中“收”、“發”都會操作一次。那麼這時候需要做的是将封包積累到一定數量比如說積累1000條封包再一次性寫入那麼磁盤I/O的操作頻率将成倍遞減。

.Net程式記憶體異常解析

(3)最後一個問題,就是講所有的方法修改為異步方法。這時候就能祭出Task、Async、Await了。但是基于的架構是.net freamwork4的,後來又去查閱MSDN的文檔發現.net freamwork4遠古架構中還是有這些特性的雖然用法稍微難受點但是還是能優化的。一定要記住一點,開發服務端要有“服務端”思維如果都是同步方法就會被同步阻塞處于“等待處理結果狀态”這樣的話服務端的并發量是上不去的。

這裡雖然沒怎麼用上的一發大招,但是這裡還是分享給大家“注釋大法”;注釋掉最有可能出問題的地方逐一排查一定能發現問題的所在就是非常的耗時那會我基本每天工作12小時,尤其是公司的遠古項目通常“代碼爛”、“設計基本沒有”、“使用的.net架構版本低”等等,一堆惡心人的事情發生。

(2)工具

  • Visual Studio自帶的Profile。【可以分析CPU、記憶體等占用情況;這款比較推薦】
  • VMMap【可以分析CPU、記憶體等占用情況】
  • ANTS Performance Profiler【這款工具比較強大能分析調用鍊路逐級告訴你記憶體占用的地方以及記憶體占用大小】
  • Window作業系統自帶的資源螢幕這個不用多說大家都會用。

Part3,總結

基于以上的修改,在測試伺服器上穩定運作3周記憶體穩定在2.9G左右;

一定要記住:

  • “遇到任何棘手的事情不要抱怨。”
  • “一個優秀的軟體工程招聘進來就是解決問題的,而不是制造問題;”
  • “對于任務的安排,高手永遠都是說出解決問題的期限;到點交東西。而不是支支吾吾說不清楚、退縮。”
  • “遇到問題冷靜思考,相信自己一定可以的;那怕失敗去嘗試一下也好。”
  • “沒解決問題的時候不要說任何話,說什麼都像是在找理由。閉上嘴巴去想辦法。”

其實解決這個問題時期發生了很多有趣的故事,不過最終還是要解決難啃的問題證明自己,開發學習本身就是一個不斷變強的過程“修技術,也修内心”當自己逐漸變強之後也不要鄙視技術不好的同僚始終保持一顆學徒的心。

Part4,彩蛋

解決這個問題之後在同部門同僚的眼裡威望都會有提升(尤其是測試部門的小姐姐,因為她們不用費力的每天去看伺服器了),最終解決項目的重大事故部門老大給了機會調到其他省的研發中心當項目經理薪資平移的基礎上再上浮百分之十。可見掌握一手救急的技能有多麼劃算。

繼續閱讀