1
2
2
1
博客/.../

实战复盘:一次 40 分钟的 add index 卡 0 行救援 —— 从 GitHub Issue #65948 看 TiDB disttask 孤儿任务的隐蔽危害

 像风一样的男子  发表于  2026-08-24

适用版本:TiDB 7.5.x(含 7.5.4 / 7.5.7) 现象:CREATE INDEX 永远卡在 write reorganization,ROW_COUNT 一直为 0 根因:DXF (disttask 框架) 残留的孤儿任务 + 框架内存态不刷新系统表 UPDATE 修复耗时:40 分钟(其中 15 分钟在等 cancel 走流程、10 分钟在等 restart)


一、问题背景:这不是孤例

业务侧在生产 TiDB 7.5.7 集群上发起一个 CREATE INDEX

CREATE INDEX idx_iid_d ON soa_vehicle.t_bike_info (iot_id, del_flag);

按照经验,这种操作应该走 ingest 模式,3 分钟内完成。然而:

  • 第 1 分钟:tidb.log 出现 [reorg.go:282] run reorg job wait timeout,ROW_COUNT 一直停在 0
  • 第 5 分钟:业务开始问"为什么索引没建好"
  • 第 10 分钟:用 ADMIN SHOW DDL JOBS 看,job 还在 running,ROW_COUNT 仍然是 0
  • 第 15 分钟ADMIN CANCEL DDL JOBS 28057 发出,但状态卡在 cancelling

更关键的是,tidb.log 每一秒都在刷同样两段 ERROR:

[ERROR] [scheduler.go:500] [onError] [task-id=90005] [error="index info not found: 6"]
[ERROR] [scheduler.go:500] [onError] [task-id=180011] [error="[schema:1146]Table '(Schema ID 449).(Table ID 2796)' doesn't exist"]

这两个 task-id、Schema ID 449、Table 2796、index 6、table 6505 —— 全部都指向 GitHub Issue #65948 里报告的同一组错误。

也就是说:我们遇到的不是孤立 bug,是 issue 已经报告但社区还没出修复的 disttask 框架问题。


二、完整现象:四件套同时出现

为了能精准定位,我们先把整个故障的特征完整列出来,方便其他同学对号入座:

症状 具体表现 错误码/位置
1. add index 不推进 ROW_COUNT=0SCHEMA_STATE=write reorganization,每 5s 一次 reorg job wait timeout tidb.log [reorg.go:282]
2. cancel 不收敛 STATE=cancelling 持续 10 分钟以上不进入 cancelled ADMIN SHOW DDL JOBS
3. 框架刷屏 日志里每秒 1 行 task-id 90005 / task-id 180011 的 ERROR tidb.log [scheduler.go:500]
4. 全局 ON 状态 SHOW VARIABLES LIKE 'tidb_enable_dist_task' 应该是 ON(这次因为历史原因暂时是 OFF) 系统变量

任何第 1 + 第 3 同时出现的场景,90% 是这个 bug。下面分析根因。


三、根因剖析:disttask 的两个机制叠加

TiDB 7.1+ 起,加索引走 "ingest 模式"/* ingest */),底层使用 DXF(Distributed eXecution Framework,disttask)做并行回填。这是一个独立于 DDL worker 的调度框架,靠系统表 mysql.tidb_global_task 协调。

DXF 框架本身有两个不互通的"逻辑坑":

坑 1:系统表 UPDATE 后,framework 内存态不主动 reload

DXF 的 scheduler 在内存里维护着一份"任务分配表",记录 task_id → dispatcher_id 的映射。这份缓存只在 framework 启动时mysql.tidb_global_task 读一次。

意思是:

UPDATE mysql.tidb_global_task SET state='canceled' WHERE id=90005;

这一行 SQL 在系统表里改成功,但 framework 内部那个 scheduler 仍然认为这个 task 处于 running/pausing 状态,会一直尝试调度它(直到下一个 fork 出来的 process 重启)。

坑 2:task 长期处于非终态,调度器不主动超时退出

参考 TiDB 源码 pkg/disttask/framework/scheduler/manager.go:onPausingTasks,scheduler 的"心跳"会反复轮询所有非终态 task,期望它们被 driver 推进到终态。但如果 driver 进程(dispatcher)已经宕机,框架不会主动回收 task,会一直留在 pausing 状态占用调度槽位。

我们现场查 1290001 这个 task:

SELECT id, task_key, state, dispatcher_id,
       TIMESTAMPDIFF(SECOND, start_time, NOW()) AS run_sec
FROM mysql.tidb_global_task WHERE id=1290001;

-- 结果: dispatcher_id=Null, state=pausing, run_sec=3016495 (~35天)

35 天没收敛,这就是 framework 一直在等一个永远不会上线的 dispatcher。

卡死的完整链路

历史某次 add index 被异常中断
    ↓
mysql.tidb_global_task 残留非终态条目(state=reverting/pausing)
    ↓
framework scheduler 反复尝试调度这些僵尸任务
    ↓
每秒一次 ERROR 刷屏("index info not found"、"Table doesn't exist")
    ↓
scheduler 调度槽位被这些任务占满
    ↓
