详解 Elasticsearch refresh 机制

本文解析Elasticsearch刷新机制,探讨配置及执行逻辑,介绍searchidle优化策略,深入Lucene层面揭示刷新流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概览

Refresh 是Elasticsearch的运行机制的一个环节,保证了索引信息的增删改变化能够被及时感知检索,本篇博客我们将深入理解refresh的配置,运行机制,执行逻辑等等内容。

Elasticsearch 将 refresh 指令分发到所有的 Index Shard 不是本篇博客的关注内容,我们主要关注的是每个Index Shard 上的refresh的执行逻辑。

refresh 配置

通过在index settings 里配置 index.refresh_interval 来指定refresh间隔

{
  "my_index" : {
    "settings" : {
      "index" : {
        "refresh_interval" : "2s"
      }
    }
  }
}

refresh 间隔可以设置秒,分,小时,天 等等,设置方式参考java.util.concurrent.TimeUnit,当 index.refresh_interval=-1 时关闭refresh功能。

refresh 在ES层执行逻辑


IndexService#new

每个Index的分片在实例化时会创建IndexService,专门负责处理索引的各种问题,包括增删改查,分片setting是,mappings更新,refresh等等,其在实例化时会创建一个AsyncRefreshTask 用于负责索引分片的refresh功能

public class IndexService extends AbstractIndexComponent implements IndicesClusterStateService.AllocatedIndex<IndexShard> {
	//......
	private volatile AsyncRefreshTask refreshTask;

	public IndexService(......){
		// ......
        this.refreshTask = new AsyncRefreshTask(this);
        // ......
	}
}

AsyncRefreshTask#new

AsyncRefreshTask 是负责refresh 索引的任务类,其在实例化时就在构造函数内执行了一些列的出发逻辑。

/**
 * 异步refresh任务
 */
final class AsyncRefreshTask extends BaseAsyncTask {

    AsyncRefreshTask(IndexService indexService) {
        // 索引service, refresh间隔
        super(indexService, indexService.getIndexSettings().getRefreshInterval());
    }

    @Override
    protected void runInternal() {
        // 执行 refresh
        indexService.maybeRefreshEngine(false);
    }

    /**
     * 指定线程池
     *
     * @return
     */
    @Override
    protected String getThreadPool() {
        return ThreadPool.Names.REFRESH;
    }
}

BaseAsyncTask#new

AsyncRefreshTask 继承 自 BaseAsyncTask 其在构造函数内就执行了 rescheduleIfNecessary()

abstract static class BaseAsyncTask extends AbstractAsyncTask {

    protected final IndexService indexService;

    BaseAsyncTask(final IndexService indexService, final TimeValue interval) {
        super(indexService.logger, indexService.threadPool, interval, true);
        this.indexService = indexService;
        // 重新调度当前任务
        rescheduleIfNecessary();
    }
    //......
}

AbstractAsyncTask#rescheduleIfNecessary

BaseAsyncTask 在构造函数内就执行 rescheduleIfNecessary(),这方法是其父类 AbstractAsyncTask 定义的函数

public abstract class AbstractAsyncTask implements Runnable, Closeable {
	// refresh 线程池
    private final ThreadPool threadPool;
	// index.refresh_interva 参数 指定的refresh间隔
    private volatile TimeValue interval;

    protected AbstractAsyncTask(Logger logger, ThreadPool threadPool, TimeValue interval, boolean autoReschedule) {
        // ......
        this.threadPool = threadPool;
        this.interval = interval;
    }

    public synchronized void rescheduleIfNecessary() {
       // ......
       if (interval.millis() > 0 && mustReschedule()) {
           if (logger.isTraceEnabled()) {
               logger.trace("scheduling {} every {}", toString(), interval);
           }
           // 调度当前任务
           cancellable = threadPool.schedule(this, interval, getThreadPool());
           isScheduledOrRunning = true;
       } else {
           logger.trace("scheduled {} disabled", toString());
           cancellable = null;
           isScheduledOrRunning = false;
       }
   }

}

AsyncRefreshTask 在实例化时就启动调度任务,执行其 runInternal() 方法(方法重写逻辑这里没有详细说明),也就是 indexService.maybeRefreshEngine(false); ,非强制性的执行 refresh 任务。

refresh 在Lucene层执行逻辑


Elasticsearch 曾的refresh最终会调用到 Lucene的 SearcherManager#refreshIfNeeded(...) 方法:

SearcherManager#refreshIfNeeded

public final class SearcherManager extends ReferenceManager<IndexSearcher> {

