为了防止集群里节点OOM,Presto有个循环线程来获取当前集群节点和集群整体内存占用情况。通过这篇文章:Presto内存管理相关参数设置 我们知道Presto里分为RESERVED_POOL和GENERAL_POOL。
判断节点是否阻塞(内存不足)
如果使用RESERVED_POOL(意思是说最大SQL使用这个POOL),那判断集群内存超出内存的方法就是:
1、RESERVED_POOL内存被SQL占用了
2、GENERAL_POOL里有被阻塞的Node
因为RESERVED_POOL会导致内存浪费,我们集群配置参数没有使用这个POOL,只使用了GENERAL_POOL,所以只需要查看下GENERAL_POOL是怎么判断节点是否Block住的。
1 | if (poolInfo.getFreeBytes() + poolInfo.getReservedRevocableBytes() <= 0) { |
getReservedRevocableBytes 这个是用于获取spill到磁盘的内存,目前我们集群是不允许内存Spill到磁盘的,因为Presto面向的是ad-hoc场景,要求是快,如果说需要spill到磁盘,那spark是一个更好的选择,且早期版本Presto spill到磁盘之前测试过稳定性比较差,场景也比较少。
所以就判断GENERAL_POOL里是否还有剩余内存,如果小于等于0,那就表示该节点是个Block状态。
Kill策略
把所有query遍历一遍,如果查询有RESOURCE_OVERCOMMIT标志,且内存溢出了,那就把带有此标志的SQL Kill掉。
1 | if (resourceOvercommit && outOfMemory) { |
如果没有RESOURCE_OVERCOMMIT标志,那就看看内存是否超过集群允许的内存:
1 | if (!resourceOvercommit) { |
由上面我们可以知道,想让上述配置生效,query.max-memory需要配置合理的值,比如我们有5台Worker,每台Worker最大允许的内存为10G(超过10GB,每台Worker会自动Kill掉SQL),那此集群允许的最大内存为5*10G = 50GB,如果你配置query.max-memory(maxQueryMemory)100GB,那上面的逻辑将不会走,导致此逻辑失效。
同时,如果发现集群有节点已经OOM了,但是过了5S,依然没有SQL被Kill掉,那就会使用以下触发Presto SQL Kill策略,一共2种策略,query.low-memory-killer.policy指定:
1 | public static class LowMemoryKillerPolicy |
total-reservation表示杀掉集群中占有内存最大的SQL,total-reservation-on-blocked-nodes表示杀死在内存不足(阻塞)的节点上使用最多内存的查询。
假如我们通过RESOURCE_GROUP限制集群的并发大小为10,单个节点最大内存为10,集群允许的最大内存少于100GB,那此配置集群肯定不会出现OOM现象(不考虑内存泄露问题),但是这样的话,会导致资源利用率不够,所以线上一般会配置并发数 * 单机最大内存远大于集群允许的最大内存,这就指望LowMemoryKillerPolicy能帮助我们Kill掉内存占用最大的SQL。但是实际上依然会出现Worker OOM情况,此时一般思路:
1、dmesg -T查看系统日志,确定是操作系统触发的OOM还是JVM OOM,如果是操作系统触发的OOM,可能堆外内存或Presto内存泄露会导致这个问题,但是更多情况下需要确定下Presto节点是否有其他服务,一般情况下是其他服务内存突然增大,导致系统内存不够。
2、配置是否正确,是否限制了单机最大内存,是否限制了集群最大内存且小于单机最大内存 * 集群节点数,是否配置了LowMemoryKillerPolicy。
3、如果上面配置都正确,需要考虑2个问题,1)什么原因没有触发LowMemoryKillerPolicy,可以将判断节点阻塞(内存不足)的逻辑变的更严格点,因为不是个实时过程。2)结点是否有内存泄露情况。
从上面可以看出Presto内存管理比较粗暴,内存不够就杀掉SQL,而其他一些引擎做法就比较细腻些,SQL会一直Block住,直到有可用内存为止,不过Presto是面向ad-hoc场景,这种内存管理方法更简单有效吧。