Flink发送Elasticsearch的问题
文章目录
- Flink发送Elasticsearch的问题
- 场景
- 问题
- 资源配置困难
- 反压和job重启
- Direct buffer memory OOM
- 总结
- 参考文档
场景
目前是基于Flink1.11.1来实现日志的处理,中间涉及日志的解析和转发,最终发送到Elasticsearch,Elasticsearch服务端版本为6.3.1,客户端使用flink-connector-elasticsearch6_2.11(Flink提供的es sink connector,依赖的es客户端版本也是6.3.1),部署模式采用Flink Standalone,集群上一共就运行1个任务。
问题
资源配置困难
flink每个operator都可以配置并行度,具体每个operator应该设置多少并行度,每个TaskManager设置多少slot,多少内存,目前都是盲配的状态,主要有以下几个原因:
- flink taskmanager提供很多内存参数,包括堆内存、堆外内存、metaspace内存等,粒度较细;
- slot虽然是flink最小的资源单元,但并没有对资源做完全的隔离;
- task和slot对应关系不确定,1个slot里可以运行多个task(都来自同一个Job的不同task的subtask允许共享slot,即share);
- flink提供的默认资源配置很多时候无法满足要求
对此有几点建议:
- slot个数必须大于等于整个job的最大并行度,比如
source(3)->window(5)->sink(4)
这个链路,其中source为3个并发,window为5个并发,sink为4个并发,则最大并行度为5,slot个数必须大于等于5才能让job调度起来; - kafka source的并行度应该和topic的分区个数对应来调整,一般要求并行度小于等于topic分区数,否则会存在空跑的task,浪费资源
- 尽可能地将 operator 的 subtask 链接(chain)在一起形成 task,每个 task 在一个线程中执行,可以减少线程之间的切换,减少消息的序列化/反序列化,减少数据在缓冲区的交换,减少了延迟的同时提高整体的吞吐量。
- 保证足够的并行度,但并行度也不是越大越好,太多会加重数据在多个solt/task manager之间数据传输压力,包括序列化和反序列化带来的压力。
- CPU资源是task manager上的solt共享的,注意监控CPU的使用。
反压和job重启
在flink standalone模式下运行job,从kafka读取数据发送到es,es版本为6.3.1,当并发度设置比较高的情况下,job运行一段时间后,出现严重反压,并且超过checkpoint超时时间(设置的10分钟)后job会重启,不断重启无法成功。
通过jstack查看堆栈信息发现大量BulkProcessor
相关线程处于BLOCKED
阻塞状态,查看Taskmanager日志,有did not react to cancelling signal for
关键字,堆栈显示cancel job阻塞在BulkProcessor$Flush.run
方法。
TaskManager堆栈信息:
"elasticsearch[scheduler][T#1]" Id=15008 BLOCKED on org.elasticsearch.action.bulk.BulkProcessor@61e178a6 owned by "Sink: ProtoTraceLog (39/60)" Id=8781
at org.elasticsearch.action.bulk.BulkProcessor$Flush.run(BulkProcessor.java:366)
- blocked on org.elasticsearch.action.bulk.BulkProcessor@61e178a6
at org.elasticsearch.threadpool.Scheduler$ReschedulingRunnable.doRun(Scheduler.java:182)
at org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
...
Number of locked synchronizers = 1
- java.util.concurrent.ThreadPoolExecutor$Worker@15659a74
"Sink: ProtoTraceLog (39/60)" Id=8781 WAITING on java.util.concurrent.CountDownLatch$Sync@58bbbd7c
at sun.misc.Unsafe.park(Native Method)
- waiting on java.util.concurrent.CountDownLatch$Sync@58bbbd7c
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:997)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1304)
at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:231)
at org.elasticsearch.action.bulk.BulkRequestHandler.execute(BulkRequestHandler.java:86)
at org.elasticsearch.action.bulk.BulkProcessor.execute(BulkProcessor.java:339)
...
TaskManager日志
2020-12-16 14:36:35,291 ERROR com.hundsun.flink.handler.HsActionRequestFailureHandler [] - Sink to es exception ,exceptionData: index {[full_link_apm_span-2020- 12-16][apm][null], source[n/a, actual length: [5.8kb], max length: 2kb]} ,exceptionStackTrace: java.lang.InterruptedException
68224 at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:998)
68225 at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1304)
68226 at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:231)
68227 at org.elasticsearch.action.bulk.BulkRequestHandler.execute(BulkRequestHandler.java:86)
68228 at org.elasticsearch.action.bulk.BulkProcessor.execute(BulkProcessor.java:339)
68229 at org.elasticsearch.action.bulk.BulkProcessor.executeIfNeeded(BulkProcessor.java:330)
68230 at org.elasticsearch.action.bulk.BulkProcessor.internalAdd(BulkProcessor.java:288)
68231 at org.elasticsearch.action.bulk.BulkProcessor.add(BulkProcessor.java:271)
68232 at org.elasticsearch.action.bulk.BulkProcessor.add(BulkProcessor.java:267)
68233 at org.elasticsearch.action.bulk.BulkProcessor.add(BulkProcessor.java:253)
68234 at org.apache.flink.streaming.connectors.elasticsearch6.Elasticsearch6BulkProcessorIndexer.add(Elasticsearch6BulkProcessorIndexer.java:72)
68235 at com.hundsun.flink.util.ElasticSearchSinkUtil$1.process(ElasticSearchSinkUtil.java:59)
68236 at com.hundsun.flink.util.ElasticSearchSinkUtil$1.process(ElasticSearchSinkUtil.java:47)
68237 at org.apache.flink.streaming.connectors.elasticsearch.ElasticsearchSinkBase.invoke(ElasticsearchSinkBase.java:310)
68238 at org.apache.flink.streaming.api.operators.StreamSink.processElement(StreamSink.java:56)
68239 at org.apache.flink.streaming.runtime.tasks.OneInputStreamTask$StreamTaskNetworkOutput.emitRecord(OneInputStreamTask.java:161)
68240 at org.apache.flink.streaming.runtime.io.StreamTaskNetworkInput.processElement(StreamTaskNetworkInput.java:178)
68241 at org.apache.flink.streaming.runtime.io.StreamTaskNetworkInput.emitNext(StreamTaskNetworkInput.java:153)
68242 at org.apache.flink.streaming.runtime.io.StreamOneInputProcessor.processInput(StreamOneInputProcessor.java:67)
68243 at org.apache.flink.streaming.runtime.tasks.StreamTask.processInput(StreamTask.java:345)
68244 at org.apache.flink.streaming.runtime.tasks.mailbox.MailboxProcessor.runMailboxStep(MailboxProcessor.java:191)
68245 at org.apache.flink.streaming.runtime.tasks.mailbox.MailboxProcessor.runMailboxLoop(MailboxProcessor.java:181)
68246 at org.apache.flink.streaming.runtime.tasks.StreamTask.runMailboxLoop(StreamTask.java:558)
68247 at org.apache.flink.streaming.runtime.tasks.StreamTask.invoke(StreamTask.java:530)
68248 at org.apache.flink.runtime.taskmanager.Task.doRun(Task.java:721)
68249 at org.apache.flink.runtime.taskmanager.Task.run(Task.java:546)
出现以上问题的原因如下:
ISSUE-47599显示Elasticsearch7.3.0之前发送数据时存在死锁的问题
描述翻译如下:
如果BulkProcessor导致批量请求失败,则将通过RetryHandler重试它们。在7.3.0之前的Elasticsearch版本中,这可能导致死锁。
由于Flush和Retry逻辑共享一个Scheduler,因此可能发生死锁。之所以会发生死锁,是因为Scheduler只配置了1个工作线程,该线程可能在Flush方法中被阻塞。Flush的run方法内加了BulkProcessor.this同步锁,但internalAdd方法也被同一个对象锁定。可能发生的情况是,大量请求通过但internalAdd来获得了锁,但发送这批请求时遇到了错误,因此重试逻辑开始了。由于scheduler线程阻塞在Flush方法里,导致internalAdd被同步锁定,因此现在当Retry尝试安排重试时无法进行重试,由于Flush正在阻塞scheduler的唯一工作线程。进一步导致这里Flush由于正在等待internalAdd完成而无法继续,并且internalAdd无法结束因为他在等待Retry,但是Retry也无法结束因为他在等待schedule线程,该线程无法获得,因为它正在等待Flush完成。
Schedule初始化corePoolSize为1
@Scheduler
static ScheduledThreadPoolExecutor initScheduler(Settings settings) {
ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1,
EsExecutors.daemonThreadFactory(settings, "scheduler"), new EsAbortPolicy());
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
scheduler.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
scheduler.setRemoveOnCancelPolicy(true);
return scheduler;
}
Flush
线程用于发送数据,采用同步代码块,作用的对象是BulkProcessor
class Flush implements Runnable {
@Override
public void run() {
synchronized (BulkProcessor.this) {
...
execute();
}
}
}
internalAdd方法是用synchronized
修饰的同步方法
@BulkProcessor
private synchronized void internalAdd(DocWriteRequest request,
@Nullable Object payload){
ensureOpen();
bulkRequest.add(request, payload);
executeIfNeeded();
}
整个逻辑图如下所示:
由于ES7.3.0之前的Scheduler
只有1个工作线程,Flush
和Retry
都共用这一个调度。但是大量数据发送失败后可能会导致死锁,从而导致flink处理被hang住,数据无法流动,然后触发了反压,进一步导致checkpoint的Barrier
无法正常流动,所以checkpoint执行也被hang住,然后达到checkpoint超时时间(10分钟)后抛出异常,导致job发生重启(根据job重启策略)。
Direct buffer memory OOM
解决了问题2之后,job运行一会出现Direct buffer memory OOM异常,堆栈如下:
2020-12-16 15:37:08,704 ERROR org.apache.http.impl.nio.client.InternalHttpAsyncClient [] - I/O reactor terminated abnormally
org.apache.http.nio.reactor.IOReactorException: I/O dispatch worker terminated abnormally
at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor.execute(AbstractMultiworkerIOReactor.java:359) ~[blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
at org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager.execute(PoolingNHttpClientConnectionManager.java:221) ~[blob_p- f8aae2a0113951c2d34918dcffc906be6d5e9c33-80068570f848a6ac286386df457c6c1c:?]
at org.apache.http.impl.nio.client.CloseableHttpAsyncClientBase$1.run(CloseableHttpAsyncClientBase.java:64) [blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_181]
Caused by: java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694) ~[?:1.8.0_181]
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) ~[?:1.8.0_181]
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) ~[?:1.8.0_181]
at sun.nio.ch.Util.getTemporaryDirectBuffer(Util.java:241) ~[?:1.8.0_181]
at sun.nio.ch.IOUtil.write(IOUtil.java:58) ~[?:1.8.0_181]
at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:471) ~[?:1.8.0_181]
at org.apache.http.impl.nio.codecs.AbstractContentEncoder.doWriteChunk(AbstractContentEncoder.java:171) ~[blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
at org.apache.http.impl.nio.codecs.AbstractContentEncoder.doWriteChunk(AbstractContentEncoder.java:164) ~[blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
at org.apache.http.impl.nio.codecs.AbstractContentEncoder.writeToChannel(AbstractContentEncoder.java:138) ~[blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
at org.apache.http.impl.nio.codecs.LengthDelimitedEncoder.write(LengthDelimitedEncoder.java:130) ~[blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
at org.apache.http.nio.entity.NByteArrayEntity.produceContent(NByteArrayEntity.java:136) ~[blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
at org.apache.http.nio.protocol.BasicAsyncRequestProducer.produceContent(BasicAsyncRequestProducer.java:125) ~[blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
at org.apache.http.impl.nio.client.MainClientExec.produceContent(MainClientExec.java:262) ~[blob_p-f8aae2a0113951c2d34918dcffc906be6d5e9c33- 80068570f848a6ac286386df457c6c1c:?]
该异常通常说明 JVM 的直接内存限制过小,或者存在直接内存泄漏(Direct Memory Leak)。 请确认用户代码及外部依赖中是否使用了 JVM 直接内存,以及如果使用了直接内存,是否配置了足够的内存空间。 可以通过调整堆外内存来增大直接内存限制。 有关堆外内存的配置方法,请参考 TaskManager、JobManager 以及 JVM 参数的相关文档。
Direct Memory
包括Framework Off-Heap Memory
、Task Off-Heap Memory
和Network Memory
三部分,分别作用如下:
参数 | 配置选项 | 描述 |
Framework Off-Heap Memory | taskmanager.memory.framework.off-heap.size | Flink框架需要的堆外内存 |
Task Off-Heap Memory | taskmanager.memory.task.off-heap.size | Flink任务需要的堆外内存,一般用于operator |
Network Memory | taskmanager.memory.network.min taskmanager.memory.network.max taskmanager.memory.network.fraction | 数据发生网络shuffle需要的堆外内存 |
direct memory = framework off-heap + task off-heap+ network。其中network会在初始化时分配,可以视为和前2部分是隔离的,而前2部分之间是没有隔离的。
所以解决java.lang.OutOfMemoryError: Direct buffer memory
的思路是调整这三个参数。
将taskmanager.memory.framework.off-heap.size
根据实际情况调大,目前两个测试环境1个调整为256m(手动压得日志,单条日志只有6k左右),另一个调整为1g(贴近线上环境,单条日志比较大),job均运行正常。另外两个参数调大不起作用,因此主要是taskmanager.memory.framework.off-heap.size
参数起了作用。
对于当前版本,任务(taskmanager.memory.task.off-heap.size)/框架(taskmanager.memory.framework.off-heap.size)内存之间几乎没有区别。它们为将来的优化做好了准备,我们计划在其中允许将任务管理器的内存动态地切片到插槽中(当前将其切成固定数量和大小的插槽)。然后,可以对任务内存进行切片以执行任务,而将框架内存保留给任务管理器的框架。通常,我们建议用户仅设置任务内存,因为框架内存非常稳定,并且Flink已经为他们提供了很好的默认值。
以上摘自宋辛童老师在issue FLINK-18427: 关于堆外内存溢出的问题 中的评论,所以这里调大taskmanager.memory.task.off-heap.size也能解决问题。
总结
目前看来,主要还是flink提供的资源配置无法实现自动调优,需要用户根据实际情况进行手动调整,调优难度较大,因此后续增加自动调整很有必要。另外对于job不断重启的问题,根本原因还是Elasticsearch客户端存在死锁问题导致,包括反压、checkpoint超时和job重启都是因为sink hang住导致,通过升级Elasticsearch客户端版本可以解决。但是对于taskmanager.memory.framework.off-heap.size
的值应该如何设置,有没有经验值,需要进一步研究。目前这个值还是测出来的。