新发起的 CREATE INDEX 任务进不到调度池
    ↓
所有 ingest 模式 add index 卡 row_count=0

核心矛盾:系统表改了 state=terminated,framework 内存里还以为它是 running —— 必须重启 tidb-server 才能彻底刷新。


四、实战排查:与 AI 协同的 40 分钟

这部分是我在这次救援里印象最深的一段。处理这次故障时,我把整个过程交给了一个 AI 助手(不是搜索引擎或文档)协作,40 分钟内完成了从现象到根因到修复的全链路。下面还原关键对话节点:

节点 1:用户给出原始症状

用户:我的 add index 卡主了,没有进度,tidb.log 在刷 180011 / 90005 的 ERROR。

AI 的第一反应不是去搜文档,而是要求看现场证据

  1. 完整的 tidb.log ERROR 段
  2. ADMIN SHOW DDL JOBS 的输出
  3. SHOW VARIABLES LIKE 'tidb_enable_dist_task'

这是排查分布式数据库故障的基本功:没有现场证据不猜根因

节点 2:AI 锁定第一个关键变量

AI 通过日志里的 "ingest mode"=true 立刻关联到 DXF 框架,并追问了一个关键点:

"你之前有没有动过 tidb_enable_dist_task?"

这就是盲区。因为之前清理过孤儿任务,我们临时设了 tidb_enable_dist_task=OFF,清完后忘了恢复。这导致 ingest 模式直接失能,新加索引的 subtask 永远进不了调度队列。

教训:全局开关绝不能长时间为 OFFtidb_enable_dist_task=OFF 等于把所有 ingest 任务直接送进死队列。

节点 3:AI 引导探查系统表

恢复 ON 之后问题没解决(cancelling 卡 13 分钟)。AI 引导查更深:

SELECT id, task_key, state, dispatcher_id, start_time,
       TIMESTAMPDIFF(SECOND, start_time, NOW()) AS run_sec
FROM mysql.tidb_global_task
WHERE state IN ('pending','running','pausing','reverting');

结果发现了第二组孤儿任务1290001 / 1290002,state=pausingdispatcher_id=Null,start_time 距今 35 天。

这就是 issue #65948 报告的同类 bug 的变种:除了 reverting 刷屏型,还有 pausing 静默占位型。两者根因相同但症状不同。

节点 4:AI 建议终极大招

UPDATE 系统表没生效(framework 内存态不刷新),AI 没有再绕弯,直接给出最强手段:

tiup cluster restart <cluster-name> -R tidb

只重启 tidb-server 角色,不重启 tikv / pd / tiflash

  • 业务连接闪断 1-2 秒,框架内存态全清
  • 28057 强制 rollback done
  • 1290001/1290002 的 pausing 状态被丢弃
  • scheduler pool 重建,干净状态

重启后 90 秒内,业务的所有 add index 自动恢复正常。下面的截图是我们最终恢复后跑的几个索引:

JOB_ID DB TABLE ROW_COUNT 用时 状态
28059 soa_vehicle t_bike_info 27,301,410 3 分钟 2 秒 ✅ synced
28060 soa_ota t_upgrade_strat 862,868 10 秒 ✅ synced
28061 soa_ota t_upgrade_push 12,515,082 56 秒 ✅ synced

五、临时解决方案:按症状对号入座

把这次救援固化成可复用的 SOP,按"严重程度 / 紧急程度"分三档:

场景 A:刚刚发现卡 0 行(最佳介入时机)

症状:add index 1-3 分钟,ROW_COUNT 一直为 0,tidb.log 开始刷 ERROR。

立即动作

-- 1. 确认全局开关
SHOW VARIABLES LIKE 'tidb_enable_dist_task';
-- 如果是 OFF:
SET GLOBAL tidb_enable_dist_task = ON;
-- 重新发起(cancel 当前 job 后再 CREATE)
ADMIN CANCEL DDL JOBS <job_id>;
CREATE INDEX ... ;

场景 B:cancel 卡在 cancelling 不收敛(中期)

症状:已经 ADMIN CANCEL DDL JOBS,但 60 秒以上仍 STATE=cancelling

立即动作

-- 1. 看 system table 是否有 dispatcher_id=Null 的 pausing 任务
SELECT id, task_key, state, dispatcher_id,
       TIMESTAMPDIFF(SECOND, start_time, NOW()) AS run_sec
FROM mysql.tidb_global_task
WHERE dispatcher_id IS NULL
  AND state IN ('pausing','reverting');
-- 2. UPDATE 状态到终态(让 framework 知道这事结束了)
UPDATE mysql.tidb_global_task
SET state='canceled', state_update_time=NOW()
WHERE id IN (上面查到的 id);

注意:UPDATE 后框架内存态可能不会自动 reload,需要 1-2 分钟观察。如果 90 秒后 cancel 还没走通,进场景 C。

场景 C:UPDATE 后仍卡住(终极大招)

症状:以上两步都做了,cancel 还是 cancelling > 5 分钟。

立即动作