	// 在需要时刷新参数指定的Index
	protected IndexSearcher refreshIfNeeded(IndexSearcher referenceToRefresh) throws IOException {
        final IndexReader r = referenceToRefresh.getIndexReader();
        assert r instanceof DirectoryReader : "searcher's IndexReader should be a DirectoryReader, but got " + r;
        // 如果当前索引目录有变化,比如生成了代表修改删除的.liv 文件,内存中有新写入的未生成segment的document,对当前索引目录生成一个新的Reader
        final IndexReader newReader = DirectoryReader.openIfChanged((DirectoryReader)r);
        // 如果新的Reader == null, 也就是说目录内数据未发生变化
        if (newReader == null) {
            return null;
        } else {
            // 通过新的Reader 生成新的IndexSearcher
            return getSearcher(searcherFactory, newReader, r);
        }
    }

}

StandardDirectoryReader#doOpenIfChanged

public final class StandardDirectoryReader extends DirectoryReader {

	private IndexWriter writer;

	protected DirectoryReader doOpenIfChanged(final IndexCommit commit) throws IOException {
        // If we were obtained by writer.getReader(), re-ask the
        // writer to get a new reader.
        if (writer != null) {
            return doOpenFromWriter(commit);
        } else {
            return doOpenNoWriter(commit);
        }
    }

	/**
     * 重新打开一个Reader
     *
     * @param commit
     * @return
     * @throws IOException
     */
    private DirectoryReader doOpenFromWriter(IndexCommit commit) throws IOException {
        if (commit != null) {
            return doOpenFromCommit(commit);
        }
        // segmentInfos 是否发生变化, 具体逻辑查看下一个方法
        if (writer.nrtIsCurrent(segmentInfos)) {
            return null;
        }
		// 重新生成一个目录的Reader
        DirectoryReader reader = writer.getReader(applyAllDeletes, writeAllDeletes);

        // If in fact no changes took place, return null:
        if (reader.getVersion() == segmentInfos.getVersion()) {
            reader.decRef();
            return null;
        }

        return reader;
    }

}

IndexWriter#nrtIsCurrent

public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {

	/**
     * 当前segmengInfos版本没有发生变化, 比如是否有新的segment生成, 内存总是否有新写入的没有写入到segment的document等等
     *
     * @param infos
     * @return
     */
    synchronized boolean nrtIsCurrent(SegmentInfos infos) {
        ensureOpen();
        boolean isCurrent = infos.getVersion() == segmentInfos.getVersion()
            && docWriter.anyChanges() == false      // 内存中是否有document, 也就是不在segment里的 document
            && bufferedUpdatesStream.any() == false // 内存中是否有更新的数据
            && readerPool.anyChanges() == false;
        // ......
        }
        return isCurrent;
    }

}

当有新的document被写入,或者修改,删除等情况时,会触发重新构建 DirectoryReader ,也就是重新关联索引目录下的所有数据。

IndexWriter#getReader

