ADMIN RECOVER/CLEANUP INDEX修复后一段时间索引问题又出现

【TiDB 使用环境】生产环境
【TiDB 版本】5.2.4
【部署方式】机器部署
【操作系统/CPU 架构/芯片详情】Ubuntu
【遇到的问题:问题现象及影响】
上上周机房4个tikv挂载点的几块固态硬盘异常,将该4个tikv下线,正常tikv执行unsafe-recover remove-fail-stores -s,对于region3peer全丢失的region,将该类region重建为空region,集群正常启动。

后续部份表有索引与数据不一致的问题出现,对该部份表执行了
ADMIN RECOVER INDEX及ADMIN CLEANUP INDEX索引修复语句,SQL正常返回修复情况,但是过了一段时间又出现索引不一致的问题,为什么执行了ADMIN RECOVER INDEX及ADMIN CLEANUP INDEX语句后续还会出现索引异常问题。

ADMIN RECOVER INDEX 只补充缺失索引,ADMIN CLEANUP INDEX 只清理多余索引。两者都不校验索引值的数据正确性,也不解决产生不一致的根因(DDL 竞争、GC 误删、分区表缺失校验、SkipWriteUntouchedIndices
优化等)。只要根因还在,异常就会反复出现。

– 1. 打开 mutation checker 以根因定位
SET GLOBAL tidb_enable_mutation_checker = ON;

– 2. 查看最近一段时间是否有 DDL 操作
ADMIN SHOW DDL JOBS;
– 关注 ADD INDEX / DROP INDEX 相关的 Job

– 3. 查看 GC Worker 日志(确认是否有误删范围)
– 在 TiDB 日志中搜索 “unsafe destroy range”

– 4. 检查分区表
SHOW CREATE TABLE <表名>;
– 如果是分区表,需要特别关注

异常复现的 7 种根因

  1. DDL 在线建索引期间的并发窗口(最常见)

在 ADD INDEX 的 WriteReorganization 阶段:

SnapshotVer 时刻的快照 → Backfill 扫描全表建索引

并发 DML 在 StateWriteOnly 下双写数据和索引

pkg/ddl/index.go:2585 的注释明确承认:

// For the normal pessimistic transaction, it’s ok.
// But if async commit is used, it may lead to inconsistent data and index.

异步提交事务中,如果 TiKV 的锁被 GC 提前清理了,并发写入的索引条目可能丢失。这种情况下:

  • 行数据写入了
  • 索引条目丢失了
  • ADMIN RECOVER INDEX 补上了
  • 但同样的场景还会再次发生,只要有并发 DDL + 异步提交
  1. SkipWriteUntouchedIndices 优化导致的不一致留存

pkg/executor/write.go:299-308:

if sessVars.InTxn() || … {
opts = append(opts, …)
} else {
opts = append(opts, table.SkipWriteUntouchedIndices) // 自动提交时启用
}

这个优化的逻辑是:UPDATE 时,如果索引列的值没有变化(“untouched”),就跳过写索引条目,因为它应该已经存在。

问题链条:

  1. 某条数据因为某些原因丢失了索引条目(比如 DDL backfill 遗漏)

  2. ADMIN RECOVER INDEX 补上了

  3. 后续有一条 UPDATE,改了其他列,索引列没变

  4. SkipWriteUntouchedIndices 生效 → 不再写索引(认为它已存在)

  5. 但此时如果旧索引条目因为某些原因又丢了(见第 3、4 点)

  6. 这条数据的索引再次缺失

  7. GC UnsafeDestroyRange — 物理删除索引 KV

pkg/store/gcworker/gc_worker.go 中,GC Worker 在执行 doUnsafeDestroyRange 时,直接从 TiKV 物理层面删除 KV 范围,不经过 MVCC。

哪些 DDL 会注册删除范围:

┌────────────────┬────────────────────────────┐
│ DDL 操作 │ 注册的删除范围 │
├────────────────┼────────────────────────────┤
│ ADD INDEX 回滚 │ 索引和临时索引的 key range │
├────────────────┼────────────────────────────┤
│ DROP INDEX │ 索引 key range │
├────────────────┼────────────────────────────┤
│ DROP TABLE │ 整个表(含数据和索引) │
├────────────────┼────────────────────────────┤
│ TRUNCATE TABLE │ 旧表数据范围 │
├────────────────┼────────────────────────────┤
│ DROP PARTITION │ 分区 + 全局索引范围 │
└────────────────┴────────────────────────────┘

