ASR專案實戰-交付過程中遇到的疑似記憶體洩漏問題

jackieathome發表於2024-01-03

基於Kaldi實現語音識別時,需要引入一款名為OpenFST的開源軟體,本文中提到的記憶體問題,即和這款軟體相關。
考慮到過程比較曲折,內容相對比較長,因此先說結論。

在做長時間的語音識別時,整合了Kaldi和OpenFST的程式將會佔用遠超出預期的記憶體,這個現象可能和OpenFST、glibc的實現相關,未必是記憶體洩漏。

程式佔用超出大量記憶體的原因,簡單說一下:

  • OpenFST在工作過程中,申請了很多記憶體,同時產生了很多記憶體碎片。
  • 語音識別程式預設使用的glibc無法合併相關的碎片,因而即便相關的記憶體已經被釋放,但glibc仍然無法向作業系統釋放記憶體。
  • 因此,在使用top觀察程式的虛擬記憶體時,發現程式佔用的記憶體會時間增長而一直增長,進而會被判定為疑似記憶體洩漏。

當然了,經過分析後,現在可以確認前述現象為非問題,只需要調整機器規格即可解決問題,但如前所述,整個過程比較曲折,這裡記錄下來,以備後察。

測試同事反饋,在效能環境上,執行壓測過程中,演演算法服務出現了重啟的現象。這是一個大問題,於是在第一時間聯絡我進行定位。

觀察測試同事的壓測環境,發現確實如測試同事所說,壓測開始後,演演算法服務佔用的虛擬記憶體以肉眼可見的速度緩慢增長。透過作業系統的硬體資源監控平臺,觀察程式一段時間內虛擬記憶體的佔用趨勢,發現沒有進入平穩狀態的跡象。最終觀察的結果是演演算法服務佔用的虛擬記憶體一直在增長,最終隨著程式異常退出而結束。

我們的演演算法服務由業務程式碼、演演算法推斷程式碼和機器學習模型組成。

  • 業務程式碼使用Java開發,編譯、構建成jar檔案,執行時由JVM載入並執行。
  • 演演算法推斷程式碼使用C++開發,基於JNA規範,Java程式碼暴露介面,編譯、構建成動態庫,執行時由JVM載入。
  • 機器學習模型,其實是幾個資料檔案,執行時由演演算法推斷程式碼讀取並使用。

考慮到當前版本中,演演算法推斷程式碼和資料模型並沒有引入新的變動點,因此重啟現象的定位工作從演演算法服務的業務程式碼入手。

檢查業務流程

首先分析業務流程。
本版本引入了長語音檔案轉寫的特性,因此業務程式碼有比較大的變動。前期在實現時,為了簡化實現方案,在檔案轉寫的過程中,記憶體裡快取了很多資料。透過分析這部分實現,沒有發現物件生命週期超長的現象,但仍然做了改進,將記憶體中快取的資料交給資料庫來快取。
這時在開發環境中復現操作,觀察記憶體增長的曲線,發現增長趨勢有所減緩,但演演算法服務佔用的虛擬記憶體,仍然在漲,沒有收斂的跡象,因此仍然需要繼續分析。

檢查JVM配置

演演算法服務使用的Java堆的引數中,堆的最大值,比較大。本質上講,經過上一環節的最佳化後,演演算法服務的業務程式碼中不涉及大量Java物件的生成,因此執行時,Java堆可以使用較少的記憶體。
修改演演算法服務Java堆的引數後,在開發環境中復現操作,基本功能正常。此外,使用jstat -gcutil <pid> 1000 1000觀察,確認JVM的GC操作執行正常,未發現異常現象。
長時間觀察記憶體增長的曲線,發現沒有明顯改進,演演算法服務佔用的虛擬記憶體,仍然在漲,沒有收斂的跡象,因此仍然需要繼續分析。

分析Java堆記憶體

記憶體問題分析到現在, 光靠看程式碼已經不解決問題,是時候召喚專業工具上場了。
對於Java應用的記憶體,jmapMAT是一對完美的組合。
執行如下命令,匯出Java應用程式的堆。

jmap -dump:live,format=b,file=dump001.bin <pid>

為了方便對比分析,一般至少需要匯出四次堆。

  • Java應用程式啟動完畢。匯出的堆檔案命名為dump001.bin
  • 壓力測試持續一段時間之後。假如可以準確的控制執行的壓力測試的用例數量,則可以使用用例數量來衡量。匯出的堆檔案命名為dump002.bin
  • 在上次匯出操作後,壓力測試的TPS保持穩定,繼續持續一段時間或者執行完畢一部分用例之後,再提取一次堆。匯出的堆檔案命名為dump003.bin
  • 停止壓力測試,等待一段時間,此時再提取一次堆。匯出的堆檔案命名為dump004.bin

將上述匯出的三個檔案,dump001.bindump002.bindump003.bin一起匯入至MAT。MAT基於eclipse開發,在配置檔案中指定了Java堆的最小值和最大值,可以視堆檔案的大小,酌情修改MAT的JVM引數。
使用MAT的histogram功能,對這三個檔案進行對比。

  • 對比dump001.bindump002.bin,可以確認業務啟動後,堆中出現的Java物件的型別和數量。結合業務用例和程式碼,可以確認物件的型別和數量,是否符合預期。
  • 對比dump002.bindump003.bin,可以確認業務執行平穩後,堆中出現的Java物件的型別和數量,是否穩定。假如壓力測試的TPS保持穩定,則從理論上講,Java堆中出現、湮滅的物件的數量應當是穩定的,物件的數量不會有太大的變化。
  • 對比dump003.bindump004.bin,確認Java堆中業務相關的物件的型別和數量,是否有較大的下降。一般而言,執行過程中的Java物件,應當在壓力測試結束後,在JVM的垃圾回收操作中被回收掉,不應存在大量的殘留。
  • 對比dump001.bindump004.bin,由於壓力測試已經結束,Java堆中物件的型別和數量,二者之間的差異應當比較小。

使用jmap命令,匯出堆,使用MAT分析。
反覆撥測業務,使用jstat命令觀察GC情況。
修改程式碼的實現,降低記憶體佔用。

問題仍然存在。
演演算法同事參與分析,使用valgrind分析,memcheck和massif,未發現記憶體洩漏點。
使用pmap觀察,Java程式的記憶體空間,發現很多64MB的塊。在網上找到很多文章。

縮小變數的值
關閉執行緒分配器,均無效

使用tcmalloc分配器,記憶體仍然會漲,並且偶發性的程式異常退出,因此本方案不能在生產環境使用。

最終,定期呼叫malloc_trim,定期向作業系統釋放記憶體。

總結

無法更新GPU驅動的版本,流程操作比較複雜,時間和技術上均不允許。

參考資料

Kaldi

glibc

JVM

valgrind

malloc

pmap

相關文章