📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入
Easysearch BKD Merge 异常排查实录:最终定位到旧版 GraalVM JIT 运行时

最近一次高并发写入压测中,我们遇到了一个非常诡异的 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 的热点代码执行错了。


为什么这个问题难查 #

它有几个特别迷惑人的特征:

  • 只在高并发写入压测下触发
  • 服务重启后的前几轮最容易复现
  • 同一进程里,删了索引重新压,后面复现率反而下降
  • 不是固定字段,多个数字类型字段都中过招
  • ZSTDbest_compression 两种 codec 下都能复现

实际命中过的字段包括 @timestampsizestatus_seq_no。所以这不是某个字段、某种 codec 或某个 mapping 的偶发问题。


第一层排除:merge reader 不是第一现场 #

一开始我们确实怀疑 merge reader,毕竟异常直接出现在 merge 路径上。但日志顺序很快给出了相反的证据。在 merge 真正崩溃之前,source segment 已经先出现了这些异常信号:

  1. point-sort-restore-multiple-zero-ords
  2. source-write-point-doc-mismatch
  3. pointCount > docCount
  4. pack-index-negative-code
  5. reader-invalid-start-pos
  6. 最后才是 ArrayIndexOutOfBoundsException

这意味着两件事:merge reader 不是第一现场,source segment 在写出阶段就已经坏了。merge reader 只是读到了已经损坏的 BKD index,并在那个阶段暴露了异常。


第二层排除:Easysearch 自己的 BKD 写入逻辑也没有先出错 #

继续往前追溯,我们发现问题比 OneDimensionBKDWriter.add() 还要早。真正的异常出现在排序/回填链路上:

  • PointValuesWriter
  • MutablePointTreeReaderUtils.sort()
  • StableMSBRadixSorter

关键证据来自两个探针:

  • point-sort-restore-multiple-zero-ords
  • unwrittenSlotCount == 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=_x
  • field=status
  • k=7
  • expectedLoopCount=9800
  • actualIterationCount=8204
  • firstCoverageMismatchBucket=201
  • firstCoverageExpected=9788
  • firstCoverageActual=8192

更关键的是,同一条日志里还带出了这个信息:

skippedSourceSamples=[201:[{ord=8192,bucket=201,docID=9090,byteAtK=200}, ...]]

这条信息非常重要,因为它说明:bucket 201 理论上应该处理 9788 条,实际只处理了前 8192 条,但从 ord=8192 往后的样本,读出来仍然还是 bucket=201。这直接推翻了“后半段数据被污染后改桶”的旧解释,指向了一个更直接的结论:reorder() 自己的 coverage 被截断了

另一个样本中出现了同类边界:firstCoverageExpected=31822firstCoverageActual=16384

到这里,一个很不自然的特征浮现出来:819216384——这些明显的 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 对照实验基本排除了这个可能性。


最强证据:只换 JDK / JIT 路径,结果就变了 #

这次排查中最有说服力的,不是某一条日志,而是对照实验。

基线组:旧版 Oracle GraalVM 21,默认 JVMCI/Graal JIT #

环境:

  • Oracle GraalVM 21+35.1
  • 21+35-jvmci-23.1-b15
  • Linux aarch64 / ARM64
  • UseJVMCICompiler = true

结果:很快复现,命中了 point-sort-reorder-coverage-mismatchpoint-sort-reorder-underfilledpoint-sort-restore-multiple-zero-ords,随后 merge 报 ArrayIndexOutOfBoundsException: Index -4 out of bounds for length 8

对照组:关闭 JVMCI/Graal JIT 或纯解释执行 #

只改 JVM 参数,不改代码和压测口径:

  • -XX:-UseJVMCICompiler
  • -Xint

结果一致:都没有再出现上述探针和异常。

这三组对照的意义很直接:如果这是 Easysearch 或 Lucene 的纯 Java 逻辑 bug,解释执行也应该能稳定复现。但现实是基线组复现,关闭 JVMCI 和纯解释执行都不复现。问题显然高度依赖 JIT 路径。

