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即可。