如果 GC UnsafeDestroyRange 的 key range 计算有偏差(比如 redoDeleteRanges 重试逻辑),或者删除了仍被引用的范围,索引数据会物理消失。ADMIN RECOVER INDEX 补回来后,下一次 GC 如果再次误删,又会出现不一致。

  1. 分区表 — Mutation Checker 完全关闭

pkg/table/tables/mutation_checker.go:90-93:

if t.Meta().GetPartitionInfo() != nil {
// TODO: Support check for partitions as well
return nil // 完全跳过检查
}

分区表在写入时的数据一致性检查完全被跳过。而且 tidb_enable_mutation_checker 默认就是 OFF(pkg/sessionctx/variable/tidb_vars.go:1465):

DefTiDBEnableMutationChecker = false

即使 ADMIN RECOVER/CLEANUP 修复了分区表的不一致,后续的任何 DML 写入都不会校验索引和数据是否一致,异常会反复累积。

  1. Temp Index Merge 3 阶段建索引的竞争

pkg/ddl/index_merge_tmp.go 的三阶段合并:

  • Running: Backfill 写临时索引,并发 DML 双写真实索引+临时索引
  • ReadyToMerge: 等待 all TiDB 节点感知 schema 版本
  • Merging: 将临时索引重放到真实索引

合并过程的竞争:在 mergeIndexWorker.BackfillData 中,从临时索引读取条目后,需要检查对应数据行是否还存在。但此时并发 DML 可能同时修改同一行,导致:

  • 临时索引的条目被重放到真实索引
  • 但对应数据行已经被并发事务删除了
  • 产生了多余的索引条目

ADMIN CLEANUP INDEX 可以删掉,但只要建索引操作(ADD INDEX)重跑或新节点加入触发 rebuild,同样的竞争还会发生。

  1. 索引值正确但 Handle 指向错误行(最隐蔽)

ADMIN RECOVER INDEX 的 batchMarkDup(pkg/executor/admin.go:489):

// recover index: the constraint of unique index is broken,
// handle in index is not equal to handle in table
// 只打 WARNING 日志,不修复!

如果索引键存在但 handle 指向了另一行,代码只是记录一条 warning 日志,不做任何修复。这意味着:

  • 唯一索引的约束实际上被破坏了
  • 查询通过该索引可能返回错误行
  • 后续使用该索引的 UPDATE/DELETE 会操作错数据行
  • 错误的数据修改会进一步扩散不一致
  1. 不完整事务 — 数据写入成功但索引写入失败

TiDB 内部写入顺序(pkg/table/tables/tables.go):

INSERT: 写行数据 → 写索引 → 提交
UPDATE: 删旧索引 → 写新行数据 → 写新索引 → 提交
DELETE: 删行数据 → 删索引 → 提交

如果在 “写索引” 这一步遇到临时错误(region split、leader 切换等),但之前的数据写入已经进了 staging buffer:

  • 乐观事务 → 提交时整体失败,回滚
  • 悲观事务 → 写入行数据不会被回滚,但索引没写进去

这个错误可能间歇性复现(比如集群正在做 region 调度时),所以 ADMIN RECOVER INDEX 修复后,同样的错误还会发生。

重建索引如何

这个最近没有DDL操作、也不是分区表,就是tikv磁盘坏了,执行了unsafe-recover remove-fail-stores以及 recreate-region ,recreate-region就是丢失数据和索引的原因;

执行ADMIN RECOVER INDEX及ADMIN CLEANUP INDEX的时候,有数据在进该表,会不会是这个原因影响索引新增和删除修复不彻底。

新建一张表,把数据搬过去,拉倒了

涉及的表多 占用空间大 可用的空间少 最差就是删索引重建了

1 个赞

unsafe-recover 重建空 Region 后,ADMIN RECOVER INDEX 无法彻底修复底层数据与索引的一致性 ,导致问题反复出现

1 个赞

看情况是又遇到bug了

1 个赞

如果不是新版的,索引重建,回填数据需要很久的

1 个赞

是的 计划先升级高版本 完了Fast DDL快。

1 个赞

不是普通索引损坏吧。感觉是TiKV 磁盘坏盘 + 手动 unsafe-recover + 空 Region 重建后出现的元数据 / 数据索引持久化不一致 + 副本不一致问题。

1 个赞

修复的是 “当前数据”,但集群里还存在 “坏副本 / 旧副本 / 缺失副本”,调度一发生、副本一同步,坏数据又把索引覆盖坏了 → 索引反复异常。

1 个赞

实在没办法就搞一个表和索引重建,找一个业务量少的时间

是不是遇到bug了?

此话题已在最后回复的 7 天后被自动关闭。不再允许新回复。