ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SonarQube索引崩溃:Unrecoverable indexation failures排查与修复

SonarQube索引崩溃:Unrecoverable indexation failures排查与修复

项目标题是一部典型的“看一眼就知道服务端状态不对”的报错。先说结论:这个报错基本不是因为你代码写得有问题,也不是某个插件版本冲突,而是 SonarQube 服务端的内部索引状态出了问题。java.lang.IllegalStateException: Unrecoverable indexation failures: 1 errors among 1 request这句话翻译过来就是“有 1 个索引请求失败了,而且这个失败没法靠自动重试恢复”。刚看到时容易慌,但按我实际排查下来的经验,它其实是一条可以一步步定位、甚至大部分场景下几分钟就能恢复的故障。这篇文章围绕这条报错展开,适合正在维护 SonarQube 平台的开发、测试、运维同学,尤其是那些升级完版本或磁盘出过问题后,突然发现服务起不来的场景。

1. 先搞清楚这个报错在说什么

1.1 索引在 SonarQube 里扮演的角色

SonarQube 的核心数据其实存在数据库里,比如规则、问题、指标、项目元信息。但界面上的项目浏览、问题列表搜索、代码跳转、度量历史曲线,这些快动作都依赖 Elasticsearch 索引。SonarQube 内置了一个 Elasticsearch 实例,作用相当于给数据库里的数据做了一层“加速缓存”和“多维检索目录”。

你可以把数据库想成一个大仓库,Elasticsearch 更像是仓库门口的那套电子货架标签。货架标签乱了,仓库里的货还在,但你要快速找到某个东西就会很费劲,甚至显示不出来。SonarQube 的“indexation”就是指这套货架标签的建立和更新过程。启动项目、分析代码、更新规则集、切换分支,都会触发索引写入请求。正常情况下的索引请求是高频且轻量的,失败后 SonarQube 也会自动重试。可一旦报出Unrecoverable indexation failures,说明这个失败被 SonarQube 判定为“重试也没用”。

1.2 Unrecoverable indexation failures 的真实含义

这里的Unrecoverable是关键词。SonarQube 内部有一套索引任务的容错机制,单次失败通常会在后台重试若干次,重试失败也可能只影响局部的搜索功能,不至于把整个服务打挂。真正打到IllegalStateException级别的,往往是底层 Elasticsearch 状态已经无法继续接受写入,比如索引分片损坏、索引映射(mapping)异常、磁盘只读、或者数据目录里的元数据文件与当前版本不兼容。

报错里的1 errors among 1 request也是一个小细节。它说明那一刻发生的问题很具体,不是“大量请求同时失败”,而是一个请求触发了不可恢复异常。实际现场里,这个“1 request”往往就是 SonarQube 启动后尝试重建或启动索引时发出的第一个关键请求。你可以把它理解成门被卡住了,连推第一下都推不动,后面的请求自然全部排队失败。

1.3 为什么是 IllegalStateException 而不是普通异常

Java 的IllegalStateException表示“当前方法被调用的时机或对象状态不对”。比如你明明调用了启动方法,但实例已经处于不可用状态。SonarQube 用它来抛出索引问题,就是在明确告诉你:不是参数不对,不是网络超时,而是服务端内部状态机已经走进了死胡同。这也是为什么网上那些“加大超时时间”“重启一下 web 服务”的建议有时候治标不治本,因为问题根本不在网络或者超时配置,而在于 Elasticsearch 的本地存储状态已经不对了。

2. 常见的诱因和排查入口

2.1 数据目录损坏:最典型的元凶

在我处理过的同类情况里,十次里有七八次都能归到 SonarQube 数据目录异常。SonarQube 默认会把内置 Elasticsearch 的数据写在安装目录下的data/es目录里,另有data/web存储网页端缓存资源。这个data/es目录如果因为断电、强制 kill 进程、磁盘空间耗尽、容器重启不当而导致写入中断,Elasticsearch 内部的 translog 或分片数据就可能出现不一致。

判断是不是这个原因,最直接的方式不是看报错字符串,而是去翻 SonarQube 的日志目录。通常位于$SONARQUBE_HOME/logs/es.log或$SONARQUBE_HOME/logs/sonar.log。如果日志里有类似ElasticsearchStatusException、shard lock、failed to obtain node locks、corruption detected这样的内容,基本就能锁定数据目录损坏。有时候甚至连es.log都写不出来,那就要去看系统层面的磁盘状态。

