Elasticsearch-BulkProcessor浅析

Elasticsearch-BulkProcessor浅析

1 概述

可参考Elasticsearch Bulk Processor

BulkProcessor提供了一个简单的接口来实现批量提交请求(多种请求,如IndexRequest,DeleteRequest),且可根据请求数量、大小或固定频率进行flush提交。flush方式可选同步或异步。以下是一个官方例子

import org.elasticsearch.action.bulk.BackoffPolicy;
import org.elasticsearch.action.bulk.BulkProcessor;
import org.elasticsearch.common.unit.ByteSizeUnit;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.TimeValue;

// create a bulkProcessor
BulkProcessor bulkProcessor = BulkProcessor.builder(
        client,  
        new BulkProcessor.Listener() {
            @Override
            public void beforeBulk(long executionId,
                                   BulkRequest request) { 	
                                   request.numberOfActions() } 

            @Override
            public void afterBulk(long executionId,
                                  BulkRequest request,
                                  BulkResponse response) {
                                  response.hasFailures() } 

            @Override
            public void afterBulk(long executionId,
                                  BulkRequest request,
                                  Throwable failure) { 
                                  failure.getMessage() } 
        })
        // 每10000个request flush一次
        .setBulkActions(10000) 
        // bulk数据每达到5MB flush一次
        .setBulkSize(new ByteSizeValue(5, ByteSizeUnit.MB)) 
        // 每5秒flush一次
        .setFlushInterval(TimeValue.timeValueSeconds(5)) 
        // 0代表同步提交即只能提交一个request;
        // 1代表当有一个新的bulk正在累积时,1个并发请求可被允许执行
        .setConcurrentRequests(1) 
        // 设置当出现代表ES集群拥有很少的可用资源来处理request时抛出
        // EsRejectedExecutionException造成N个bulk内request失败时
        // 进行重试的策略,初始等待100ms,后面指数级增加,总共重试3次.
        // 不重试设为BackoffPolicy.noBackoff()
        .setBackoffPolicy(
            BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(100), 3)) 
        .build();

// add a IndexRequest to bulkprocessor
bulkProcessor.add(new IndexRequest("twitter", "_doc", "1").source(/* your doc here */));
// add a DeleteRequest to bulkprocessor
bulkProcessor.add(new DeleteRequest("twitter", "_doc", "2"));

// await close the bulkprocessor
bulkProcessor.awaitClose(10, TimeUnit.MINUTES);

2 源码

2.1 创建BulkProcessor

就是上述例子中的build()方法,创建时用了builder模式。

BulkProcessor.builder(client,listener)

其中client为TransportClient实例,listener bulk各事件监听器。

使用各种.setxxx方法设置属性,最后使用.build()方法创建一个BulkProcessor实例:

BulkProcessor(Client client, BackoffPolicy backoffPolicy, Listener listener, @Nullable String name, int concurrentRequests, int bulkActions, ByteSizeValue bulkSize, @Nullable TimeValue flushInterval) {
        this.bulkActions = bulkActions;
        this.bulkSize = bulkSize.bytes();

        this.bulkRequest = new BulkRequest();
        // 根据concurrentRequests值不同设置同步或异步Handler
        this.bulkRequestHandler = (concurrentRequests == 0) ? BulkRequestHandler.syncHandler(client, backoffPolicy, listener) : BulkRequestHandler.asyncHandler(client, backoffPolicy, listener, concurrentRequests);
		
        if (flushInterval != null) {
        // 设置了flushInterval,就会开启一个定时执行的线程池
            this.scheduler = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1, EsExecutors.daemonThreadFactory(client.settings(), (name != null ? "[" + name + "]" : "") + "bulk_processor"));
            this.scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
            this.scheduler.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
            // 按配置的flush定时提交bulk
            this.scheduledFuture = this.scheduler.scheduleWithFixedDelay(new Flush(), flushInterval.millis(), flushInterval.millis(), TimeUnit.MILLISECONDS);
        } else {
            this.scheduler = null;
            this.scheduledFuture = null;
        }
    }

