ElasticSearch 2.3 Master OOM

it2025-03-25  23

环境

ES

version:2.3.0

部署:Master Node+Data Node

配置:3台 master为8C8G,data node为8C16G

备注:此问题与JDK、操作系统无关,因此就没列详细信息

问题描述

ES集群每天动态创建500+索引,索引保留三天,三天后清掉。近期,Master出现两次OOM,且内存一直不会释放。查看dump文件如下:

 

                                                                                            (图1)

                                                                                            (图2)

定位一阶段

从dump文件发现InternalClusterService中变量updateTaskPerExecutor持有800M内存,

updateTaskPerExecutor的定义

private final Map<ClusterStateTaskExecutor, List<UpdateTask>> updateTasksPerExecutor = new HashMap<>();

添加操作:

innerSubmitStateUpdateTask中以ClusterStateTaskExecutor为key添加List<UpdateTask>到HashMap中

private <T> void innerSubmitStateUpdateTask(...) { try { //new 新的updateTask synchronized (updateTasksPerExecutor) { if (updateTasksForExecutor == null) { //创建新的ArrayList updateTasksPerExecutor.put(executor, new ArrayList<UpdateTask>()); } //只有此处会添加数据到updateTasksPerExecutor updateTasksPerExecutor.get(executor).add(updateTask); } //timeout不为空,延迟执行updateTask if (config.timeout() != null) { updateTasksExecutor.execute(...) } else { //在线程池(中有一个线程)中立即执行updateTask updateTasksExecutor.execute(updateTask); } } catch (EsRejectedExecutionException e) { } }

删除操作:

Master启动了单线程

//只有一个线程的线程池 private volatile PrioritizedEsThreadPoolExecutor updateTasksExecutor;

在updateTask的runTaskForExecutor中删除List<UpdateTask>,

void runTasksForExecutor(ClusterStateTaskExecutor<T> executor) { synchronized (updateTasksPerExecutor) { //按executor删除List<UpdatTask> List<UpdateTask> pending = updateTasksPerExecutor.remove(executor); ... } ... }

思考:

代码看没有问题,有释放内存的地方,难道是线程堵塞了,或者死锁了?

                                                                                            (图3)

 

很显然线程没有堵塞,也没有死锁。难道是一个线程处理不过来,updateTask任务积压,处理不过来?但为什么内存会持续增加,full gc也没用呢?从dump文件中看Task是积压了,但是晚上没有业务的,积压的任务总会做完,但为什么内存一直不释放呢?

定位二阶段

没把握问题,肯定是细节忽略了。继续看dump文件。图2中显示updateTaskPerExecutor中很多ClusterState的transitive reference引用,占用内存的是 RoutingTable,而且每个ClusterState和其中的RoutingTable的内存地址均不一样。每个ClusterState对象大小差不多。为什么会有这么多ClusterState对象呢?

                                                                                     (图4)

先看InternalClusterService中对ClusterState的定义

//volatile保证线程之间的内存可见性,总是能看到新的clusterState private volatile ClusterState clusterState;

每个线程都能看到新的clusterState,那更不应该有问题了。

再看新、老clusterState更新

void runTasksForExecutor(ClusterStateTaskExecutor<T> executor) { ... // update the current cluster state clusterState = newClusterState; ... }

还是没有头绪,感觉不应该有问题

定位三阶段

UpdateTask介绍

Master节点的对create索引、删除索引、修改mapping等操作最终都包装成一个UpdateTask添加到updateTaskPerExecutor中由updateTasksExecutor异步执行。

TransportMasterNodeAction类是所有操作类型的父类,主要完成master节点判断请求和处理失败重试功能。实现了doExecute方法,新启动一个异步单线程(异步响应,不占用接收request线程),如果本节点是master节点,则启动执行masterOperation;如果不是,则发送给mastr节点执行,执行抽象方法masterOperation由子类实现;另外通过ClusterStateObserver.waitForNextChange完成了错误重试功能。ClusterStateObserver类,集群状态监控器。每次动作都会创建一个AsyncSingleAction和ClusterStateObserver实例。

分析

分析其中一个clusterState对象的incoming reference

                                                                                            (图5)

注意:每个clusterStateObserver代表一次Master的操作,图5所示,引用关系很复杂。

问题原因

条件:当对Master有频繁的操作时,导致UpdateTask在updateTaskPerExecutor中积压

1. 由于InternalClusterService中定义的clusterState实例对外的引用关系太复杂了,同时clusterState变更保护不够,仅仅是加了一个volatile,很容易产生ABA问题,最终导致就算clusterState变更后,老的clustertState不会被释放

2. ClusterStateObserver中定义的

final AtomicReference<ObservedState> lastObservedState;

还会持有老的引用。最终clusterState是哪个版本只有天知道。

3. 每次创建新的clusterState都会新建一份RoutingTable,

RoutingTable(long version, Map<String, IndexRoutingTable> indicesRouting) { this.version = version; //copy一份,这也解释上面看到大小差不多 this.indicesRouting = ImmutableMap.copyOf(indicesRouting); }

持续分析

ES的高版本是否存在这问题?查看github在5.x解决了这问题

问题:https://github.com/elastic/elasticsearch/issues/21439

问题原因官方解说:https://github.com/elastic/elasticsearch/issues/21568

问题解决:

https://github.com/elastic/elasticsearch/pull/21578

清掉和多线程只是缓解

https://github.com/elastic/elasticsearch/pull/21631,这个改造很有基本上解决了这个问题是,观察者根本不需要观察clusterState本身,而是观察masterid和version即可。

 

最新回复(0)