提示:不要一上来就删data/es。先备份,再决定是否清理。虽然大部分场景最后都需要清理重建,但先备份能让你在清理后发现问题更大的时候有退路。

2.2 内存与资源受限导致的异常

另一个高频诱因是内存分配不合理。SonarQube 启动时会启动三个 JVM:Web 服务、Compute Engine、Elasticsearch 搜索服务。三者默认的堆内存配置分别是 1GB、1GB、512MB 左右,但在项目数量大、索引分片多的时候,这个默认值经常不够。尤其是 Elasticsearch 的 JVM 堆,如果被系统 OOM Killer 杀掉,或者因为内存设置超过物理机可用内存导致无法分配,重启后索引恢复请求一样会报IllegalStateException。

此时排查入口有两个方向:一是看系统日志中有没有Out of memory或者进程被 kill 的记录,二是直接查看$SONARQUBE_HOME/conf/sonar.properties里的内存配置项。如果sonar.search.javaOpts里的-Xmx设置得过高,比如一个小内存机器上给了 4GB,而系统可用内存根本没有这么多,ES 进程根本起不来,报错就会很类似。

2.3 版本升级带来的索引映射冲突

还有一种容易被忽略的情况:SonarQube 做过版本升级,而旧的索引映射还停留在旧版本格式。SonarQube 在升级时通常会执行迁移脚本,但有些自定义插件、或者长期未升级的旧版本,会导致 Elasticsearch 索引的 mapping 无法跟随主版本升级自动完成。

这类问题有个典型特征:报错日志里会出现字段类型冲突、unknown field、mapping parser 之类的措辞。如果你近期做过大版本升级,优先怀疑这个方向。处理方式通常也是清理索引并让系统重建,不过重建之后要考虑让所有项目重新入索引,这个时间成本会比前两种情况高一些。

2.4 常见诱因速查

诱因类型典型日志关键词判断入口
数据目录损坏corruption detected、failed to obtain node lockslogs/es.log、logs/sonar.log
内存资源不足OutOfMemoryError、Killed系统日志、sonar.properties内存配置
索引映射冲突mapping、unknown field、parse_exception升级时间点回顾、es.log
磁盘写满或只读No space left on device、read-onlydf -h、mount状态
权限问题AccessDeniedException、permission denied数据目录属主与运行用户是否一致

3. 一步步实操:从备份到重建索引

3.1 我建议的固定操作套路

处理这类问题,我习惯采用一套固定流程,能避免在慌乱中误操作。先后顺序是:确认版本、备份、停服务、查看日志、清理数据目录、调整配置、启动、验证索引重建。下面逐个说。

第一步确认当前 SonarQube 版本和内置 ES 版本,这个信息能从$SONARQUBE_HOME/lib/或者logs/sonar.log启动横幅里看到。确认版本的意义在于,后续清理数据目录后系统重建的索引结构必须与当前版本匹配。如果你本来就在做版本升级,那么尽量以目标版本为基准来确认问题。

第二步备份。这里说的备份不是备份整个安装目录,而是重点备份data目录。SonarQube 的项目数据本体在数据库里,但data目录里存着索引快照和 web 缓存。在清理前执行:

cp -r data data_backup_$(date +%Y%m%d%H%M)

备份完确认备份目录大小和源目录一致,再进行下一步。磁盘空间不够的话,至少也要把data/es单独备份出来,哪怕打包压缩也要留一份。

第三步停服务。无论你是通过 systemd、还是bin/sonar.sh stop停服务,都应该确认进程真正退出。一个简单办法是检查端口占用,SonarQube Web 默认 9000 端口,ES 内部通信端口是 9001,确保ss -lntp里这两个端口都释放了再继续。

3.2 日志定位:决定你该清理到什么程度

停服务之后不要急着删目录,先花 5 分钟看日志。这是快速判断问题范围的关键一步,很多新手在这里就翻车了。

打开logs/es.log,重点看有没有异常堆栈。如果看到的是Failed to load node lock或者corruption detected,说明问题确实出在本地索引数据上,清理data/es是合理的。如果看到的是 Elasticsearch 启动后反复出现网络连接失败、master not discovered 之类,那问题可能出在配置层面,比如节点发现配置被改过。