版本对照:较新的 GraalVM 21 构建在当前测试中未复现 #

这里需要补充一条重要的边界条件。我们后来又测试了一个较新的 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 构建有问题,较新的构建很可能已经修复或规避了该问题。这里仍需保持严谨:只能说“在当前压测中未复现”,还不能直接说“已经被完整证明没有问题”。

平台边界:不能写成 ARM 专属 #

除了前面详细展开的 Linux aarch64 / ARM64 主要实验环境外,有用户反馈在以下环境中也出现过同类问题:

  • 操作系统:openEuler
  • 内核:4.19.90-2112.8.0.0131.oe1.x86_64
  • 架构:x86_64

这是用户的测试环境,不是我们能够独立完整复现并逐项展开的。但这条信息已经足够说明:当前不能把问题简单写成“ARM 平台专属”。更准确的说法是:我们在 ARM64 上系统性复现并完成了主要对照实验,另外也有 openEuler x86_64 测试环境的同类现象反馈,因此平台边界目前还没有被完全钉死。


更强的同机对照:换成 Oracle HotSpot 21.0.10 后,全量写入跑完也没有问题 #

为了进一步排除“是不是所有 Java 21 都会这样”,我们在同一台服务机上把 /infini/easysearch/jdkOracle GraalVM 21 换成普通 Oracle HotSpot 21.0.10,恢复默认 JVM 参数,用同样的写入压测继续验证。

其中一轮的结果很有说服力:

  • 索引:nginx_zstd3_40mt4
  • codec:ZSTD
  • threads=16
  • bulk_size=1000
  • target_docs=181463624

最终 after_count=181463624delta_written=181463624,全量文档写入完成,服务端没有出现任何 BKD merge 错误。

这条结果至少说明:同一台机器、同一套 Easysearch、同样的数据规模和写入模型,只要把 JDK 从 Oracle GraalVM 21 换成 Oracle HotSpot 21.0.10,问题就不再出现。

到这一步,工程判断已经比较清晰了:不是 Easysearch 自身逻辑导致,也不是所有 Oracle JDK 21 都会出错,更像是特定 Oracle GraalVM 21 构建相关的 JVMCI/Graal JIT 路径问题。


最关键的外部对照:Elasticsearch 8.19.5 也复现了 #

如果说前面的结论还能被质疑为“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.readNodeDataBKDWriter$MergeReader.collectNextLeafBKDWriter$MergeReader.next

这条证据的力度很强:不是 Easysearch 独有的问题,不是当前这套 Lucene 代码路径独有的问题,Elasticsearch 8.19.5 + Lucene 9.12.2 在同类 GraalVM 21 环境下也会出现同类异常。到这一步,再把问题归因于 Easysearch 本身的代码逻辑,已经缺乏依据了。


这次排查最终说明了什么 #

把整条证据链串起来,当前阶段的结论已经比较清楚。

已验证的事实:

  • 问题不是 merge reader 先制造坏数据,source segment 在更早阶段就已经进入错误状态
  • 不是单字段问题,也不是 ZSTDbest_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 热点路径被充分打热的场景下,需要特别警惕这类问题。

