起因
最近信息流推薦的業(yè)務(wù)方在使用tensorflow進(jìn)行分布式訓(xùn)練時(shí)延窜,反饋說程序有內(nèi)存泄露的情況吵冒。詳細(xì)了解之后,現(xiàn)場(chǎng)情況是這樣的:
- 數(shù)據(jù)從hdfs讀取,checkpoint也保存到hdfs
- 對(duì)于推薦模型算谈,查表多,計(jì)算少料滥,所以跑在了CPU上然眼。而由于單個(gè)進(jìn)程的CPU利用率上不去,所以每個(gè)機(jī)器上都起了多個(gè)tensorflow進(jìn)程(當(dāng)然葵腹,如何提高單進(jìn)程時(shí)的程序性能高每,是另一個(gè)話題了,這里先不談)践宴。
- 隨著運(yùn)行時(shí)間的增加鲸匿,系統(tǒng)的空閑物理內(nèi)存在逐漸減少,最終會(huì)引發(fā)Linux的OOM Killer殺掉某個(gè)進(jìn)程阻肩。而由于PS占用的物理內(nèi)存最大带欢,所以基本上都是PS被殺掉。
初次分析
盡管ps被kill烤惊,但內(nèi)存消耗卻不一定是ps引起的乔煞。為了進(jìn)一步確定問題,我首先觀察了下各進(jìn)程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
通過對(duì)內(nèi)存使用的觀察柒室,我大致總結(jié)了一些現(xiàn)象:
- 無論是ps還是worker渡贾,virtual memory都要比res memory大出不少來。這應(yīng)該是比較正常的現(xiàn)象雄右。
- ps的virtual memory和res memory都還維持在一個(gè)比較穩(wěn)定的狀態(tài)空骚,不像是有內(nèi)存泄漏的樣子。
- worker的virtual memory有緩慢的上漲擂仍,而res memory則比較快的進(jìn)行增長(zhǎng)囤屹,但也還遠(yuǎn)遠(yuǎn)沒有達(dá)到virtual memory的大小。由于一臺(tái)物理機(jī)上起了好幾個(gè)worker進(jìn)程防楷,所以物理內(nèi)存會(huì)消耗的比較快牺丙。
通過這幾點(diǎn),我開始腦補(bǔ)問題的原因:
- ps的virtual和res memory都比較平穩(wěn)复局,所以應(yīng)該不像是內(nèi)存泄漏的樣子
- worker的virtual memory增長(zhǎng)遠(yuǎn)沒有res memory快冲簿,這種情況有點(diǎn)像申請(qǐng)了一個(gè)大內(nèi)存池,然后池里面的內(nèi)存在發(fā)生著泄露亿昏。
出于這個(gè)原因峦剔,我就沒急著上gperftools這種內(nèi)存檢測(cè)工具〗枪常考慮到tensorflow進(jìn)程里由于hdfs的使用而嵌入了一個(gè)jvm吝沫,所以我覺得這搞不好是java的問題:申請(qǐng)了一大坨堆內(nèi)存呻澜,然后開始慢慢的把它們都用滿。
驗(yàn)證
為了驗(yàn)證猜想惨险,我先把代碼改成了讀本地羹幸,非常幸運(yùn)的是:?jiǎn)栴}消失了。所以很自然的辫愉,我認(rèn)為應(yīng)該是jvm申請(qǐng)了太大的堆內(nèi)存栅受,總體造成了物理內(nèi)存的浪費(fèi)。
于是我設(shè)置了下hdfs c接口的jvm參數(shù)恭朗,將堆內(nèi)存限制為1G:
export LIBHDFS_OPTS=-Xmx1g -Xms256m -Xmn128m
運(yùn)行了一段時(shí)間后屏镊,java開始報(bào)OutOfMemory:
再來
雖然問題沒解決,但java OOM的異常堆棧給提供了一個(gè)很有用的信息:程序在創(chuàng)建hadoop Filesystem對(duì)象時(shí)出錯(cuò)了痰腮。翻一下tensorflow的代碼而芥,從注釋中你就會(huì)發(fā)現(xiàn)這其實(shí)是不符合程序本意的:
tensorflow希望只有一個(gè)FileSystem的對(duì)象,但它依賴于hdfs的cache層來保證這一點(diǎn)膀值。所以棍丐,程序在運(yùn)行一段時(shí)間后,還會(huì)去創(chuàng)建新的FileSystem對(duì)象虫腋,非常不合理骄酗。
無奈只好開始啃hadoop的代碼稀余。發(fā)現(xiàn)c接口可以通過設(shè)置一個(gè)開關(guān)參數(shù)來打印FileSystem的泄漏情況:
在tensorflow調(diào)用hdfs的代碼中加上這個(gè)參數(shù)后悦冀,F(xiàn)ileSystem對(duì)象的創(chuàng)建過程得到了更加清楚的展示:
至此,已經(jīng)開始逐漸浮出水面了:
- 由于某種原因睛琳,tensorflow在調(diào)用hdfs進(jìn)行數(shù)據(jù)讀寫時(shí)盒蟆,每次都會(huì)創(chuàng)建一個(gè)新的FileSystem對(duì)象。而這些對(duì)象很有可能是一直在cache中存放而沒有進(jìn)行刪除的师骗。
后來历等,hdfs的同事通過對(duì)java dump文件的一些分析,也證實(shí)了這個(gè)結(jié)論辟癌。有關(guān)jvm的內(nèi)存分析工具寒屯,不再詳細(xì)展開,大家可以用jmap為關(guān)鍵字進(jìn)行搜索黍少。
root cause
找到內(nèi)存泄漏的來源后寡夹,就開始從代碼層面進(jìn)行分析,大體流程如下:
- 我們?cè)谑褂胻ensorflow的時(shí)候厂置,會(huì)通過
KERB_TICKET_CACHE_PATH
環(huán)境變量來指定hdfs kerberos的ticket cache(如果你對(duì)kerberos不熟悉菩掏,可以看我的這篇文章)。而一旦設(shè)置了該環(huán)境變量后昵济,訪問hadoop就會(huì)創(chuàng)建一個(gè)新的UserGroupInformation智绸,從而創(chuàng)建一個(gè)新的FileSystem野揪。 - 通過和hdfs的同學(xué)溝通,可以在程序外部執(zhí)行kinit瞧栗,并且將ticket cache放到默認(rèn)位置后斯稳,不設(shè)置該環(huán)境變量也可以訪問帶安全的hdfs。
- 我們之所以使用了該環(huán)境變量迹恐,可能是由于受老的tensorflow文檔的影響平挑。但就hdfs而言,使用自定義路徑ticket cache接口的行為系草,的確也略微有些不太清晰通熄。
所以,最后通過去除這個(gè)環(huán)境變量找都,這個(gè)問題得以解決唇辨。
寫在最后
這么簡(jiǎn)單的一個(gè)問題,花了我其實(shí)有將近一周的時(shí)間調(diào)試能耻,回想下還是有些啼笑皆非的赏枚。整個(gè)調(diào)試過程的感慨如下:
- 找bug有時(shí)候也是個(gè)運(yùn)氣活。想從風(fēng)馬牛不相及的現(xiàn)象回溯到原因中去晓猛,能夠找到懷疑的方向是非常重要的饿幅。而為了提高自己在方向判斷上的敏銳度,還是得努力擴(kuò)充自己在系統(tǒng)層面的知識(shí)面才行戒职。
- 對(duì)于一個(gè)復(fù)雜的系統(tǒng)而言栗恩,把接口行為定義清楚,把文檔寫清楚洪燥,真的相當(dāng)重要磕秤。很多看似在開發(fā)階段節(jié)省下來的少量時(shí)間,在維護(hù)階段很有可能都得加倍償還回去捧韵。