2.2 Flush

在上述BulkProcessor的最后就启动了Flush定时任务,下面看看Flush任务具体是做什么:

class Flush implements Runnable {
        @Override
        public void run() {
            synchronized (BulkProcessor.this) {
                if (closed) {
                // 如果已经close,什么都不做
                    return;
                }
                if (bulkRequest.numberOfActions() == 0) {
                // 无数据,什么都不做
                    return;
                }
                //否则执行bulk提交
                execute();
            }
        }
    }

2.3 execute

Flush定时任务会调用execute方法提交bulk。

2.3.1 SyncBulkRequestHandler

同步的SyncBulkRequestHandler的execute方法如下:

public void execute(BulkRequest bulkRequest, long executionId) {
    boolean afterCalled = false;
    try {
        // 调用注册的listener的beforeBulk方法
        listener.beforeBulk(executionId, bulkRequest);
        // 根据重试策略同步的写入数据到ES
        BulkResponse bulkResponse = Retry
                .on(EsRejectedExecutionException.class)
                .policy(backoffPolicy)
                .withSyncBackoff(client, bulkRequest);
        afterCalled = true;
        // 调用注册的listener的afterBulk方法
        listener.afterBulk(executionId, bulkRequest, bulkResponse);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        if (!afterCalled) {
            //出错时调用afterBulk
            listener.afterBulk(executionId, bulkRequest, e);
        }
    } catch (Throwable t) {
        logger.warn("Failed to execute bulk request {}.", t, executionId);
        if (!afterCalled) {
            listener.afterBulk(executionId, bulkRequest, t);
        }
    }
}
2.3.2 AsyncBulkRequestHandler

该类的bulk提交是异步的。

  • 构造方法
    private AsyncBulkRequestHandler(Client client, BackoffPolicy backoffPolicy, BulkProcessor.Listener listener, int concurrentRequests) {
        super(client);
        this.backoffPolicy = backoffPolicy;
        assert concurrentRequests > 0;
        this.listener = listener;
        // 这里就是setConcurrentRequests的值
        this.concurrentRequests = concurrentRequests;
        // 这里创建了concurrentRequests个Semaphore许可
        this.semaphore = new Semaphore(concurrentRequests);
    }
    
  • execute方法如下:
    public void execute(final BulkRequest bulkRequest, final long executionId) {
        boolean bulkRequestSetupSuccessful = false;
        boolean acquired = false;
        try {
            listener.beforeBulk(executionId, bulkRequest);
            //申请许可,无许可时阻塞
            semaphore.acquire();
            acquired = true;
            Retry.on(EsRejectedExecutionException.class)
                    .policy(backoffPolicy)
                    // 异步方式提交bulk
                    .withAsyncBackoff(client, bulkRequest, new ActionListener<BulkResponse>() {
                        @Override
                        // es响应后调用此方法
                        public void onResponse(BulkResponse response) {
                            try {
                                listener.afterBulk(executionId, bulkRequest, response);
                            } finally {
                            	// 该bulk提交完了,才会释放许可
                                semaphore.release();
                            }
                        }
    
                        @Override
                        public void onFailure(Throwable e) {
                            try {
                                listener.afterBulk(executionId, bulkRequest, e);
                            } finally {
                            	// 该bulk提交完失败了,也会释放许可
                                semaphore.release();
                            }
                        }
                    });
            // 异步提交成功        
            bulkRequestSetupSuccessful = true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.info("Bulk request {} has been cancelled.", e, executionId);
            listener.afterBulk(executionId, bulkRequest, e);
        } catch (Throwable t) {
            logger.warn("Failed to execute bulk request {}.", t, executionId);
            listener.afterBulk(executionId, bulkRequest, t);
        } finally {
            if (!bulkRequestSetupSuccessful && acquired) {  
            // if we fail on client.bulk() release the semaphore
            // 即异步提交过程就失败了,也会释放许可
                semaphore.release();
            }
        }
    }
    