logs/sonar.log里如果出现了明确指向某个索引名(比如issues、components、measures)的写入失败,也可以考虑在控制台先尝试重新触发索引,而不是立刻全量清理。SonarQube 管理后台有一个“Projects”页面下的背景任务区,正常状态时这里能看到索引任务进展。但大多数情况下,走到IllegalStateException这一步时,后台任务已经处于卡死状态,所以别抱太大期望。

注意:日志里如果出现了DiskWatermark相关的词,比如flood stage disk watermark exceeded,说明 ES 因为磁盘空间过高主动把索引设置成了只读。这种场景下你直接清理数据目录是没用的,正确做法是清理磁盘空间,让磁盘使用率降到阈值以下,ES 会自动恢复写入。如果自动恢复不生效,可以执行一次:

curl -XPUT 'http://localhost:9001/_cluster/settings' -H 'Content-Type: application/json' -d '{"persistent":{"cluster.routing.allocation.disk.threshold_enabled":"false"}}'

不过这个操作只能临时解除限制,根本解法还是扩容磁盘或者清理系统日志。

3.3 核心修复动作:清理数据目录与重建索引

确认问题出在本地索引后,清理data/es是主要动作。执行:

rm -rf data/es/*

注意只清理data/es里面的内容,不要把data/web也顺手删了。data/web是 web 服务的缓存,删除后虽然也能重建,但没必要增加恢复变量。清理完以后,同样的道理,也别急着启动,先检查data/es目录属主是否还是当前运行 SonarQube 的用户。

这一步容易踩坑:如果你是用 root 停的服务,但 SonarQube 本身是以普通用户身份运行的,那清理后重建的目录属主可能变成 root,导致启动时权限不足。保险起见,清理后把目录属主重新指定一遍:

chown -R sonarqube:sonarqube data/es

如果你用的是 Docker 部署,原理相同,但要确认data目录挂载卷的权限和容器内运行用户的 UID 一致。

清理完成后,重启服务。首次启动会比平时慢,因为 SonarQube 需要创建全新的索引。日志中会出现大量 “indexing...” 相关记录,这说明系统正在重建索引。这一阶段建议等待而不是反复点击界面,否则会给服务端带来额外压力。等 Web 界面出现且能正常登录后,去菜单栏的“Projects”页面,看看各个项目是否显示“Indexing”状态或者在后台任务中出现索引任务记录。必要时可以触发一次全量重新分析。

3.4 如果清理后还是不行

清理data/es是最常规的恢复手段,但偶尔会遇到清理之后重启依然报同类错误的情况。这时候你得换个思路:大概率问题不在 ES 数据本身,而在于底层环境。

优先检查磁盘空间。df -h看挂载 SonarQube 数据目录的分区,如果使用率超过 85%,ES 默认的磁盘水位设置就会拒绝写入。此时先清理空间,再重启。还有个相关参数是sonar.search.javaOpts中的-Xms和-Xmx,如果堆设置过大、物理机本身没有那么多内存,ES 进程连启动都做不到,表现也会是索引失败。可以临时调低堆内存,先确认能否排除这个因素:

sonar.search.javaOpts=-Xms512m -Xmx512m

改完配置重启,注意观察es.log里 JVM 启动时间和堆内存初始化日志。

4. 索引修复后的关键验证与日常防御

4.1 修复之后至少要做四件事

很多团队处理完报错、界面能打开就以为结束了,结果第二天又发现项目历史度量图变成空的。原因很简单:索引重建只保证“数据结构正确”,不保证“历史数据已经全部重新入索引”。SonarQube 的索引数据一部分来自数据库表的扫描重建,另一部分则跟随项目分析任务实时增量写入。老的索引被清理后,历史 issue、metric 这些数据需要扫描数据库重新填充,这个过程未必会立刻启动。

所以修复后建议按顺序做四件事。第一,去后台任务页面确认没有任何失败的索引任务。第二,随机挑两个项目,打开 issue 页面搜索几个历史关键字,确认搜索能出结果。第三,重新触发一遍项目分析,确保 CI 或扫描任务能正常写入新索引。第四,如果项目数量大,观察 1-2 小时,确认sonar.log里没有持续的异常堆栈。

4.2 日常防御:让这种报错尽量少出现

结合几次实战经验,有几件事属于“早知道就不会踩坑”的级别。首先是备份,很多团队对 SonarQube 的备份理解停留在数据库 dump 上,但data目录同样需要定期备份。我建议至少把data/es纳入到备份脚本中,虽然它很大,但比起出问题时手动重建全量索引的时间成本,多占一点存储是划算的。

其次是升级策略。不要在生产环境直接跨大版本升级,尽量先在测试环境同步数据跑一遍升级流程。SonarQube 的索引格式和插件兼容性在不同大版本之间变化明显,预演一遍能提前发现 mapping 冲突、插件禁用等问题。有个很实用的经验:升级之前,先把升级说明文档中关于索引迁移的章节读一遍,确认当前版本是否清理了某些索引结构。如果升级后立刻报索引相关错误,优先检查是否缺少了某个必要的迁移步骤。

然后是健康巡检。SonarQube 自带内置 ES,但它不会像独立的 Elasticsearch 集群那样提供充足的可观测性指标。你可以简单写一个脚本,定期检查磁盘使用率、检查logs/es.log中是否有ERROR,检查 Web 服务端口是否健康。以下是一个最简化的巡检片段:

#!/bin/bash if df -h | grep -q '/opt/sonarqube/data' && [ $(df --output=pcent /opt/sonarqube/data | tail -1 | tr -d '%') -gt 85 ]; then echo "[$(date)] data disk usage high" >> /var/log/sonar_check.log fi if grep -q "ERROR" /opt/sonarqube/logs/es.log 2>/dev/null; then echo "[$(date)] es log error found" >> /var/log/sonar_check.log fi

这个脚本只是辅助,但它能让你在问题爆发前多一个预警信号。真正的值守思路是:SonarQube 的索引异常不会突然发生,之前一定会有小征兆,比如搜索变慢、后台任务堆积、日志里间歇性报 ES 超时。抓住这些信号尽早处理,通常不用走到Unrecoverable indexation failures这一步。

5. 常见问题速查:直接照着做

场景典型现象处理动作
磁盘使用率过高ES 日志出现 disk watermark清理磁盘,释放空间后重启,必要时临时禁用软阈值
强制断电/杀进程后启动失败,报 corruption 或 lock 错误备份,清理data/es,重启等待重建
大版本升级后mapping 或 unknown field 报错确认迁移步骤,清理索引后全量重扫
内存配置过高进程启动失败,或直接被 OOM Kill调低sonar.search.javaOpts堆内存,确认物理内存余量
数据目录权限错乱AccessDenied 或 unable to write重置data/es属主或容器卷权限

还有两个容易被忽略但实际影响很大的小细节。第一个是防火墙或 Docker 网络配置,如果你用的是容器部署,ES 节点之间的通信如果被网络策略拦住,日志中看起来是“索引失败”,实际上跟数据无关。此时应看 ES 层的master not discovered yet之类的报错。第二个是data/web目录偶尔也会出问题,表现为界面静态资源加载异常,但它不会影响后端索引请求。不要把data/web和data/es混在一起清理,否则只会延长恢复时间。

6. 最后分享一个我认为最省事的经验

排查这类报错时,我自己的原则是“先隔离后清理”。很多时候团队同事会直接把 SonarQube 数据目录整个删掉,然后从数据库恢复,这个操作不是不行,但成本很高,而且容易丢失一些没有同步到数据库的中间状态。折中一点的做法是:先备份data/es,然后只删data/es里的索引文件,把data/web和配置文件原样保留。我实际遇到过的几回,这样处理后基本都能在几分钟内恢复正常服务,后续只需要等待后台重建索引即可。

另外,恢复期间最容易忽略的操作是“多等一等”。SonarQube 启动后索引重建不是瞬时完成的,控制台显示的绿色运行状态并不代表索引已经全部重建好。我遇到过几次同事以为修复成功,结果第二天才发现 issue 历史数据缺失,其实只是当晚重建还没完成而已。所以修复完当天离开前,记得再确认一遍后台任务页面的状态,看到持续的索引任务在跑,才算真正收尾。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进