Presto內存泄露問題調查

問題背景:

sls的線上流量越來越大,S1幾乎增長了100%。在杭州region,每隔一段時間,一部分機器Presto就會開始頻繁的Full GC,重啓後穩定一段時間,然後過一段時間又開始頻繁Full GC。Full GC達到一定次數的時候,就發生OOM,進程直接crash。由於Full GC時間長,影響線上的可用性,因此開始投入精力進行調查。

查看GC 文件

當頻繁發生GC時,會在gc文件中打出下邊的內容,表示GC發生的類型(Full GC(Allocation Failure)), 在發生full gc之前,堆用了19.9G;GC後,還是19.8G,也就是說GC發生後,只釋放了0.1G的空間。

2019-08-28T15:58:46.140+0800: 2414974.134: [Full GC (Allocation Failure)  19G->19G(20G), 25.4147570 secs]   
   [Eden: 0.0B(1024.0M)->0.0B(1024.0M) Survivors: 0.0B->0.0B Heap: 19.9G(20.0G)->19.8G(20.0G)], [Metaspace: 13
9463K->139463K(1267712K)]
 [Times: user=40.66 sys=0.00, real=25.42 secs]

由於沒有足夠的可用內存,於是Presto很快再次Full GC,知道最終一點內存都沒有了,發生了OOM 異常,程序crash。

注:GC文件怎麼打:

-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=5M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps

監控內存的變化趨勢

從外部看進程的內存使用率,如下圖所示,是看不出任何信息的。因爲java進程內部維護了內存的分配。
image.png

於是我通過jmx監控去查看Presto進程內部的內存分佈。 java進程把內存分成幾個部分:年輕區,倖存區,老年區。不同的內存依據生命週期的長短放在不同的區域內。通常,內存是在年輕代分配,當年齡達到一定大小時,會移動老年區(這裏簡化了模型,實際還有幸存區的作用)。

在jmx中,存在如下的監控項目,分別表示老年代(Old Gen)和年輕代(Eden Space)已經使用的內存。通過jmx接口週期性的讀取這兩個屬性,接入到神農監控。

java.lang:type=MemoryPool,name=G1 Old Gen/Usage/used (Long) = 1182129304
java.lang:type=MemoryPool,name=G1 Eden Space/Usage/used (Long) = 251658240

我們把時間週期拉長了看,可以看到老年代(紅色線)的內存趨勢一致處於緩慢上漲狀態。

image.png

通過智能算法查找原因

由於內存趨勢的規律性,首先懷疑的是由於某些用戶的訪問上漲,因此通過相似性算法,查找哪個用戶的訪問趨勢和這個內存曲線相似。很遺憾的是,沒有找到類似的用戶趨勢。

接下來日誌聚類查看有哪些日誌pattern,日誌聚類能夠把相似日誌聚合成一個pattern,幫助我們快速瀏覽大量的日誌。 我們注意到了下圖中的這個pattern:這個pattern雖然不能說明和內存問題直接相關,但是這個pattern給我們留下了印象,接下來的一系列調查也證明內存問題和這個pattern相關。

image.png

通過heapdump查看內存泄露的原因

智能算法沒有找到特定的用戶,沒有辦法,只能老老實實的去分析內存。

java 自帶的工具jmap,可以打印出當前內存的object分佈,也可以把內存寫到磁盤上。

打印內存直方圖,:live表示在打印前執行一次full gc
jmap -histo:live `pid of java` > /tmp/jmap00   
dump堆文件
jmap -dump:format=b,file=heap.dump `pid of java`  

直方圖能夠看到內存的分配,但是信息太粗,只能到object級別,看不出具體的對象,也看不到reference。

dump heap會讓程序停頓,爲了避免對線上訪問造成影響,先把這臺機器的心跳摘掉,移除流量。再執行上述的dump命令。 獲取到dump文件後,可以用jhat打開,也可以用jvisualVM 打開。不過用jhat打開時,遇到OOM的問題。最終用jvisualVM打開後,看到如下的大量對象PendingRead。再查看reference,可以確定是SqlTask 下的內存泄露了。

從reference中可以看到一個taskId,一個偶然的機會,在presto的http-request.log文件中,找到了線索,

image.png

通過sls查看presto的http請求。最近15分鐘,一些task持續運行了大約15分鐘。從task命名上可以看到,22號的task仍然在執行。

通過task的api,獲取task當前的狀態:可以發現task仍然處於running狀態。

curl 11.194.214.145:10008/v1/task/20190816_183242_49436_pwzkn.2.5
{"taskStatus":{"taskId":"20190816_183242_49436_pwzkn.2.5","taskInstanceId":"1ae02d83-f024-4d45-997b-96dea741b04e","version":1768082,"state":"RUNNING","self":"http://h51c07359.cloud.et91:10008/v1/task/20190816_183242_49436_pwzkn.2.5","failures":[],"queuedPartitionedDrivers":0,"runningPartitionedDrivers":0,"memoryReservation":"0B"},"lastHeartbeat":"2019-08-27T01:34:56.905Z","outputBuffers":{"type":"UNINITIALIZED","state":"OPEN","canAddBuffers":true,"canAddPages":true,"totalBufferedBytes":0,"totalBufferedPages":0,"totalRowsSent":0,"totalPagesSent":0,"buffers":[]},"noMoreSplits":[],"stats":{"createTime":"2019-08-24T12:42:07.748Z","elapsedTime":"0.00ms","queuedTime":"0.00ms","totalDrivers":0,"queuedDrivers":0,"queuedPartitionedDrivers":0,"runningDrivers":0,"runningPartitionedDrivers":0,"completedDrivers":0,"cumulativeMemory":0.0,"memoryReservation":"0B","systemMemoryReservation":"0B","totalScheduledTime":"0.00ms","totalCpuTime":"0.00ms","totalUserTime":"0.00ms","totalBlockedTime":"0.00ms","fullyBlocked":false,"blockedReasons":[],"rawInputDataSize":"0B","rawInputPositions":0,"processedInputDataSize":"0B","processedInputPositions":0,"outputDataSize":"0B","outputPositions":0,"pipelines":[]},"needsPlan":true,"complete":false}

