.Net程式記憶體洩漏解析

dotNet源計劃發表於2021-04-20

一、概要

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

二、場景

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

“目前服務出現了提交記憶體洩漏的問題,目前分析出來可能是日誌元件有大量的日誌訊息堆積把記憶體佔滿導致服務崩潰了。在國內某地區客戶的伺服器上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裡,最可怕的是從佇列裡取資料的還是單執行緒處理。

  • (2)還會有很多磁碟I/O的操作會儲存在應用伺服器本機上例如socket通訊的報文和需要轉發的內容等等都會進行寫入操作。
  • (3)逐步除錯的時候發現大部分的方法實現都是同步方法,而且框架版本居然是.net freamwork4。

解決方案:

(1)

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

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

(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,彩蛋

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

相關文章