public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {

	final DocumentsWriter docWriter;

	DirectoryReader getReader(boolean applyAllDeletes, boolean writeAllDeletes) throws IOException {
		// ......
		DirectoryReader r = null;
		try {
            boolean success = false;
            synchronized (fullFlushLock) {
                try {
                    // 1.flush 所有DWPT, Document Writer Per Thread
                    long seqNo = docWriter.flushAllThreads();
                   // ......
                    synchronized (this) {
                        // ......
                        // 重新打开索引目录
                        r = StandardDirectoryReader.open(this, segmentInfos, applyAllDeletes, writeAllDeletes);
                        // ......
                    }
                    success = true;
                } finally {
                   // ......
            }
            anyChanges |= maybeMerge.getAndSet(false);
            if (anyChanges) {
            	// 2. 触发 Merge, 也就是每次refresh 都有可能出发segment 合并
                maybeMerge(config.getMergePolicy(), MergeTrigger.FULL_FLUSH, UNBOUNDED_MAX_MERGE_SEGMENTS);
            }
            // ......
        } catch (AbortingException | VirtualMachineError tragedy) {
           ......
        } 
         return r;
	}

}

上述代码中,有两个步骤比较关键,就是 flushAllThreads()merge(......)

docWriter.flushAllThreads() (关键内容)

这里的Threads并不是线程的意思,而是DocumentWriterPerThread(DWPT)的对象,Lucene维护了一个DWPT的列表,专门用于写入Document数据,当Flush时,每个DWPT的数据都能生成一个Segment。

每个DWPT对象同一时间只能被一个线程使用,当一个Index线程在获取一个DWPT对象时,如果有空闲的DWPT对象,则从空闲列表中获取锁定一个;当没有空闲的DWPT时,创建一个,在用完后放入空闲列表中;

当Elasticsearch写入压力很大时,Lucene的DWPT的池子容量会增大,上限是和Elasticsearch的 index 线程池的线程数上限一致。所以当写入压力较大时,适当增大 index 的线程池的线程数上限能够增大写入吞吐量。

Elasticsearch 每次 refresh时,大部分场景下都会调用 Lucene 的 flush,每个DWPT对象持有的数据都会生成一个Segment,在Flush后释放DWPT池的所有对象,之后Index时重新创建。

每个DWPT在Flush时都会在内存中生成一个segment,但没有实时Sync同步到磁盘,没有调用commit 提交数据到磁盘,具体调用时机有操作系统自行决定,比如当前操作系统比较空闲,则能立马同步磁盘,生成相应的segment文件,比如 .cfe,.cfs,.si 文件。


maybeMerge(…);

每一次flush,如果生成了新的segment,则都会触发 Segment Merge。触发 Merge 操作不表示一定会 merge segment,是否要merge是由 Merge Policy 来决定的。 Merge Policy 有ForceMergePolicy(ES强制merge),TieredMergePolicy(阶梯式的,Lucene默认策略)等等

refresh search idle

Refresh 的作用是将内存中新写入的document生成一个segment,基于系统文件形式缓存在内存中,这样Lucene就能够检索(Lucene基于系统文件缓存的形式来读取加载数据)。

如果当前系统没有检索的需求,那么即使数据不断的写入,延迟refresh的执行时间,那么就能一次性生成比较大的segment,而不是反复的生成小的segment,然后merge,这种情况下性能会有明显的提高,毕竟不需要多次的刷盘,文件句柄也能有效减少。search idle 就是基于上述原理做的一个refresh 优化策略。

在上文中我们展示了Elasticsearch 的 AsyncRefreshTask,即执行refresh的任务类,在创建实列时会定时调度执行refresh,具体代码如下:

IndexShard#scheduledRefresh

	/**
     * Executes a scheduled refresh if necessary.
     *
     * @return <code>true</code> iff the engine got refreshed otherwise <code>false</code>
     */
    public boolean scheduledRefresh() {
        verifyNotClosed();
        // 当前是否有refreshListener,也就是要强制refresh然后回调
        boolean listenerNeedsRefresh = refreshListeners.refreshNeeded();
        // 综合条件是否要执行当前调度的 refresh             在指定索引上是否有新的变化,比如增删改,有的话 need
        if (isReadAllowed() && (listenerNeedsRefresh || getEngine().refreshNeeded())) {

            if (listenerNeedsRefresh == false                       // if we have a listener that is waiting for a refresh we need to force it
                && isSearchIdle()                                   // 当前 index shard 是否是search idle
                && indexSettings.isExplicitRefresh() == false       // 没有显式的设置了index.refresh_interval
                && active.get()) {                                  // it must be active otherwise we might not free up segment memory once the shard became inactive
                // lets skip this refresh since we are search idle and
                // don't necessarily need to refresh. the next searcher access will register a refreshListener and that will
                // cause the next schedule to refresh.
                final Engine engine = getEngine();
                engine.maybePruneDeletes(); // try to prune the deletes in the engine if we accumulated some
                // 推迟refersh,刷新 pendingRefreshLocation, 也就是translog里refresh起始location
                setRefreshPending(engine);
                return false;
            } else {
                if (logger.isTraceEnabled()) {
                    logger.trace("refresh with source [schedule]");
                }
                return getEngine().maybeRefresh("schedule");
            }
        }
        final Engine engine = getEngine();
        engine.maybePruneDeletes(); // try to prune the deletes in the engine if we accumulated some
        return false;
    }

    /**
     * 判断当前index shard 是否是检索空闲状态
     * Returns true if this shards is search idle
     * @see IndexSettings#INDEX_SEARCH_IDLE_AFTER
     */
    final boolean isSearchIdle() {
        // 当前时间 - 最新的search时间 >= 设定的检索空闲时间
        return (threadPool.relativeTimeInMillis() - lastSearcherAccess.get()) >= indexSettings.getSearchIdleAfter().getMillis();
    }

	/**
     * 推迟refresh
     * @param engine
     */
    private void setRefreshPending(Engine engine) {
        // 上次refresh是的transLog location
        Translog.Location lastWriteLocation = engine.getTranslogLastWriteLocation();
        Translog.Location location;
        do {
            // 先获取上一次refresh 的 location
            // 目前这变量没有特殊作用,仅仅是做一个标识,有值时需要先 force refresh, 再 serach
            location = this.pendingRefreshLocation.get();
            // 如果之前已经设置了localtion, 那么不能讲location后退,因为会丢失数据
            if (location != null && lastWriteLocation.compareTo(location) <= 0) {
                break;
            }
            // 更新pending refresh 的 location
        } while (pendingRefreshLocation.compareAndSet(location, lastWriteLocation) == false);
    }

Elasticsearch 在 refresh前会做一个判断,如果索引可读且有必要refresh时才refresh,有必要的条件包括以下两步:

  1. 没有注册refreshListener,如果注册了要强制refresh,之后回调
  2. 索引上有变化,比如有写入, 修改,删除等操作

当满足了上述前置条件后,才算可以进行refresh,但是否确定要refresh还需要做进一步的条件判断,search idle 就在这一步骤生效。如果不立即refresh,需要满足下述条件

  1. 没有注册refreshListener,也就是没有强制refresh的请求
  2. 当前是search idle 状态
  3. 没有显式的设置index.refresh_interval,也就是refresh的间隔是默认值1s
  4. 当前index正常生效

当满足了上述所有条件时,会推迟refresh的执行时机,会标记 translog 上一次写的位置,下次refresh时从translog上次位置处开始回放。

上述条件中有一个不是很容易理解, search idle 状态,顾名思义,就是检索空闲,当一段时间没有检索请求时就是检索空闲状态,检索请求类型包括 search, get, explain。一段时间是多久,由参数 index.search.idle.after 指定,默认30s,也就是当30s内没有检索请求时,就会进入search idle 状态,会推迟refresh,那具体推迟到什么时候执行?

上述代码的最后一段 setRefreshPending ,将translog的上次写入位置标识下来,保持进变量 pendingRefreshLocation 中,然后在下次要执行 search 相关请求时先refresh 然后 searchpendingRefreshLocation目前没有特殊作用,仅仅是一个标识,当有值时需要先force refresh,再search,所以refresh其实和tranlog没有什么关系 。具体代码如下:

IndexShard#awaitShardSearchActive

	/**
     * 注册一个listener, 当shard 又接到一个search请求时 触发执行所有的pending refresh , refresh 基于 translog location
     * 也就是基于translog的指定位置, 后续的index,update,delete 数据都记录在translog的location后, 从此location后开始apply
     * 此方法主要用作定位transLog 的 location
     *
     * Registers the given listener and invokes it once the shard is active again and all
     * pending refresh translog location has been refreshed. If there is no pending refresh location registered the listener will be
     * invoked immediately.
     * @param listener the listener to invoke once the pending refresh location is visible. The listener will be called with
     *                 <code>true</code> if the listener was registered to wait for a refresh.
     */
    public final void awaitShardSearchActive(Consumer<Boolean> listener) {
        markSearcherAccessed(); // move the shard into non-search idle
        // 获取pending refresh 的 location
        final Translog.Location location = pendingRefreshLocation.get();
        // 如果存在则说明有必要先refresh 再 search。一般适用于 search idle 情况
        if (location != null) {
            // 添加refresh listener, 当接收到search请求时先触发resresh, 然后执行search
            addRefreshListener(location, (b) -> {
            	// 将translog refresh location 重置,重置 search idle 状态
                pendingRefreshLocation.compareAndSet(location, null);
                // 响应request
                listener.accept(true);
            });
        } else {
            listener.accept(false);
        }
    }

    public void addRefreshListener(Translog.Location location, Consumer<Boolean> listener) {
        final boolean readAllowed;
        if (isReadAllowed()) {
            readAllowed = true;
        } else {
            synchronized (mutex) {
                readAllowed = isReadAllowed();
            }
        }
        if (readAllowed) {
        	// force refresh, 强制refresh
            refreshListeners.addOrNotify(location, listener);
        } else {
            // we're not yet ready fo ready for reads, just ignore refresh cycles
            listener.accept(false);
        }
    }

从上述 search ilde 机制中我们能够知晓,当数据批量导入时,没有检索,持续30s后就会进入search idle 状态,refresh被无限推迟直到下一次search类型请求,所以在批量导入数据无检索请求时,修改index.refresh_interval其实没什么屌用,哈哈

总结

本篇博客从多个角度分享了Elasticsearch 的 refresh机制,从es到lucene的执行流程,配置方式,search idle 等等,因该能让你对refresh有一个比较深入的理解,有疑问欢迎留言。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值