由于 Elasticsearch 索引问题导致 SonarQube Pod 启动失败

问题描述

SonarQube Pod 无法变为 Ready,并且会反复重启。常见症状包括:

  • Pod 处于 CrashLoopBackOff 状态,重启次数每隔几分钟就会增加。

  • Pod 日志在启动期间包含从 org.sonar.server.es.IndexCreator 抛出的 NullPointerException

  • Pod 日志显示启动卡在 Elasticsearch 数据恢复阶段,例如:

    Elasticsearch is launching, waiting for recovery to complete

    最终 Pod 会被 liveness 或 readiness 探针杀死。

在这些情况下,PostgreSQL 是健康的(其 Pod 处于 Running 且未重启);只有 SonarQube Pod 处于不健康状态。

根本原因

SonarQube 在 /opt/sonarqube/data/es8 下嵌入了一个 Elasticsearch 实例(该目录与嵌入式 Elasticsearch 的主版本匹配;较旧的 SonarQube 版本使用 es7)。以下两种相关情况都可能导致启动失败:

  • Elasticsearch 索引损坏。 索引文件处于不一致状态——通常是由于非正常关机、存储故障,或磁盘空间耗尽。SonarQube 无法打开索引,web 进程会中止。对于全新实例,这会表现为 IndexCreatorNullPointerException;对于已有实例,则会表现为启动循环。
  • 恢复过慢被探针终止。 Elasticsearch 能够恢复索引,但恢复时间超过了 liveness 或 readiness 探针允许的时长,因此 Pod 在恢复过程中被杀死,循环不断重复。

SonarQube 中的 Elasticsearch 数据属于派生状态。它会在下次启动时从 PostgreSQL 自动重建,因此删除索引不会丢失用户数据——只是会在首次启动时多花一些时间来重新创建索引。

故障排查

步骤 1 — 区分“恢复过慢”和“索引损坏”

检查最近的重启历史以及每次重启的原因:

kubectl -n <NAMESPACE> describe pod <RELEASE>-sonarqube-xxxxx
  • 如果你看到 Last State: Terminated,并且在探针失败后立即出现 Reason: Error(例如 Liveness probe failed),同时日志最后还在输出恢复进度,则说明 Pod 正在恢复过程中被杀死。应将其视为恢复过慢
  • 如果日志以堆栈跟踪结尾(例如来自 IndexCreatorNullPointerException,或 IndexNotFoundException),并且 Pod 重启时没有任何探针失败事件,则应将其视为索引损坏

步骤 2 — 检查失败组件对应的 Pod 日志

kubectl -n <NAMESPACE> logs <RELEASE>-sonarqube-xxxxx --previous

确认故障源自 SonarQube 进程(而不是 PostgreSQL)。相关行通常会引用 IndexCreatorElasticSearch,或 es7/es8 路径。

解决方案

选择与步骤 1 相匹配的选项。

选项 A — 给恢复更多时间(恢复过慢)

如果 Pod 在恢复过程中被杀死,可临时放宽或移除 liveness 和 readiness 探针,这样 Pod 就不会在较长的首次恢复期间被重启。编辑 SonarQube Deployment(你可能需要在 Deployment 上设置 skip-sync: "true" 注解,以防 operator 回滚你的更改):

kubectl -n <NAMESPACE> annotate deployment <RELEASE>-sonarqube skip-sync=true --overwrite
kubectl -n <NAMESPACE> edit deployment <RELEASE>-sonarqube

移除或显著增大 livenessProbereadinessProbe 中的 initialDelaySeconds / failureThreshold,然后等待 Pod 完成恢复并变为 Ready。一旦稳定,再移除 skip-sync 注解,以便在下一次 reconcile 时恢复原始探针配置。

选项 B — 重置 Elasticsearch 索引(索引损坏)

删除磁盘上的索引,使 SonarQube 在下次启动时重新构建它。具体操作取决于数据卷的供应方式:

B.1 — 从正在运行的 Pod 内部执行(PVC 或临时存储)

kubectl -n <NAMESPACE> exec -it <RELEASE>-sonarqube-xxxxx -- bash

# Inside the Pod
cd /opt/sonarqube/data
ls       # confirm the directory name (typically es8 on SonarQube 9+)
mv es8 es8.bak
exit

# Restart the Pod so SonarQube rebuilds the index
kubectl -n <NAMESPACE> delete pod <RELEASE>-sonarqube-xxxxx

如果 Pod 无法持续运行到足以执行 exec,则先将 Deployment 缩容,把 PVC 挂载到一个调试 Pod 中(kubectl debug,或者使用挂载相同 PVC 的小型 Pod 清单),在其中执行重命名操作,然后再将原始 Deployment 扩容回来。

B.2 — 使用 localpath 存储时

登录到承载 SonarQube Pod 的节点,找到节点 localpath 根目录下的 SonarQube 数据目录,并重命名或删除 es* 目录:

# On the node hosting the Pod
ls /<LOCALPATH_ROOT>/<NAMESPACE>/<PVC_NAME>/
mv /<LOCALPATH_ROOT>/<NAMESPACE>/<PVC_NAME>/es8 /<LOCALPATH_ROOT>/<NAMESPACE>/<PVC_NAME>/es8.bak

然后触发 Pod 重启:

kubectl -n <NAMESPACE> delete pod <RELEASE>-sonarqube-xxxxx

步骤 3 — 验证恢复

等待 Pod 变为 Ready,然后在 SonarQube UI 中确认项目、问题和规则能够正常渲染。重新索引后的首次扫描可能会比平时更久,因为索引正在重新填充。

如果索引已重置但 Pod 仍然无法启动,则故障并非 Elasticsearch 问题——在重试之前,请重新检查 Pod 日志以确认底层错误。

注意事项

  • 存储空间。 这两种失败模式通常都由数据卷磁盘空间耗尽引发。在判断索引损坏之前,请在 Pod 内(或节点上的对应路径)检查 df -h /opt/sonarqube/data,如果已满则增大卷大小。
  • 备份。 选项 B 中创建的 es8.bak 目录可在 SonarQube 再次稳定后删除。在此之前请保留它,以便需要回滚时使用。
  • 官方指导。 SonarQube 文档中关于强制执行 Elasticsearch 重新索引的操作步骤,是该场景的上游参考:参见 重新索引