原文地址
摘要
本文基于ES 6.4.3。主要分析由于在使用BulkProcessor向ES集群提交数据而集群响应异常时,导致的BulkProcessor死锁,而无法继续向BulkProcessor提交新数据,导致整个处理流程卡死的问题。
问题概述
我们程序流程是:
从Kafka消费数据
程序解析数据
处理好后,将数据组装为IndexRequest,使用bulkProcessor.add(indexRequest),异步批量提交数据到ES
最近Kafka消费延迟监控报警,发现某个topic消费延迟持续增长,消费速率降为0,所以查看了日志,发现最开是出问题的时候报错了:
2020-09-05 00:36:21.196 ERROR ElastServiceImpl$2,478 - ElastServiceImpl saveData failed: null
2020-09-05 00:36:21.196 ERROR ElastServiceImpl$2,479 - bulk failed null msgs
2020-09-05 00:36:26.798 ERROR ElastServiceImpl$2,478 - ElastServiceImpl saveData failed: null
2020-09-05 00:36:26.798 ERROR ElastServiceImpl$2,479 - bulk failed null msgs
2020-09-05 00:36:28.526 ERROR ElastServiceImpl$2,478 - ElastServiceImpl saveData failed: null
2020-09-05 00:36:28.526 ERROR ElastServiceImpl$2,479 - bulk failed null msgs
2020-09-05 00:36:28.927 ERROR ElastServiceImpl$2,478 - ElastServiceImpl saveData failed: null
2020-09-05 00:36:28.927 ERROR ElastServiceImpl$2,479 - bulk failed null msgs
这段日志来源于BulkProcessor
@Override
public void afterBulk(long executionId,
BulkRequest request,
Throwable failure) {
logger.error("ElastServiceImpl saveData failed: " + failure.getMessage());
logger.error("bulk failed " + failure.getMessage() + " msgs");
}
而我们的程序消费Kafka后提交offset日志也停在了同一时段:
2020-09-05 00:36:19.865 INFO AbstractKafkaConsumeTask,172 - EsKafkaConsumeTask successfully to commit offset to zookeeper, topic=xxx, consumeId is:1
2020-09-05 00:36:21.192 INFO AbstractKafkaConsumeTask,172 - EsKafkaConsumeTask successfully to commit offset to zookeeper, topic=xxx, consumeId is:2
2020-09-05 00:36:21.249 INFO AbstractKafkaConsumeTask,172 - EsKafkaConsumeTask successfully to commit offset to zookeeper, topic=xxx, consumeId is:3
问题排查
日志
消费线程挂了?
否,没有出现相关日志
消费出错?
否,没有出现相关日志
写入ES出错?
否,没有出现相关日志
top
我先用以下命令看了下有没有占cpu很高的:
top -Hp pid
发现线程占得资源都很平均。
jstack
也就是说程序只是卡住了,具体卡在哪里,只能用jstack分析。
首先把jstack输出到文件
jstack 29192 > slave.jstack
然后分析该文件.
less slave.jstack
直接搜索BulkProcessor关键字,发现大量相关线程处于BLOCKED阻塞状态,也就是说代码阻塞在synchronized等待对象锁:
上图中- waiting to lock <0x0000000606b340c8> (a org.elasticsearch.action.bulk.BulkProcessor)字样就是指当前线程在等待对象锁,可以看看该处BulkProcessor#internalAdd代码:
private synchronized void internalAdd(DocWriteRequest request, @Nullable Object payload) {
ensureOpen();
bulkRequest.add(request, payload);
executeIfNeeded();
}
可以看到,该方法是需要获取对象级别的同步锁的。
我们以某个等待的锁编号0x0000000606b340c8来查询,结果如下:
可以看到,这个elasticsearch[scheduler][T#1]线程已经获取了该对象锁,但现在处于WAITING状态,等待申请 Semaphore,相关代码如下:
而我们知道,BulkProcessor的Flush线程会定时调用这个execute方法,并在响应后释放semaphore。但现在没有被正确释放,说明这个之前的定时调度没有调用回调方法:
@Override
public void onResponse(BulkResponse response) {
try {
listener.afterBulk(executionId, bulkRequest, response);
} finally {
semaphore.release();
latch.countDown();
}
}
@Override
public void onFailure(Exception e) {
try {
listener.afterBulk(executionId, bulkRequest, e);
} finally {
semaphore.release();
latch.countDown();
}
}
而我们Flush线程定时调度相关代码:
scheduler.scheduleWithFixedDelay(flushRunnable, flushInterval, ThreadPool.Names.GENERIC);
那也就造成了无法继续调度,全部在这hang住了。
3 问题总结
因为Flush定时调度线程有两次调度semaphore没有正确被释放,导致后续调度执行到semaphore.acquire()的时候无法获得许可,所以调用了底层的AQS使用的LockSupport.park阻塞,变为WAITING状态。
Flush线程每次run的时候会占用BulkProcessor对象级别的同步锁
@Override
public void run() {
synchronized (BulkProcessor.this) {
if (closed) {
return;
}
if (bulkRequest.numberOfActions() == 0) {
return;
}
execute();
}
}
所以我们的程序消费线程往BulkProcessor插入数据的时候,就无法获取该BulkProcessor对象级同步锁了
private synchronized void internalAdd(DocWriteRequest request, @Nullable Object payload) {
ensureOpen();
bulkRequest.add(request, payload);
executeIfNeeded();
}
最终,造成全部卡死!
重试线程也无法进行,因为重试线程的调度是和Flush线程使用同一个只有一个核心线程的线程池进行的:
该issue已经有人提出,相关连接:
4 根本原因
根本原因,没有再去深究,这个是ES内核的问题,也不知道到底是哪一步卡住了。