2.4 add

下面看看bulkProcessor.add(IndexRequest)方法,实际执行的是以下方法

// 注意该方法拥有全局对象锁
private synchronized void internalAdd(ActionRequest request, @Nullable Object payload) {
	// 确保没有close
    ensureOpen();
    // bulkRequest内部有一个ActionRequestList,会将request放入
    // 然后增加累积的size(每个request的source及50字节的head)
    bulkRequest.add(request, payload);
    // 该方法会判断当前request条数或字节数超过阈值则会执行execute方法
    // 需要注意的是这个execute方法还是需要申请许可,跟flush那个execute是同一个
    // 否则什么都不做
    executeIfNeeded();
}

3 补充 ES6改动

3.1 概述

ES6对BulkProcessor做了改动,这里主要分析下execute方法。

3.2 BulkProcessor.execute

// 当前需要在获取锁的情况下才能执行
private void execute() {
	// 获取当前的BulkRequest
    final BulkRequest bulkRequest = this.bulkRequest;
    final long executionId = executionIdGen.incrementAndGet();

	// 创建一个新的BulkRequest
    this.bulkRequest = new BulkRequest();
    // 用老的BulkRequest去执行
    this.bulkRequestHandler.execute(bulkRequest, executionId);
}

注意,该execute的几个调用位置都是使用BulkProcessor实例的对象级别synchronized锁定来保证线程安全的。

3.3 BulkRequestHandler构造方法

ES6中不再分同步异步的BulkRequestHandler,而是仅有一个:

public final class BulkRequestHandler {
    private final Logger logger;
    private final BiConsumer<BulkRequest, ActionListener<BulkResponse>> consumer;
    private final BulkProcessor.Listener listener;
    private final Semaphore semaphore;
    private final Retry retry;
    private final int concurrentRequests;

    BulkRequestHandler(BiConsumer<BulkRequest, ActionListener<BulkResponse>> consumer, BackoffPolicy backoffPolicy,
                       BulkProcessor.Listener listener, Scheduler scheduler, int concurrentRequests) {
        assert concurrentRequests >= 0;
        this.logger = Loggers.getLogger(getClass());
        // (bulkRequest, bulkListener) -> client.bulkAsync(bulkRequest, RequestOptions.DEFAULT, bulkListener)
        // 即带listener异步方式执行bulkRequest
        this.consumer = consumer;
        // 这就是我们设置的bulkProcessorListener
        this.listener = listener;
        // 这里就是setConcurrentRequests的值
        this.concurrentRequests = concurrentRequests;
        // 重试策略
        // es6中,默认为初始等待时间为50 ms,重试8次,总共耗时约5.1秒的策略。
        this.retry = new Retry(backoffPolicy, scheduler);
        // 创建concurrentRequests个Semaphore许可,默认为1个
        this.semaphore = new Semaphore(concurrentRequests > 0 ? concurrentRequests : 1);
    }
}

3.4 BulkRequestHandler.execute