现阶段比较明确的建议是:

  1. 避免继续使用已经验证可复现的旧版构建Oracle GraalVM 21+35.121+35-jvmci-23.1-b15
  2. 优先升级到当前测试中未复现的版本Oracle GraalVM 21.0.9+7.1(即 21.0.9+7-LTS-jvmci-23.1-b79
  3. 如果短期内不方便升级 GraalVM,直接切换到普通 Oracle HotSpot 21.0.10

直接落到版本号上会更清晰:

  • 已确认应避开:21+35-jvmci-23.1-b15
  • 当前更推荐:21.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 运行时。

标签
Easysearch x
Elasticsearch x
Lucene x
GraalVM x
JDK x
产品更新 x
performance x
2026 x
开源 x
赞助 x
开源生态 x
社区 x
Coco AI x
二等奖 x
兴智杯 x
人工智能 x
赛事 x
低空经济 x
商业化 x
数据分析 x
金猿奖 x
国产化 x
搜索引擎 x
技术卓越奖 x
创新产品奖 x
IT168 x
APM x
Skywalking x
Easy-Es x
Coco x
AI x
GitLab x
代码审核 x
石油石化 x
Gitee x
投票 x
Meilisearch x
Rust x
轻量级 x
搜索百科 x
Docker x
Docker Compose x
Easyserach x
Console x
DevOps x
国产替代 x
backup x
snapshot x
CCR x
Gateway x
esdump x
source_reuse x
ignore_above x
OpenSearch x
AWS x
Solr x
Easyearch x
发明专利 x
数据分区 x
国际专利 x
一等奖 x
人工智能应用创新大赛 x
bulk x
embedding x
OpenAI x
IK x
TDBC x
2025 x
信通院 x
可信数据库大会 x
搜索型数据库 x
中国数据库产业图谱 x
上海开源创新菁英荟 x
开源创新新星企业 x
Workshop x
AI 搜索 x
智能助手 x
Automation x
Logstash x
MongoDB x
开源中国 x
直播 x
merge x
Elasticsearch 9 x
GitCode x
AI搜索 x
Cloud x
rollup x
Kubernetes x
Operator x
Arm64 x
Snapshot x
S3 x
Grafana x
Opensearch x
Nginx x
直播活动 x
搜索客社区 x
Meetup x
ES x
企业搜索 x
DeepSeek x
RAG x
certificate x
windows x
Rollup x
TopN x
Filebeat x
Ubuntu x
请求限速 x
INFINI Console x
指标 x
Kibana x
多集群 x
client x
Spring Boot x
ECE x
ES Bulk x
vector database x
Postgres x
可搜索快照 x
SDK x
官网 x
Web 开发 x
Next.js x
React x
Three.js x
Metrics x
Helm x
filter x
querycache x
practice x
Agent x
localStorage x
响应式 x
时间组件 x
时区组件 x
极限科技 x
三周年 x
周年庆 x
国家高新技术企业 x
校园招聘 x
湖北工业大学 x
Tauri x
Web 开发人员 x
桌面应用开发 x
桌面端 x
Electron x
Pizza x
认证培训 x
报名 x
Scrapy x
爬虫 x
Rust开发者大会 x
docsearch x
文档搜索 x
Easyseach x
有奖征文 x
黑神话悟空 x
EKS x
征文系列 x
跨集群搜索 x
科技中小企业 x
白皮书 x
Python SDK x
数据库产业图谱 x
超大规模 x
分布式集群 x
写入限流 x
2024可信数据库发展大会 x
创新型中小企业 x
搜索数据库 x
正排索引 x
免费许可证 x
K8S x
DTC2024 x
实时搜索 x
ES国产化 x
Redis x
OOM x
测试 x
内存 x
趋势 x
AI绘画 x
Stable Diffusion x
Diffusion x
Model x
GAN x
语义搜索 x
知识图 x
向量数据库 x
中国信通院 x
星河(Galaxy) x
标杆案例 x
鲲鹏 x
鲲鹏技术认证 x
客户端 x
日志平台 x
LDAP x
Loadgen x
中国一汽 x
国内数据库 x
墨天轮 x
监控系统 x
集成测试 x
ZSTD x
Helm Charts x
国产适配 x
兆芯 x
Linux x
LoongArch x
信创适配 x
二维拆分算法 x
中国移动云 x
Vault x
加密 x
安全工具 x
kNN x
向量检索 x
图片搜索 x
Alerting x
SQL x
搜索 x
Embedding x
可信数据库 x
统信 x
海光 x
龙芯 x
restore x
Arm x
大数据企业证书 x
移动云大会 x
信通院产品评测 x
国内首家 x
数据可视化 x
北京软协 x
第十届理事会会员单位 x
Apache Arrow x
宣传片 x
大会分享 x
多集群管理 x
无缝数据迁移 x
Loadrun x
INFINI Gateway x
log4j x