基於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應用的記憶體,jmap和MAT是一對完美的組合。
執行如下命令,匯出Java應用程式的堆。
jmap -dump:live,format=b,file=dump001.bin <pid>
為了方便對比分析,一般至少需要匯出四次堆。
- Java應用程式啟動完畢。匯出的堆檔案命名為
dump001.bin
。 - 壓力測試持續一段時間之後。假如可以準確的控制執行的壓力測試的用例數量,則可以使用用例數量來衡量。匯出的堆檔案命名為
dump002.bin
。 - 在上次匯出操作後,壓力測試的TPS保持穩定,繼續持續一段時間或者執行完畢一部分用例之後,再提取一次堆。匯出的堆檔案命名為
dump003.bin
。 - 停止壓力測試,等待一段時間,此時再提取一次堆。匯出的堆檔案命名為
dump004.bin
。
將上述匯出的三個檔案,dump001.bin
、dump002.bin
、dump003.bin
一起匯入至MAT。MAT基於eclipse開發,在配置檔案中指定了Java堆的最小值和最大值,可以視堆檔案的大小,酌情修改MAT的JVM引數。
使用MAT的histogram功能,對這三個檔案進行對比。
- 對比
dump001.bin
和dump002.bin
,可以確認業務啟動後,堆中出現的Java物件的型別和數量。結合業務用例和程式碼,可以確認物件的型別和數量,是否符合預期。 - 對比
dump002.bin
、dump003.bin
,可以確認業務執行平穩後,堆中出現的Java物件的型別和數量,是否穩定。假如壓力測試的TPS保持穩定,則從理論上講,Java堆中出現、湮滅的物件的數量應當是穩定的,物件的數量不會有太大的變化。 - 對比
dump003.bin
和dump004.bin
,確認Java堆中業務相關的物件的型別和數量,是否有較大的下降。一般而言,執行過程中的Java物件,應當在壓力測試結束後,在JVM的垃圾回收操作中被回收掉,不應存在大量的殘留。 - 對比
dump001.bin
和dump004.bin
,由於壓力測試已經結束,Java堆中物件的型別和數量,二者之間的差異應當比較小。
使用jmap命令,匯出堆,使用MAT分析。
反覆撥測業務,使用jstat命令觀察GC情況。
修改程式碼的實現,降低記憶體佔用。
問題仍然存在。
演算法同事參與分析,使用valgrind分析,memcheck和massif,未發現記憶體洩漏點。
使用pmap觀察,Java程式的記憶體空間,發現很多64MB的塊。在網上找到很多文章。
縮小變數的值
關閉執行緒分配器,均無效
使用tcmalloc分配器,記憶體仍然會漲,並且偶發性的程式異常退出,因此本方案不能在生產環境使用。
最終,定期呼叫malloc_trim,定期向作業系統釋放記憶體。
總結
無法更新GPU驅動的版本,流程操作比較複雜,時間和技術上均不允許。
參考資料
Kaldi
glibc
JVM
valgrind
- valgrind massif記憶體分析
- 透過Valgrind的Massif工具進行C++記憶體使用分析
- valgrind-memcheck功能的使用和分析
- 施昌權--淘寶衛霍
- 如何使用Valgrind memcheck工具進行C/C++的記憶體洩漏檢測
- How to Detect Memory Leaks Using Valgrind memcheck Tool for C / C++
malloc
- TCMalloc原理
- 圖解 TCMalloc
- 記憶體最佳化總結:ptmalloc、tcmalloc和jemalloc
- glibc記憶體管理ptmalloc底層實現
- ptmalloc總結
- 使用 malloc_trim()
- malloc_trim
- 幾個有用的 malloc 環境變數
- Malloc Tunable Parameters