4. 强制段合并
代码入口:org.elasticsearch.action.admin.indices.forcemerge.TransportForceMergeAction#shardOperation
对于待合并处理的分片,需要先校验该分片的状态
/**
* 判断分片状态是否为STARTED,如果已被关闭或异常,则无法merge
*/
protected final void verifyActive() throws IllegalIndexShardStateException {
IndexShardState state = this.state;
if (state != IndexShardState.STARTED) {
throw new IllegalIndexShardStateException(shardId, state, "operation only allowed when shard is active");
}
}
进入到执行入口:org.elasticsearch.index.engine.InternalEngine#forceMerge;开始获取mergePolicy和mergeScheduler,即合并的策略和调度器(ES7.5.2默认使用TieredMergePolicy策略及ConcurrentMergeScheduler执行器)。
// 获取mergePolicy,ES
ElasticsearchMergePolicy mp = (ElasticsearchMergePolicy) indexWriter.getConfig().getMergePolicy();
从上面的代码看到,mergePolicy是从indexWriter的config中获取的,因此看下indexWriterConfig如何生成。
// ES的mergeScheduler,调度器具体的执行过程(及Lucene自带Scheduler的异同),下面具体分析代码
private final ElasticsearchConcurrentMergeScheduler mergeScheduler;
// 初始化IndexWriterConfig
private IndexWriterConfig getIndexWriterConfig() {
// IndexWriterConfig继承自LiveIndexWriterConfig,如果没有设置,默认为TieredMergePolicy和ConcurrentMergeScheduler
final IndexWriterConfig iwc = new IndexWriterConfig(engineConfig.getAnalyzer());
// 此处省略与merge无关的代码
/ ... ... /
// 设置mergeScheduler,即上面声明的ElasticsearchConcurrentMergeScheduler
iwc.setMergeScheduler(mergeScheduler);
// 从config中获取mergePolicy
MergePolicy mergePolicy = config().getMergePolicy();
// 如果开启了softDelete,则初始化一个RecoverySourcePruneMergePolicy
if (softDeleteEnabled) {
mergePolicy = new RecoverySourcePruneMergePolicy(SourceFieldMapper.RECOVERY_SOURCE_NAME, softDeletesPolicy::getRetentionQuery,
new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, softDeletesPolicy::getRetentionQuery,
new PrunePostingsMergePolicy(mergePolicy, IdFieldMapper.NAME)));
}
// 设置mergePolicy
iwc.setMergePolicy(new ElasticsearchMergePolicy(mergePolicy));
// 使用cfs复合文件(从该角度出发,如果发现segment不是cfs,则表明很可能(而不是一定)已经做过段合并)
iwc.setUseCompoundFile(true);
return iwc;
}
现在知道了mergePolicy及mergeScheduler入口,需要来分别看下,在ES侧两者的实现过程,及与Lucene默认的异同。
4.1 MergePolicy
在setMergePolicy中,先获取一个mergePolicy,再初始化一个ElasticsearchMergePolicy;因此,先看下传入的mergePolicy,mergePolicy从engineConfig中获取,可以在org.elasticsearch.index.shard.IndexShard #newEngineConfig中看到,mergePolicy从indexSettings中获取。
// indexSettings中初始化一个MergePolicyConfig
this.mergePolicyConfig = new MergePolicyConfig(logger, this);
// 初始化一个EsTieredMergePolicy
private final EsTieredMergePolicy mergePolicy = new EsTieredMergePolicy();
// mergePolicy的配置
MergePolicyConfig(Logger logger, IndexSettings indexSettings) {
// 次数省略获取settings值的代码
/... .../
maxMergeAtOnce = adjustMaxMergeAtOnceIfNeeded(maxMergeAtOnce, segmentsPerTier);
// 如果合并的大小,超过该索引的这个比例(默认10%),则将禁用cfs
mergePolicy.setNoCFSRatio(indexSettings.getValue(INDEX_COMPOUND_FORMAT_SETTING));
// 当forceMergeDeletes被调用时,只删除“段内待删除数据占比大于这个值(默认10%)的段”
mergePolicy.setForceMergeDeletesPctAllowed(forceMergeDeletesPctAllowed);
// 小于该值的段,其大小都将被当做该值
mergePolicy.setFloorSegmentMB(floorSegment.getMbFrac());
// 执行一次正常合并(非force)操作,最多可包含的段的个数
mergePolicy.setMaxMergeAtOnce(maxMergeAtOnce);
// 在forceMerge或forceMergeDeletes时,同一时刻在合并的段的最大个数
mergePolicy.setMaxMergeAtOnceExplicit(maxMergeAtOnceExplicit);
// 1.限制合并段集的总量;2.如果段的大小超过该值的一半,则不参与段合并
mergePolicy.setMaxMergedSegmentMB(maxMergedSegment.getMbFrac());
// 层级的概念:每次可以挑选oneMerge的总段集;例如本次需要从1~10号中,选择5个段构成oneMerge,则1~10号为一个层级
// 每一层中,包含至少这个数量的段,才允许开始合并(如果待删除的数据量,大于下一行的值,则该条件无效)
mergePolicy.setSegmentsPerTier(segmentsPerTier);
// 待删除的数据量,大于该值,则上一个参数无效;该值需要在20~50之间,默认33
mergePolicy.setDeletesPctAllowed(deletesPctAllowed);
}
下面来看EsTieredMergePolicy的具体实现过程。
// 传入的mergePolicy,继承自FilterMergePolicy
final class EsTieredMergePolicy extends FilterMergePolicy {
// 常规merge为Lucene侧的TieredMergePolicy
final TieredMergePolicy regularMergePolicy;
// forcemerge为Lucene侧的TieredMergePolicy
final TieredMergePolicy forcedMergePolicy;
EsTieredMergePolicy() {
// 父类构造方法,传入一个TieredMergePolicy
super(new TieredMergePolicy());
// 初始化regularMergePolicy,此处的in,即为上一行父类构造方法中传入的TieredMergePolicy
regularMergePolicy = (TieredMergePolicy) in;
// 初始化forcemergePolicy
forcedMergePolicy = new TieredMergePolicy();
// 设置forceMerge合并时的最大值,强制段合并不做限制
forcedMergePolicy.setMaxMergedSegmentMB(Double.POSITIVE_INFINITY);
}
// 返回待合并的段信息,即oneMerge实例的list;oneMerge表示待合并的段集
// 假设10个段,其中1~5号需要合并一次,6~10号合并一次;则1~5号的段集构成一个oneMerge,6~10号的段集构成一个oneMerge
@Override
public MergeSpecification findForcedMerges(SegmentInfos infos, int maxSegmentCount,
Map<SegmentCommitInfo, Boolean> segmentsToMerge, MergeContext mergeContext) throws IOException {
return forcedMergePolicy.findForcedMerges(infos, maxSegmentCount, segmentsToMerge, mergeContext);
}
// 返回待(物理)删除数据的oneMerge实例列表
@Override
public MergeSpecification findForcedDeletesMerges(SegmentInfos infos, MergeContext mergeContext) throws IOException {
return forcedMergePolicy.findForcedDeletesMerges(infos, mergeContext);
}
// max_merged_segment参数,只对普通merge生效;forcemerge无限制。(与其他set方法不同)
public void setMaxMergedSegmentMB(double mbFrac) {
regularMergePolicy.setMaxMergedSegmentMB(mbFrac);
}
// 此处省略mergePolicy的其他get、set方法,因为其他的set,会将参数分别设置给regularMergePolicy和forcedMergePolicy
/... .../
}
从上面的代码看到,传入的mergePolicy只是在内部初始化TieredMergePolicy,并设置相应的参数值;接下来看下设置的mergePolicy,即ElasticsearchMergePolicy。
/**
* 该策略主要是对待合并段的版本进行了兼容,即比对段的版本号,将老旧的版本升级到当前版本
*/
public final class ElasticsearchMergePolicy extends FilterMergePolicy {
// 是否需要在合并的过程中,升级版本号较低的段
private volatile boolean upgradeInProgress;
// 是否只升级较老的段
private volatile boolean upgradeOnlyAncientSegments;
// 一次可以升级的最大的段数量
private static final int MAX_CONCURRENT_UPGRADE_MERGES = 5;
// 构造方法
public ElasticsearchMergePolicy(MergePolicy delegate) {
super(delegate);
}
// 判断是否需要升级
private boolean shouldUpgrade(SegmentCommitInfo info) {
// 从当前段信息中,获取版本号
org.apache.lucene.util.Version old = info.info.getVersion();
// 当前ES版本对应的Lucene版本号
org.apache.lucene.util.Version cur = Version.CURRENT.luceneVersion;
// 只能向下兼容,不能兼容未来版本
assert old.major <= cur.major;
// 比较大版本号
if (cur.major > old.major) {
return true;
}
// 比较小版本号(在允许小版本差异升级的情况下)
if (upgradeOnlyAncientSegments == false && cur.minor > old.minor) {
return true;
}
// 版本足够新(或不需要对小版本进行升级),不需要升级
return false;
}
// 寻找oneMerge列表
@Override
public MergeSpecification findForcedMerges(SegmentInfos segmentInfos,
int maxSegmentCount, Map<SegmentCommitInfo,Boolean> segmentsToMerge, MergeContext mergeContext)
throws IOException {
// 如果需要升级
if (upgradeInProgress) {
// 初始化一个MergeSpecification(一个oneMerge的列表),将需要升级的段放进去
MergeSpecification spec = new MergeSpecification();
// 遍历所有的段信息
for (SegmentCommitInfo info : segmentInfos) {
// 判断当前段是否需要升级
if (shouldUpgrade(info)) {
// 将需要升级的段加到oneMerge列表中
spec.add(new OneMerge(Collections.singletonList(info)));
}
// 需要升级的段的数量达标后,将该MergeSpecification返回去处理(将通过联级调用继续执行)
if (spec.merges.size() == MAX_CONCURRENT_UPGRADE_MERGES) {
return spec;
}
}
// 遍历过所有的段后,如果有需要升级的段,则返回MergeSpecification去处理
if (spec.merges.isEmpty() == false) {
return spec;
}
// 已经处理结束,不需要再升级
upgradeInProgress = false;
}
// 不需要升级时,直接调用lucene接口,返回待合并的oneMerge列表
return super.findForcedMerges(segmentInfos, maxSegmentCount, segmentsToMerge, mergeContext);
}
// 设置参数:upgrade表示是否升级;onlyAncientSegments表示是否只对老版本(大版本)升级
// 这两个参数不对外,由内部直接设置;forcemerge入口处,均为false
public void setUpgradeInProgress(boolean upgrade, boolean onlyAncientSegments) {
this.upgradeInProgress = upgrade;
this.upgradeOnlyAncientSegments = onlyAncientSegments;
}
}
综上,ES侧的mergePolicy,主要就是初始化TieredMergePolicy,分为normalMerge和forceMerge,最大的合并段大小只在normalMerge作用,forceMerge不做限制,同时对历史版本的段文件做了一次升级;而Lucene侧的TieredMergePolicy,在下面merge具体的执行过程中,再深入去看。
4.2 MergeScheduler
在上面mergePolicy,getIndexWriterConfig部分代码,看到了mergeScheduler,es侧则是使用了ElasticsearchConcurrentMergeScheduler,下面看下这部分具体是怎么实现的。
// 此处代码在InternalEngine,为方便理解,只贴了相关代码行
// 声明一个mergeScheduler
private final ElasticsearchConcurrentMergeScheduler mergeScheduler;
// 初始化mergeScheduler
mergeScheduler = scheduler = new EngineMergeScheduler(engineConfig.getShardId(), engineConfig.getIndexSettings());
// 在indexWriterConfig中设置该mergeScheduler
iwc.setMergeScheduler(mergeScheduler);
从上面看到,ElasticsearchConcurrentMergeScheduler类型的mergeScheduler,通过初始化一个EngineMergeScheduler对象获得,ElasticsearchConcurrentMergeScheduler最终是继承自Lucene的ConcurrentMergeScheduler;因此,我们先来看EngineMergeScheduler是怎么实现的。
private final class EngineMergeScheduler extends ElasticsearchConcurrentMergeScheduler {
// 原子操作,用来记录当前参与合并的线程数量
private final AtomicInteger numMergesInFlight = new AtomicInteger(0);
// 原子操作,用来记录当前是否已经开始节流
private final AtomicBoolean isThrottling = new AtomicBoolean();
// 构造方法
EngineMergeScheduler(ShardId shardId, IndexSettings indexSettings) {
super(shardId, indexSettings);
}
// 在merge前,检查当前参与合并的线程数,是否已经达到参数值,如果达到需要节流,控制线程数
public synchronized void beforeMerge(OnGoingMerge merge) {
// 获取 max_merge_count值
int maxNumMerges = mergeScheduler.getMaxMergeCount();
// 如果当前参与合并的线程数,已经大于了max_merge_count,则需要节流
if (numMergesInFlight.incrementAndGet() > maxNumMerges) {
// 之前没有节流,现在要开始了
if (isThrottling.getAndSet(true) == false) {
// 激活节流,获取一个lock
activateThrottling();
}}}
// 在merge后的操作
public synchronized void afterMerge(OnGoingMerge merge) {
// 获取 max_merge_count值
int maxNumMerges = mergeScheduler.getMaxMergeCount();
// 如果当前参与合并的线程数,小于参数值
if (numMergesInFlight.decrementAndGet() < maxNumMerges) {
// 设为false,表示要停止节流
if (isThrottling.getAndSet(false)) {
// 停止节流,释放lock
deactivateThrottling();
}
}
// 如果没有需要继续合并的任务;且距离最后一次写动作的时间,已经大于indices.memory.shard_inactive_time的值(默认5分钟)
if (indexWriter.hasPendingMerges() == false &&
System.nanoTime() - lastWriteNanos >= engineConfig.getFlushMergesAfter().nanos()) {
// 从线程池中获取一个线程,去执行flush操作
engineConfig.getThreadPool().executor(ThreadPool.Names.FLUSH).execute(new AbstractRunnable() {
@Override
protected void doRun() {
if (tryRenewSyncCommit() == false) {
flush();
}
}
});
} else if (merge.getTotalBytesSize() >= engineConfig.getIndexSettings().getFlushAfterMergeThresholdSize().getBytes()) {
// 如果一次merge的大小,超过index.flush_after_merge参数值(默认512MB),则需要触发flush操作,释放内存
// 此处为原子操作,初始为false,由其他线程调用,获取值后进行flush操作
shouldPeriodicallyFlushAfterBigMerge.set(true);
}
}
// 此处省略异常处理部分,主要是日志打印
/... .../
}
// 线程节流操作
public void deactivateThrottling() {
// 获取当前可用的线程数,在创建一个线程时,该值减一
int count = throttleRequestCount.decrementAndGet();
// 如果可用线程数为0,则需要控制线程数
if (count == 0) {
// 实质是切换为NoOpLock
throttle.deactivate();
}
}
从上面看到,外层的mergeScheduler主要是根据max_merge_count,在合并前后对线程数做了控制,同时,在合并结束后,触发flush操作;下面看下ElasticsearchConcurrentMergeScheduler。
// 这个scheduler是对ConcurrentMergeScheduler的补充,主要是跟踪merge,来获取合并耗时、当前合并的数据量、待合并的总数据量
class ElasticsearchConcurrentMergeScheduler extends ConcurrentMergeScheduler {
// 此处代码省略变量的声明、日志打印,及其他get方法
/... .../
// 构造方法
ElasticsearchConcurrentMergeScheduler(ShardId shardId, IndexSettings indexSettings) {
this.config = indexSettings.getMergeSchedulerConfig();
this.shardId = shardId;
this.indexSettings = indexSettings.getSettings();
// 更新配置信息,下面有该方法的具体操作
refreshConfig();
}
@Override
protected void doMerge(IndexWriter writer, MergePolicy.OneMerge merge) throws IOException {
// 获取当前oneMerge的总文档数
int totalNumDocs = merge.totalNumDocs();
// 获取当前oneMerge的数据量
long totalSizeInBytes = merge.totalBytesSize();
// 获取系统时间
long timeNS = System.nanoTime();
// 当前正在merge的数量加一
currentMerges.inc();
// 累加当前正在合并的文档数
currentMergesNumDocs.inc(totalNumDocs);
// 累加当前正在合并的段大小
currentMergesSizeInBytes.inc(totalSizeInBytes);
// 记录正在合并的oneMerge,其内部通过id和oneMerge对应关系进行记录
OnGoingMerge onGoingMerge = new OnGoingMerge(merge);
// 正在合并的oneMerge集合
onGoingMerges.add(onGoingMerge);
try {
// 即上面EngineMergeScheduler的节流操作
beforeMerge(onGoingMerge);
// 调用lucene的ConcurrentMergeScheduler,进行merge操作,后面打开看
super.doMerge(writer, merge);
} finally {
// 计算merge的耗时
long tookMS = TimeValue.nsecToMSec(System.nanoTime() - timeNS);
// 从正在merge的集合中移除
onGoingMerges.remove(onGoingMerge);
// 即上面EngineMergeScheduler的节流更新,及flush操作
afterMerge(onGoingMerge);
// 当前正在合并的merge个数减一
currentMerges.dec();
// 从“正在合并的文档数”中减去已完成的文档数
currentMergesNumDocs.dec(totalNumDocs);
// 从“正在合并的数据量”中减去已完成的数据量
currentMergesSizeInBytes.dec(totalSizeInBytes);
// 总合并文档数累加
totalMergesNumDocs.inc(totalNumDocs);
// 总合并的数据量累加
totalMergesSizeInBytes.inc(totalSizeInBytes);
// 总耗时累加
totalMerges.inc(tookMS);
// 在OneMergeProgress中,通过reason获取stop的时间(吞吐率设为0)
long stoppedMS = TimeValue.nsecToMSec(
merge.getMergeProgress().getPauseTimes().get(MergePolicy.OneMergeProgress.PauseReason.STOPPED)
);
// 在OneMergeProgress中,通过reason获取pause的时间(吞吐率过大)
long throttledMS = TimeValue.nsecToMSec(
merge.getMergeProgress().getPauseTimes().get(MergePolicy.OneMergeProgress.PauseReason.PAUSED)
);
// stop耗时累加
totalMergeStoppedTime.inc(stoppedMS);
// pause耗时累加
totalMergeThrottledTime.inc(throttledMS);
}
}
// 创建并返回一个merge线程
protected MergeThread getMergeThread(IndexWriter writer, MergePolicy.OneMerge merge) throws IOException {
// 调用父类的方法获取,实质上父类是创建了一个守护线程,run方法则是执行doMerge
MergeThread thread = super.getMergeThread(writer, merge);
// 设置名字
thread.setName(EsExecutors.threadName(indexSettings, "[" + shardId.getIndexName() + "][" + shardId.id() + "]: " + thread.getName()));
return thread;
}
MergeStats stats() {
// 初始化一个mergeStats,用来记录merge的具体信息
final MergeStats mergeStats = new MergeStats();
// 此处就是将上述获取的时间、文档数、数据量等塞进去
mergeStats.add(...);
// 返回merge信息,在InternalEngine中,提供public方法获取该对象
return mergeStats;
}
// 构造方法中的配置更新
void refreshConfig() {
// 如果参数max_merge_count和max_thread_count与默认值不同,则需要设置更新
if (this.getMaxMergeCount() != config.getMaxMergeCount() || this.getMaxThreadCount() != config.getMaxThreadCount()) {
this.setMaxMergesAndThreads(config.getMaxMergeCount(), config.getMaxThreadCount());
}
// 每个merge的写入速率,如果该值无上限,则说明IO没有节流
boolean isEnabled = getIORateLimitMBPerSec() != Double.POSITIVE_INFINITY;
// 如果开启了自动IO节流,但是当前的速率没有被限制,则需要开启IO节流
if (config.isAutoThrottle() && isEnabled == false) {
// 开启IO节流:设置开始速度为20MB/s(IO节流具体操作将在其父类的调度中看到)
enableAutoIOThrottle();
} else if (config.isAutoThrottle() == false && isEnabled) {
// 如果自动IO节流没有开启,但是速率却被限制了,需要取消限制
disableAutoIOThrottle();
}
}
}
综上,可以看到在ES侧的mergeScheduler有两种节流方式,一个是线程数,一个是IO;线程数在beforeMerge,即merge前累加当前在merge的线程数,在afterMerge中减去已完成的线程数,通过lock的切换来实现线程数节流。IO则是在ElasticsearchConcurrentMergeScheduler初始化时,更新启动速率,供后续使用(判断是否积压任务,调整速率到当前的一个百分比,有积压调整为120%,最大1GB,没有积压调整为90%,最小5MB;forcemerge则无该限制)。段合并的merge操作,是在mergeThread中进行,通过调用父类的getMergeThread获取线程。同时,ES对合并的文档数、数据量、耗时进行了记录,在InternalEngine中提供了plublic方法进行获取。
4.3 doMerge
回归主线,继续看InternalEngine#forceMerge的执行过程。forcemerge的执行过程如下,即根据入参判断,如果只删除数据,则调用forceMergeDeletes,如果maxNumSegments参数不大于0则调用maybeMerge,否则调用forcemerge;则merge结束后,flush提交本次操作,数据刷盘,释放锁。
public void forceMerge(final boolean flush, int maxNumSegments, boolean onlyExpungeDeletes,
final boolean upgrade, final boolean upgradeOnlyAncientSegments) {
// 初始化一个mergePolicy,该策略在ES侧的实现上面已经看过
ElasticsearchMergePolicy mp = (ElasticsearchMergePolicy) indexWriter.getConfig().getMergePolicy();
// 获取一个锁
optimizeLock.lock();
try {
// 验证该分片的engine是started状态
ensureOpen();
// 如果需要升级(我们手动触发的forcemerge,此处是false)
if (upgrade) {
// 设置是否升级的参数,如何获取需要升级的segment在上面章节已经看过
mp.setUpgradeInProgress(true, upgradeOnlyAncientSegments);
}
// 增加一个引用计数,防止在合并过程中,被其他操作关闭
store.incRef();
try {
// 如果只处理待删除的数据
if (onlyExpungeDeletes) {
// 调用lucene侧的forcemergeDelete接口
indexWriter.forceMergeDeletes(true);
} else if (maxNumSegments <= 0) {
// 调用lucene侧的maybeMerge接口,即ES认为的normalMerge
indexWriter.maybeMerge();
} else {
// 调用lucene侧的forcemerge,具体实现下面打开看
indexWriter.forceMerge(maxNumSegments, true);
}
// 合并完成后,如果需要flush
if (flush) {
if (tryRenewSyncCommit() == false) {
// 实际为调用lucene的commit接口
flush(false, true);
}
}
} finally {
// 释放锁
store.decRef();
}
} catch (Exception e) {
// 此处省略异常处理,主要是打印日志,抛异常
/... ../
} finally {
try {
// 重置升级参数,防止在出现异常后,参数未被重置
mp.setUpgradeInProgress(false, false);
} finally {
// 释放引用
ptimizeLock.unlock();
}
}
}
我们常用的forcemerge接口,并指定maxNumSegments,默认upgrade为flase,onlyExpungeDeletes为false,flush为true;因此先来看下lucene侧的IndexWriter#forceMerge方法执行过程。
// 此处的forceMerge是org.apache.lucene.index.IndexWriter提供
// ES对索引的读写,均是通过lucene侧提供的IndexWriter、IndexReader、IndexSearcher等来操作
public void forceMerge(int maxNumSegments, boolean doWait) throws IOException {
// 验证indexWriter是否开启,ES侧通过engine控制
ensureOpen();
// infoStream如果开启,则会将merge过程中的信息打印出来
if (infoStream.isEnabled("IW")) {
infoStream.message("IW", "forceMerge: index now " + segString());
infoStream.message("IW", "now flush at forceMerge");
}
// 执行flush,对应es中的refresh操作;并且在此处会有条件的触发maybeMerge,因为下面也会再调maybeMerge,此处先跳过
// 两个参数,分别是“是否触发merge”和“是否处理删除的数据”,后者则是在flush操作中使用
flush(true, true);
synchronized(this) {
// 重置异常:实际是使用list保存merge异常的oneMerge,此处即为初始化一个空的arrayList
resetMergeExceptions();
// 清空merge信息:实际是使用map记录segmentInfo的状态,value则是一个布尔值
segmentsToMerge.clear();
// 遍历所有的segmentInfo,value给为true则表示等待被合并
// 此处的segmentInfo是在indexWriter初始化时
for(SegmentCommitInfo info : segmentInfos) {
segmentsToMerge.put(info, Boolean.TRUE);
}
// mergeMaxNumSegments是一个全局变量,供IndexWriter使用
mergeMaxNumSegments = maxNumSegments;
// 遍历所有等待合并的oneMerge(在maybeMerge中会registerMerge,pendingMerges从此处赋值)
for(final MergePolicy.OneMerge merge : pendingMerges) {
// 将用户指定的最多段个数,设置进每个oneMerge中
merge.maxNumSegments = maxNumSegments;
if (merge.info != null) {
// 待合并的oneMerge
segmentsToMerge.put(merge.info, Boolean.TRUE);
}
}
// 遍历所有正在合并的oneMerge
for (final MergePolicy.OneMerge merge: runningMerges) {
// 将用户指定的最多段个数,设置进每个oneMerge中
merge.maxNumSegments = maxNumSegments;
if (merge.info != null) {
segmentsToMerge.put(merge.info, Boolean.TRUE);
}
}
}
// 调用maybeMerge,触发段合并操作;对于normal merge触发的maybeMerge,maxNumSegments无上限
maybeMerge(config.getMergePolicy(), MergeTrigger.EXPLICIT, maxNumSegments);
// 如果指定了wait,即任务阻塞,并等待所有的merge任务结束;es的forcemerge该参数为true,不对外开放
if (doWait) {
synchronized(this) {
while(true) {
// tragedy是一个Throwable的原子引用,当不为空时,说明有异常,需要关闭indexWriter,不能再进行merge
if (tragedy.get() != null) {
throw new IllegalStateException("this writer hit an unrecoverable error; cannot complete forceMerge", tragedy.get());
}
// 如果存在merge异常
if (mergeExceptions.size() > 0) {
// 获取每一个合并时的异常
final int size = mergeExceptions.size();
for(int i=0;i<size;i++) {
final MergePolicy.OneMerge merge = mergeExceptions.get(i);
// 如果不是normalMerge,需要把每个线程里的异常,全部抛给主线程,一起处理
if (merge.maxNumSegments != UNBOUNDED_MAX_MERGE_SEGMENTS) {
throw new IOException("background merge hit exception: " + merge.segString(), merge.getException());
}
}
}
// 如果pendingMerge或者runningMerge中,有任何一个oneMerge,都指定了maxNumSegments参数,表明该oneMerge正在或等待merge(即不为默认的-1)
if (maxNumSegmentsMergesPending())
// 等待1秒
doWait();
else
break;
}
}
// 再次检查indexWriter是否已经被关闭
ensureOpen();
}
}
上面看到了merge过程中,主要的三部分:1. 首先是flush,将buffer中的数据全部刷到系统缓存,并执行maybeMerge,此处的MergeTrigger类型是FULL_FLUSH,即文档提交时的合并;2. 触发maybeMerge,此处的MergeTrigger类型是EXPLICIT,表示由用户手动触发;3. 等待各个merge任务结束,如果子线程有异常,则将异常抛至主线程进行处理。
lucene侧的flush对应es侧的refresh;lucene侧的commit对应es侧的flush;因此,上面的flush可以在es refresh模块中再看,这部分主要看maybeMerge,而maybeMerge中,实际是调用mergeScheduler的merge方法进行merge调度;从上面mergeScheduler部分看到,es使用的是ConcurrentMergeScheduler,因此直接来看ConcurrentMergeScheduler的merge方法。
public synchronized void merge(IndexWriter writer, MergeTrigger trigger, boolean newMergesFound) {
// 合并线程数赋值:如果max_thread_count是-1,表示用户未指定,则按需要给默认值,否则什么也不做
// 设置策略:通过数据目录,判断当前使用的是旋转盘,还是固态硬盘(此处是lucene侧的策略,非es策略)
// 如果是旋转盘,则max_thread_count=1,max_merge_count=6
// 如果是固态硬盘,则maxThreadCount = Math.max(1, Math.min(4, coreCount/2)),maxMergeCount = maxThreadCount+5
// 注意:此处设置是在mergeScheduler中,而es在ElasticsearchConcurrentMergeScheduler的构造方法中就调用了refreshConfig去更新这两个参数
// 在refreshConfig中,es已经使用了固态硬盘的参数配置进行更新;如果是旋转盘,需要注意,参数值并非1和6
// 换句话说:lucene侧是根据磁盘类别来给默认值,而es继承后,直接将固态硬盘的推荐参数设置进去了
initDynamicDefaults(writer);
// 如果merge是由关闭索引时触发,我们手动触发的MergeTrigger是EXPLICIT
if (trigger == MergeTrigger.CLOSING) {
// 关闭IO节流
targetMBPerSec = MAX_MERGE_MB_PER_SEC;
updateMergeThreads();
}
// 一直循环,直到该indexWriter中等待merge的任务全部结束
while (true) {
if (maybeStall(writer) == false) {
break;
}
// 获取下一个oneMerge
OneMerge merge = writer.getNextMerge();
// 如果下一个oneMerge为空,说明该indexWriter的合并已经完成,直接退出
if (merge == null) {
return;
}
boolean success = false;
try {
// 获取到oneMerge之后,需要起一个merge线程
final MergeThread newMergeThread = getMergeThread(writer, merge);
// 添加到merge的集合中
mergeThreads.add(newMergeThread);
// 更新IO节流
updateIOThrottle(newMergeThread.merge, newMergeThread.rateLimiter);
// 合并线程执行合并操作,线程里面执行的doMerge,而上面的ElasticsearchConcurrentMergeScheduler重写了doMerge
// 重写的doMerge,则是在merge前后对线程数做了控制,并统计merge信息,最后调用flush
newMergeThread.start();
// 更新IO节流的配置(即最大速率更新)
updateMergeThreads();
success = true;
} finally {
if (!success) {
writer.mergeFinish(merge);
}
}
}
}