# 滚动重启 tidb-server 角色(-R tidb 是关键,不重启 tikv)
tiup cluster restart <cluster-name> -R tidb
  • 业务连接闪断 1-2 秒,可接受
  • framework 内存全清,等于一次"调度器冷启动"
  • 所有卡住的 DDL 强制 rollback done

场景 D:保留消毒(事后清理)

-- 把残留的孤儿任务彻底归档
UPDATE mysql.tidb_global_task
SET state='canceled', state_update_time=NOW()
WHERE state IN ('reverting','pausing')
  AND TIMESTAMPDIFF(MINUTE, start_time, NOW()) > 30;

六、长期预防:把"治标"变成"治本"

修复是临时的,机制要跟上。这一节列出在生产环境应该固化的巡检 + 告警 + 规范

6.1 巡检脚本(每天跑一次)

-- 任何 dispatcher_id IS NULL 的非终态任务都应当告警
SELECT id, task_key, state, dispatcher_id,
       TIMESTAMPDIFF(MINUTE, start_time, NOW()) AS age_min
FROM mysql.tidb_global_task
WHERE state IN ('pending','running','pausing','reverting')
  AND (dispatcher_id IS NULL
       OR TIMESTAMPDIFF(MINUTE, start_time, NOW()) > 30);

把这一段包到 crontab 或 Ansible playbook 里,命中即发飞书/Slack 告警。

6.2 业务规范

生产 TiDB 集群禁止手动 SET GLOBAL tidb_enable_dist_task=OFF

清理孤儿任务只能 ONLINE 改系统表状态,禁止全局 OFF。下游巡检加一条 SHOW VARIABLES LIKE 'tidb_enable_dist_task' 必须是 ON 的告警。

6.3 架构改进(建议 TiDB 团队)

回到根因 —— 这个 bug 的本质是 DXF 框架在 dispatcher 进程意外退出后,没有自动重选 / 超时回收。具体建议:

  1. scheduler 启动时把 pausing 超过 N 分钟的 task 自动标为 failed
  2. dispatcher 选举失败超过 3 次后,把 task 标 canceled 而不是无限期 pausing
  3. framework 内存态在每次 heartbeat 时都从 mysql.tidb_global_task 重新读关键字段(state / dispatcher_id),不要相信缓存

如果社区里遇到这个问题的同学足够多,可以提一个 PR 来修。具体受影响版本:TiDB 7.1.x ~ 7.5.7 全部。


七、AI 协同 Debug 的三点反思

这次实战里,我最深的感受不是技术,而是 AI 工具如何改变了故障排查的工作流。有三点值得分享:

7.1 AI 擅长:跨表结构推理 + 源码级关联

传统排查里,要从 [reorg.go:282] 这个日志点反推到 framework 的调度逻辑,再到 mysql.tidb_global_task 系统表结构,最后到具体的 UPDATE 语法 —— 这条链路对一个 DBA 至少要 2-3 小时。

AI 助手能在 1 分钟内串完这条链路,因为它把 TiDB 源码、文档、社区 case(asktug topic 1051392 / 1051393 / pingkai 1049735)一起索引了。

7.2 AI 短板:没有现场执行权

这次 AI 给出了 7-8 步精确指令,但每一步都需要用户当"remote hands"去实际执行 SQL、看截图、回报结果。故障排查仍然是人机协作,AI 不是银弹。

这也意味着:用 AI 协助排查时,把现场证据(截图、日志段、SQL 输出)完整喂给它,比给它一个"我的数据库坏了"有效 10 倍。

7.3 新的工作模式

用户 = 侦察兵(提供现场证据 + 执行命令) AI = 军师(解读证据 + 给出诊断 + 评估风险)

这次整个 40 分钟的救援,本质上是这个模式跑了 7-8 轮:

  • 用户:给截图("卡主了")
  • AI:解读 + 给出下一步("看 tidb_enable_dist_task 状态")
  • 用户:跑 SQL、回报结果
  • AI:升级诊断("看到 35 天 pausing 任务了")
  • ……

这种模式让 1 个普通 DBA 能在 1 小时内处理 7.5.x 之前需要 3-5 个有经验的 TiDB 工程师协作才能搞定的 bug。这可能是 AI 工具对运维领域最大的贡献 —— 不是替代人,而是把专家经验压缩成可复用的对话流


八、写在最后

回到 issue #65948 本身。这个 bug 已经存在至少半年,没有官方修复,社区没有完全一致的解决方案文档。我希望这篇文章能:

  1. 帮助遇到同样问题的同学快速定位(现象四件套 + 排查 SOP)
  2. 降低 TiDB 7.5.x 升级风险(升级前确认集群没有积压的 disttask 孤儿任务)
  3. 推动社区关注这个 bug(如果有同学愿意提 PR 修 framework 的 scheduler 逻辑,欢迎附上这个 issue)

如果你在生产环境也踩到同样的坑,欢迎在评论区贴出你的 tidb.log ERROR 段 + mysql.tidb_global_task 查询结果,我们可以一起分析。

作者:fengshuhui 创作时间:2026-08-24 适用版本:TiDB 7.5.x 参考资料:Issue #65948、asktug topic 1051392、pingkai 1049735

1
2
2
1

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论