tensorflow中一個環(huán)境變量引發(fā)的內(nèi)存泄漏血案

起因

最近信息流推薦的業(yè)務方在使用tensorflow進行分布式訓練時,反饋說程序有內(nèi)存泄露的情況。詳細了解之后,現(xiàn)場情況是這樣的:

  • 數(shù)據(jù)從hdfs讀取,checkpoint也保存到hdfs
  • 對于推薦模型,查表多,計算少,所以跑在了CPU上。而由于單個進程的CPU利用率上不去,所以每個機器上都起了多個tensorflow進程(當然,如何提高單進程時的程序性能,是另一個話題了,這里先不談)。
  • 隨著運行時間的增加,系統(tǒng)的空閑物理內(nèi)存在逐漸減少,最終會引發(fā)Linux的OOM Killer殺掉某個進程。而由于PS占用的物理內(nèi)存最大,所以基本上都是PS被殺掉。

初次分析

盡管ps被kill,但內(nèi)存消耗卻不一定是ps引起的。為了進一步確定問題,我首先觀察了下各進程virtual memory和res memory的使用情況:

while true; do
    # 打印virtual memory和res memory
    ps ux | grep 'job_name=ps\|job_name=worker' | grep -v grep | awk '{print $5,$6}' 
    sleep 30
done

通過對內(nèi)存使用的觀察,我大致總結(jié)了一些現(xiàn)象:

  1. 無論是ps還是worker,virtual memory都要比res memory大出不少來。這應該是比較正常的現(xiàn)象。
  2. ps的virtual memory和res memory都還維持在一個比較穩(wěn)定的狀態(tài),不像是有內(nèi)存泄漏的樣子。
  3. worker的virtual memory有緩慢的上漲,而res memory則比較快的進行增長,但也還遠遠沒有達到virtual memory的大小。由于一臺物理機上起了好幾個worker進程,所以物理內(nèi)存會消耗的比較快。

通過這幾點,我開始腦補問題的原因:

  • ps的virtual和res memory都比較平穩(wěn),所以應該不像是內(nèi)存泄漏的樣子
  • worker的virtual memory增長遠沒有res memory快,這種情況有點像申請了一個大內(nèi)存池,然后池里面的內(nèi)存在發(fā)生著泄露。

出于這個原因,我就沒急著上gperftools這種內(nèi)存檢測工具??紤]到tensorflow進程里由于hdfs的使用而嵌入了一個jvm,所以我覺得這搞不好是java的問題:申請了一大坨堆內(nèi)存,然后開始慢慢的把它們都用滿。

驗證

為了驗證猜想,我先把代碼改成了讀本地,非常幸運的是:問題消失了。所以很自然的,我認為應該是jvm申請了太大的堆內(nèi)存,總體造成了物理內(nèi)存的浪費。

于是我設置了下hdfs c接口的jvm參數(shù),將堆內(nèi)存限制為1G:

export LIBHDFS_OPTS=-Xmx1g -Xms256m -Xmn128m

運行了一段時間后,java開始報OutOfMemory:

java_oom.png

再來

雖然問題沒解決,但java OOM的異常堆棧給提供了一個很有用的信息:程序在創(chuàng)建hadoop Filesystem對象時出錯了。翻一下tensorflow的代碼,從注釋中你就會發(fā)現(xiàn)這其實是不符合程序本意的:

tensorflow_hadoop_source.png

tensorflow希望只有一個FileSystem的對象,但它依賴于hdfs的cache層來保證這一點。所以,程序在運行一段時間后,還會去創(chuàng)建新的FileSystem對象,非常不合理。

無奈只好開始啃hadoop的代碼。發(fā)現(xiàn)c接口可以通過設置一個開關參數(shù)來打印FileSystem的泄漏情況:

hadoop_source.png

在tensorflow調(diào)用hdfs的代碼中加上這個參數(shù)后,F(xiàn)ileSystem對象的創(chuàng)建過程得到了更加清楚的展示:

leak_detail.png

至此,已經(jīng)開始逐漸浮出水面了:

  • 由于某種原因,tensorflow在調(diào)用hdfs進行數(shù)據(jù)讀寫時,每次都會創(chuàng)建一個新的FileSystem對象。而這些對象很有可能是一直在cache中存放而沒有進行刪除的。

后來,hdfs的同事通過對java dump文件的一些分析,也證實了這個結(jié)論。有關jvm的內(nèi)存分析工具,不再詳細展開,大家可以用jmap為關鍵字進行搜索。

root cause

找到內(nèi)存泄漏的來源后,就開始從代碼層面進行分析,大體流程如下:

  • 我們在使用tensorflow的時候,會通過KERB_TICKET_CACHE_PATH環(huán)境變量來指定hdfs kerberos的ticket cache(如果你對kerberos不熟悉,可以看我的這篇文章)。而一旦設置了該環(huán)境變量后,訪問hadoop就會創(chuàng)建一個新的UserGroupInformation,從而創(chuàng)建一個新的FileSystem。
  • 通過和hdfs的同學溝通,可以在程序外部執(zhí)行kinit,并且將ticket cache放到默認位置后,不設置該環(huán)境變量也可以訪問帶安全的hdfs。
  • 我們之所以使用了該環(huán)境變量,可能是由于受老的tensorflow文檔的影響。但就hdfs而言,使用自定義路徑ticket cache接口的行為,的確也略微有些不太清晰。

所以,最后通過去除這個環(huán)境變量,這個問題得以解決。

寫在最后

這么簡單的一個問題,花了我其實有將近一周的時間調(diào)試,回想下還是有些啼笑皆非的。整個調(diào)試過程的感慨如下:

  • 找bug有時候也是個運氣活。想從風馬牛不相及的現(xiàn)象回溯到原因中去,能夠找到懷疑的方向是非常重要的。而為了提高自己在方向判斷上的敏銳度,還是得努力擴充自己在系統(tǒng)層面的知識面才行。
  • 對于一個復雜的系統(tǒng)而言,把接口行為定義清楚,把文檔寫清楚,真的相當重要。很多看似在開發(fā)階段節(jié)省下來的少量時間,在維護階段很有可能都得加倍償還回去。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容