最近一次高并发写入压测中,我们遇到了一个非常诡异的 BKD merge 崩溃。从报错看,很像 Easysearch 2.1.2 在 merge 阶段把 segment 读成了错误状态。典型错误是这样的:
java.lang.ArrayIndexOutOfBoundsException: Index -3 out of bounds for length 8 java.lang.ArrayIndexOutOfBoundsException: Index -4 out of bounds for length 8
异常栈最终落在 Lucene BKD 相关路径上:
BKDReader.readNodeData()BKDWriter.merge()Lucene90PointsWriter.merge()如果只看栈,很容易把问题归到 Easysearch 的 BKD merge 逻辑。但排查到最后,结论恰恰相反。
问题不在 Easysearch 的代码,而在 JDK 运行时。 更精确地说,是某个特定
Oracle GraalVM 21 构建中的
JVMCI/Graal JIT 路径,把 Lucene BKD 的热点代码执行错了。
它有几个特别迷惑人的特征:
ZSTD 和
best_compression 两种 codec 下都能复现实际命中过的字段包括
@timestamp、
size、
status、
_seq_no。所以这不是某个字段、某种 codec 或某个 mapping 的偶发问题。
一开始我们确实怀疑 merge reader,毕竟异常直接出现在 merge 路径上。但日志顺序很快给出了相反的证据。在 merge 真正崩溃之前,source segment 已经先出现了这些异常信号:
point-sort-restore-multiple-zero-ordssource-write-point-doc-mismatchpointCount > docCountpack-index-negative-codereader-invalid-start-posArrayIndexOutOfBoundsException这意味着两件事:merge reader 不是第一现场,source segment 在写出阶段就已经坏了。merge reader 只是读到了已经损坏的 BKD index,并在那个阶段暴露了异常。
继续往前追溯,我们发现问题比
OneDimensionBKDWriter.add() 还要早。真正的异常出现在排序/回填链路上:
PointValuesWriterMutablePointTreeReaderUtils.sort()StableMSBRadixSorter关键证据来自两个探针:
point-sort-restore-multiple-zero-ordsunwrittenSlotCount == source-write-point-doc-mismatch delta这说明在某次排序/回填过程中,有一部分槽位根本没有被写入,默认值
0 被 restore 回填到
ords[],再通过
docIDs[0] 放大成大量
docID=0,最终导致
pointCount > docCount,source segment 进入错误状态。
到这一步,排查重点已经不是“Easysearch 的 BKD merge 逻辑存在缺陷”,而是 Lucene points 排序链路的执行结果和源码语义不一致。
reorder() 自身的 coverage 异常真正把方向扭转过来的,不是又一次复现,而是一个更早的探针:
point-sort-reorder-coverage-mismatch这个探针验证的是:
StableMSBRadixSorter.reorder() 是否真的按源码应有的次数完整执行。
我们抓到的典型样本之一如下:
targetSegment=_xfield=statusk=7expectedLoopCount=9800actualIterationCount=8204firstCoverageMismatchBucket=201firstCoverageExpected=9788firstCoverageActual=8192更关键的是,同一条日志里还带出了这个信息:
skippedSourceSamples=[201:[{ord=8192,bucket=201,docID=9090,byteAtK=200}, ...]]
这条信息非常重要,因为它说明:bucket
201 理论上应该处理
9788 条,实际只处理了前
8192 条,但从
ord=8192 往后的样本,读出来仍然还是
bucket=201。这直接推翻了“后半段数据被污染后改桶”的旧解释,指向了一个更直接的结论:
reorder() 自己的 coverage 被截断了。
另一个样本中出现了同类边界:
firstCoverageExpected=31822,
firstCoverageActual=16384。
到这里,一个很不自然的特征浮现出来:
8192、
16384——这些明显的 2 的幂边界,更像是运行时或 JIT 执行异常,而不是普通业务逻辑 bug。
此时怀疑对象已经不是泛泛的“BKD 整体有问题”,而是 Lucene 中的这段热点循环:
for (int i = 0; i < HISTOGRAM_SIZE; ++i) { final int limit = endOffsets[i]; for (int h1 = fixedStartOffsets[i]; h1 < limit; h1++) { final int b = getBucket(from + h1, k); final int h2 = startOffsets[b]++;
save(from + h1, from + h2);
}
}
restore(from, to);
代码位于
org.apache.lucene.util.StableMSBRadixSorter#reorder(...)。
按源码语义,这段代码应该完整扫描每个 bucket 的范围,并最终把全部结果 restore 回去。但我们抓到的事实是:
expectedLoopCount != actualIterationCount,某些 bucket 只跑到
8192 /
16384 就停了,随后出现未写槽位,restore 把默认
0 回填,最终 source segment 进入错误状态。
如果这是 Java 源码本身的稳定逻辑 bug,它在解释执行时也应该稳定触发,而不应该强烈依赖某个 JDK/JIT 组合。后面的 JVM 对照实验基本排除了这个可能性。
这次排查中最有说服力的,不是某一条日志,而是对照实验。
环境:
Oracle GraalVM 21+35.121+35-jvmci-23.1-b15Linux aarch64 / ARM64UseJVMCICompiler = true结果:很快复现,命中了
point-sort-reorder-coverage-mismatch、
point-sort-reorder-underfilled、
point-sort-restore-multiple-zero-ords,随后 merge 报
ArrayIndexOutOfBoundsException: Index -4 out of bounds for length 8。
只改 JVM 参数,不改代码和压测口径:
-XX:-UseJVMCICompiler-Xint结果一致:都没有再出现上述探针和异常。
这三组对照的意义很直接:如果这是 Easysearch 或 Lucene 的纯 Java 逻辑 bug,解释执行也应该能稳定复现。但现实是基线组复现,关闭 JVMCI 和纯解释执行都不复现。 问题显然高度依赖 JIT 路径。
这里需要补充一条重要的边界条件。我们后来又测试了一个较新的 GraalVM 版本:
java version "21.0.9" 2025-10-21 LTS Java(TM) SE Runtime Environment Oracle GraalVM 21.0.9+7.1 (build 21.0.9+7-LTS-jvmci-23.1-b79)
在当前压测中,这个版本没有再出现 merge 错误。
因此结论必须写得更精确:已知会复现的是较早的
21+35-jvmci-23.1-b15,已知在当前测试中未复现的是较新的
21.0.9+7-LTS-jvmci-23.1-b79。更准确的工程判断不是“GraalVM 21 整体都有问题”,而是
某个特定 GraalVM 21 构建有问题,较新的构建很可能已经修复或规避了该问题。这里仍需保持严谨:只能说“在当前压测中未复现”,还不能直接说“已经被完整证明没有问题”。
除了前面详细展开的
Linux aarch64 / ARM64 主要实验环境外,有用户反馈在以下环境中也出现过同类问题:
openEuler4.19.90-2112.8.0.0131.oe1.x86_64x86_64这是用户的测试环境,不是我们能够独立完整复现并逐项展开的。但这条信息已经足够说明:当前
不能把问题简单写成“ARM 平台专属”。更准确的说法是:我们在
ARM64 上系统性复现并完成了主要对照实验,另外也有
openEuler x86_64 测试环境的同类现象反馈,因此平台边界目前还没有被完全钉死。
为了进一步排除“是不是所有 Java 21 都会这样”,我们在同一台服务器上把
/infini/easysearch/jdk 从
Oracle GraalVM 21 换成普通
Oracle HotSpot 21.0.10,恢复默认 JVM 参数,用同样的写入压测继续验证。
其中一轮的结果很有说服力:
nginx_zstd3_40mt4ZSTDthreads=16bulk_size=1000target_docs=181463624最终
after_count=181463624,
delta_written=181463624,全量文档写入完成,服务端没有出现任何 BKD merge 错误。
这条结果至少说明:同一台机器、同一套 Easysearch、同样的数据规模和写入模型,只要把 JDK 从
Oracle GraalVM 21 换成
Oracle HotSpot 21.0.10,问题就不再出现。
到这一步,工程判断已经比较清晰了:不是 Easysearch 自身逻辑导致,也不是所有 Oracle JDK 21 都会出错,更像是特定
Oracle GraalVM 21 构建相关的 JVMCI/Graal JIT 路径问题。
如果说前面的结论还能被质疑为“Easysearch 某些实现差异触发的”,那么后面的外部对照基本排除了这个方向。
我们在同一台服务器上部署了
Elasticsearch 8.19.5(Lucene 9.12.2),JDK 也切到相同的
Oracle GraalVM 21,执行同类写入压测。结果 Elasticsearch 也复现了同样的 BKD merge 崩溃。
关键异常完全一致:
java.lang.ArrayIndexOutOfBoundsException: Index -4 out of bounds for length 8
栈也一样落在
BKDReader.readNodeData、
BKDWriter$MergeReader.collectNextLeaf、
BKDWriter$MergeReader.next。
这条证据的力度很强:不是 Easysearch 独有的问题,不是当前这套 Lucene 代码路径独有的问题,
Elasticsearch 8.19.5 + Lucene 9.12.2 在同类 GraalVM 21 环境下也会出现同类异常。到这一步,再把问题归因于 Easysearch 本身的代码逻辑,已经缺乏依据了。
把整条证据链串起来,当前阶段的结论已经比较清楚。
已验证的事实:
ZSTD 或
best_compression 专属StableMSBRadixSorter.reorder() 自身的 coverage 异常UseJVMCICompiler 后问题不复现,
-Xint 下也不复现Oracle HotSpot 21.0.10 后,Easysearch 全量写入跑完未见 BKD merge 异常Elasticsearch 8.19.5 + Lucene 9.12.2 在同类 GraalVM 21 环境下也复现21.0.9+7-LTS-jvmci-23.1-b79 在当前压测中未复现openEuler x86_64 测试环境中也出现过同类错误,因此不能写成 ARM 专属工程结论:
从工程证据来看, Easysearch 本身的代码逻辑没有问题。
当前最符合事实的结论是:问题高度相关于
特定
Oracle GraalVM 21 构建,更具体地,是该构建相关的 JVMCI/Graal JIT 路径。它把 Lucene BKD 相关热点代码执行到了错误状态。已知较早构建
21+35-jvmci-23.1-b15 可复现,已知较新的
21.0.9+7-LTS-jvmci-23.1-b79 在当前测试中未复现。平台边界目前尚未完全钉死,不能再简单写成仅限
ARM64。
换句话说,这不是“Easysearch 的 BKD merge 实现有 bug”,而是 特定 JDK/JIT 运行时把本来正确的 Lucene BKD 代码执行错了。
如果你在生产或测试环境中运行 Easysearch 或 Elasticsearch,并且使用的是某些
Oracle GraalVM 21 构建,且启用了默认的 JVMCI/Graal JIT,那么在高并发写入、频繁 merge、BKD 热点路径被充分打热的场景下,需要特别警惕这类问题。
现阶段比较明确的建议是:
Oracle GraalVM 21+35.1 或
21+35-jvmci-23.1-b15Oracle GraalVM 21.0.9+7.1(即
21.0.9+7-LTS-jvmci-23.1-b79)Oracle HotSpot 21.0.10直接落到版本号上会更清晰:
21+35-jvmci-23.1-b1521.0.9+7-LTS-jvmci-23.1-b79原因很简单:前者我们已经复现了,后者在当前压测中没有复现。当然,这里的“推荐”是基于当前测试结果,不代表上游已经正式确认该问题已被修复。
这次排查最大的价值,不是“又复现了一次 BKD merge 崩溃”,而是把一个看起来像 Easysearch 代码 bug 的现象,收敛成了一个有明确边界的运行时问题。
它至少说明两件事:
这次结论之所以成立,不是因为主观判断,而是因为我们已经拿到了足够强的工程证据:同机 HotSpot 不复现,关闭 JVMCI 不复现,解释执行不复现,Elasticsearch 也复现,较新的 GraalVM 21.0.9+7.1 在当前测试中未复现,且某用户的 openEuler x86_64 测试环境也出现过同类错误。
所以,这一次,问题确实不在 Easysearch,而在特定版本的 JDK/JIT 运行时。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70029458/viewspace-3115793/ ,如需转载,请注明出处,否则将追究法律责任。