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 es update模式只更新有的字段 flink写入es越来越慢_java

问题

资源配置困难

flink每个operator都可以配置并行度,具体每个operator应该设置多少并行度,每个TaskManager设置多少slot,多少内存,目前都是盲配的状态,主要有以下几个原因:

  • flink taskmanager提供很多内存参数,包括堆内存、堆外内存、metaspace内存等,粒度较细;
  • flink es update模式只更新有的字段 flink写入es越来越慢_apache_02

  • slot虽然是flink最小的资源单元,但并没有对资源做完全的隔离;
  • task和slot对应关系不确定,1个slot里可以运行多个task(都来自同一个Job的不同task的subtask允许共享slot,即share);
  • flink提供的默认资源配置很多时候无法满足要求

对此有几点建议:

  1. slot个数必须大于等于整个job的最大并行度,比如source(3)->window(5)->sink(4)这个链路,其中source为3个并发,window为5个并发,sink为4个并发,则最大并行度为5,slot个数必须大于等于5才能让job调度起来;
  2. kafka source的并行度应该和topic的分区个数对应来调整,一般要求并行度小于等于topic分区数,否则会存在空跑的task,浪费资源
  3. 尽可能地将 operator 的 subtask 链接(chain)在一起形成 task,每个 task 在一个线程中执行,可以减少线程之间的切换,减少消息的序列化/反序列化,减少数据在缓冲区的交换,减少了延迟的同时提高整体的吞吐量。
  4. 保证足够的并行度,但并行度也不是越大越好,太多会加重数据在多个solt/task manager之间数据传输压力,包括序列化和反序列化带来的压力。
  5. CPU资源是task manager上的solt共享的,注意监控CPU的使用。

反压和job重启

在flink standalone模式下运行job,从kafka读取数据发送到es,es版本为6.3.1,当并发度设置比较高的情况下,job运行一段时间后,出现严重反压,并且超过checkpoint超时时间(设置的10分钟)后job会重启,不断重启无法成功。

flink es update模式只更新有的字段 flink写入es越来越慢_flink_03

flink es update模式只更新有的字段 flink写入es越来越慢_apache_04

通过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之前发送数据时存在死锁的问题

flink es update模式只更新有的字段 flink写入es越来越慢_flink_05

描述翻译如下:

如果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();
}

整个逻辑图如下所示:

flink es update模式只更新有的字段 flink写入es越来越慢_apache_06

由于ES7.3.0之前的Scheduler只有1个工作线程,FlushRetry都共用这一个调度。但是大量数据发送失败后可能会导致死锁,从而导致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:?]

flink es update模式只更新有的字段 flink写入es越来越慢_java_07

该异常通常说明 JVM 的直接内存限制过小,或者存在直接内存泄漏(Direct Memory Leak)。 请确认用户代码及外部依赖中是否使用了 JVM 直接内存,以及如果使用了直接内存,是否配置了足够的内存空间。 可以通过调整堆外内存来增大直接内存限制。 有关堆外内存的配置方法,请参考 TaskManagerJobManager 以及 JVM 参数的相关文档。

flink es update模式只更新有的字段 flink写入es越来越慢_flink_08

Direct Memory包括Framework Off-Heap MemoryTask Off-Heap MemoryNetwork 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参数起了作用。

flink es update模式只更新有的字段 flink写入es越来越慢_flink_09

对于当前版本,任务(taskmanager.memory.task.off-heap.size)/框架(taskmanager.memory.framework.off-heap.size)内存之间几乎没有区别。它们为将来的优化做好了准备,我们计划在其中允许将任务管理器的内存动态地切片到插槽中(当前将其切成固定数量和大小的插槽)。然后,可以对任务内存进行切片以执行任务,而将框架内存保留给任务管理器的框架。通常,我们建议用户仅设置任务内存,因为框架内存非常稳定,并且Flink已经为他们提供了很好的默认值。

以上摘自宋辛童老师在issue FLINK-18427: 关于堆外内存溢出的问题 中的评论,所以这里调大taskmanager.memory.task.off-heap.size也能解决问题。

flink es update模式只更新有的字段 flink写入es越来越慢_flink_10

总结

目前看来,主要还是flink提供的资源配置无法实现自动调优,需要用户根据实际情况进行手动调整,调优难度较大,因此后续增加自动调整很有必要。另外对于job不断重启的问题,根本原因还是Elasticsearch客户端存在死锁问题导致,包括反压、checkpoint超时和job重启都是因为sink hang住导致,通过升级Elasticsearch客户端版本可以解决。但是对于taskmanager.memory.framework.off-heap.size的值应该如何设置,有没有经验值,需要进一步研究。目前这个值还是测出来的。