從taskid獲取queryid : 20190816_183242_49436_pwzkn,從運行日誌中獲取該query的執行結果:發現query在16號的時候發生了運行時錯誤。

image.png

既然整個query已經確認fail了,但是task處於running狀態,那麼我可以強制通過task的delete api,把任務給清理掉。在清理過後,task的狀態變成了ABORT狀態,表示任務失敗了。但是調用過後, task的請求並未終止,仍然在繼續執行,過了大約15分鐘,再次去查看task的狀態,又變成了RUNNING狀態。

內存泄露的原因

已經從上述調查中,知道了內存泄露的位置。通過閱讀代碼和推演,大致理清楚了內存泄露的原因。

query執行的流程如下:在正常情況下,coordinator調度split到每個機器上,生成對應的task, 然後下游的task生成輪訓任務,向上遊task讀取計算結果。如果任何一個task發生了錯誤,那麼coordinator會把所有的task終止掉。

image.png

但是在某些情況下,某一臺機器發生了調度延遲,Task 2首先調度,並且開始了計算,但是由於遇到了計算錯誤,於是終止了task。 接下來這個時候task1纔開始調度,然後生成了向task2輪訓的任務。由於task2是異常終止的,內存中的標誌位都是沒有清空,導致認爲task2還在讀數據,因此輪訓任務一直終止不了。每次輪訓,都生成一個PendingRead放到內存中。日積月累,就造成了內存泄露。

驗證內存泄露的原因

計算task一直不能終止,那麼如果我強行通過API DELETE掉task,內存理論上可以被刪除掉。

通過curl刪除task的http請求,

curl 11.223.196.92:10008/v1/task/20190822_163910_94499_pwzkn.2.4 -X DELETE

刪除後的狀態

{"taskStatus":{"taskId":"20190822_163910_94499_pwzkn.2.4","taskInstanceId":"71f71c85-5073-4cf9-852b-16bf9c49496f","version":3239316,"state":"ABORTED","self":"
http://g24h09288.cloud.et91:10008/v1/task/20190822_163910_94499_pwzkn.2.4","failures":[],"queuedPartitionedDrivers":0,"runningPartitionedDrivers":0,"memoryRes
ervation":"0B"},"lastHeartbeat":"2019-08-27T01:32:46.223Z","outputBuffers":{"type":"UNINITIALIZED","state":"OPEN","canAddBuffers":true,"canAddPages":true,"tot
alBufferedBytes":0,"totalBufferedPages":0,"totalRowsSent":0,"totalPagesSent":0,"buffers":[]},"noMoreSplits":[],"stats":{"createTime":"2019-08-23T02:17:55.766Z
","endTime":"2019-08-27T01:32:47.296Z","elapsedTime":"0.00ms","queuedTime":"0.00ms","totalDrivers":0,"queuedDrivers":0,"queuedPartitionedDrivers":0,"runningDr
ivers":0,"runningPartitionedDrivers":0,"completedDrivers":0,"cumulativeMemory":0.0,"memoryReservation":"0B","systemMemoryReservation":"0B","totalScheduledTime
":"0.00ms","totalCpuTime":"0.00ms","totalUserTime":"0.00ms","totalBlockedTime":"0.00ms","fullyBlocked":false,"blockedReasons":[],"rawInputDataSize":"0B","rawI
nputPositions":0,"processedInputDataSize":"0B","processedInputPositions":0,"outputDataSize":"0B","outputPositions":0,"pipelines":[]},"needsPlan":true,"complet
e":true}

刪除後等待15分鐘,task會從內存中刪除:可以看到內存發生了大幅下跌。

image.png

過幾天后再去看內存,又在持續的增長,這是因爲我們只是清理了內存,但是輪訓任務並沒有被清理掉.

image.png

構造case復現

從採樣到的幾個內存泄漏點,我們可以看到明顯的特徵,就是query遇到錯誤的數據,執行失敗,然後纔有概率遇到調度異常。因此我們可以構造一些非法數據,讓source節點快速的fail掉。通過檢查http-request.log中task的運行時長,可以確認是否發生了內存泄露。

修復內存泄露

從上述驗證過程也可以看出,要想修復內存泄露,必須讓泄露的輪訓任務終止掉。有幾種修復方案:

  1. 上游判斷,如果stat是terminal(結束或失敗)狀態,返回空結果和結束標誌。
  2. 下游判斷,如果query是結束狀態,那麼不生成輪訓task。
  3. 下游判斷,如果task處於close狀態,那麼生成輪訓task,但是進入清理階段,清理掉上游的內存後退出。

綜合考慮每種方案,以及測試結果,最終採用了第三種方案,可以把內存清理乾淨,避免一些遺留問題。

參考資料

java垃圾回收完全手冊
個人博客地址

招聘阿里雲智能事業羣-智能日誌分析研發專家-杭州/上海 掃碼加我

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章