public void execute(BulkRequest bulkRequest, long executionId) {
    Runnable toRelease = () -> {};
    boolean bulkRequestSetupSuccessful = false;
    try {
    	// 调用注册的listener的beforeBulk方法
        listener.beforeBulk(executionId, bulkRequest);
        //申请许可,无许可时阻塞
        semaphore.acquire();
        // toRelease的run方法执行这个许可的release操作
        toRelease = semaphore::release;
        // 初始化一个latch
        CountDownLatch latch = new CountDownLatch(1);
        retry.withBackoff(consumer, bulkRequest, new ActionListener<BulkResponse>() {
            @Override
            public void onResponse(BulkResponse response) {
                try {
                    listener.afterBulk(executionId, bulkRequest, response);
                } finally {
                    // 该bulk提交,获得成功响应,才会释放许可和latch
                    semaphore.release();
                    latch.countDown();
                }
            }

            @Override
            public void onFailure(Exception e) {
                try {
                    listener.afterBulk(executionId, bulkRequest, e);
                } finally {
                    // 该bulk提交失败,也会释放许可和latch
                    semaphore.release();
                    latch.countDown();
                }
            }
        }, Settings.EMPTY);
        // 异步提交成功
        bulkRequestSetupSuccessful = true;
        if (concurrentRequests == 0) {
        	// 并发设为0时,必须等待该次提交bulk结果,无论结果成功与否
            latch.await();
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        logger.info(() -> new ParameterizedMessage("Bulk request {} has been cancelled.", executionId), e);
        listener.afterBulk(executionId, bulkRequest, e);
    } catch (Exception e) {
        logger.warn(() -> new ParameterizedMessage("Failed to execute bulk request {}.", executionId), e);
        listener.afterBulk(executionId, bulkRequest, e);
    } finally {
        if (bulkRequestSetupSuccessful == false) {  // if we fail on client.bulk() release the semaphore
            // 即异步提交过程就失败了,也会释放许可
            toRelease.run();
        }
    }
}

也就是说,每次execute时,是异步的,正常情况下(直接抛错时会释放许可)方法返回时是不会释放许可的,这就控制了并发数。只有当异步请求返回调用了ActionListener时才会释放许可。

这里说execute必须在有锁条件下调用,是因为该方法内用到的一些对象并非线程安全。这里应该也是未来一个优化点。

3.5 retry.withBackoff

这个方法实际是调用我们之前定义的

BiConsumer<BulkRequest, ActionListener<BulkResponse>> bulkConsumer =
                (bulkRequest, bulkListener) -> client.bulkAsync(bulkRequest, RequestOptions.DEFAULT, bulkListener)

即:

//这里的this指 RetryHandler implements ActionListener<BulkResponse>
consumer.accept(bulkRequest, this)

接下去就会使用底层的CloseableHttpAsyncClient执行了,可立即返回,不再继续分析。

4 总结

4.1 关于并发控制

从以上代码可以看到,setConcurrentRequests这个值在异步bulk场景中是很关键的。

  • 比如setConcurrentRequests=2,且在某个较大bulk请求提交后,真实响应未在flush时间内返回,那么当flush间隔到了后第二个bulk请求也能申请到许可并提交bulk。

    或在数据量很大,很快达到action/size阈值触发提交时,也需要高并行度来提高效率。

  • 如果设置setConcurrentRequests=0,则是完全同步的bulk提交,每次提交需要等待上一次bulk提交任务完成后再等待flush时间才能继续下一次bulk提交。虽然此时semaphore许可数为1,但是需要做latch.await(),而latch.countDown()必须在异步请求回调listener后才会触发。

4.2 性能

4.2.1 BulkProcessor.add

当我们将各种request放入BulkProcessor时,始终会调用如下方法:

private synchronized void internalAdd(DocWriteRequest request, @Nullable Object payload) {
    ensureOpen();
    bulkRequest.add(request, payload);
    executeIfNeeded();
}

这里有一个BulkProcessor实例的对象级别同步锁。也就是说,当我们在高并发场景,所有请求要放入Bulk都会去竞争等待对象锁。

4.2.2 BulkRequestHandler.execute

该方法必须在拥有BulkRequestHandler实例的对象级别同步锁的情况下调用,跟上述一样,高并发场景下会竞争激烈。虽然名义上使用semaphore发放多个许可,但前提是后一个调用必须等待前一个调用走完全部execute异步方法流程。

4.2.3 小结

这些点都要引起注意,在实际使用场景中需要集合实际情况来决定如何使用BulkProcessor。

我们现在是每个index写入就共用一个RestHighLevelClient,以及一个BulkProcessor实例。在调优时就发现,机器负载都还没打满,但性能就一直上不去了。据我分析应该有上述两点同步锁竞争原因。我会尝试将BulkProcessor改为该index的每个写入线程创建一个,并且统一加大并发进行调试。待有结果后上来补充。

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 20
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值