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垃圾回收完全手册
个人博客地址

招聘阿里云智能事业群-智能日志分析研发专家-杭州/上海 扫码加我

在这里插入图片描述

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