TiDB DDL Reorg / Backfill 全面解析从 Add Index、Modify Column 到分布式回填与断点续跑【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidbTiDB 中大量 DDL如加索引、改列类型、部分分区操作并不是“只改元数据”就能完成的它们需要扫描存量数据、写入新的数据结构这一阶段在源码与状态机中被称为reorganizationreorg / 回填 backfill。本文以 docs/agents/ddl/03-reorg-backfill.md 为核心骨架结合pkg/ddl下 worker 调度、状态流转、分布式回填与 ingest 加速路径的源码实现系统讲清 reorg 的触发条件、运行框架、断点续跑机制与实战开发注意事项帮助你具备直接阅读和修改 TiDB DDL 回填代码的能力。哪些 DDL 属于 “reorg”reorg 的本质是DDL 进入reorg状态后后台 worker 在 DML 仍持续写入受一定限制的前提下扫描存量数据并回填新的数据结构。典型的 reorg 场景包括Add index / Add primary key为存量行逐条构建索引项是最常见的 index backfillModify column 需要数据重写列类型变更导致行格式或编码变化时需重写整表数据部分分区操作涉及数据搬移或变换的分区 DDL如reorganize partition也是 reorg 重负载操作。在 pkg/ddl/backfill_metrics_test.go 中可以看到这些动作的归类佐证——测试用例通过ActionReorganizePartition、ActionAddIndex等model.Job.Type来构造reorgInfo并断言各类操作的统计行为说明同一套 reorg 框架被多个动作类型共用。对上述场景的深入拆解TiDB Agent 文档体系中另有专门文档Add index 深度解析Modify column 深度解析Partition DDLreorg-heavy 操作schema statereorg 阶段的可见性契约这类作业通常会进入 schema statereorg此时遵循两条契约schema 变更处于“部分可见/兼容”状态——元数据版本对外发布的形态保证新旧事务都能正确执行后台 worker 执行数据回填——回填期间 DML 可以继续运行例如写新行时同步维护新索引但会附加限制如对正在回填的区间进行写冲突检测、必要时加锁等以确保最终一致性。状态边界上的 schema 版本同步是否正确是 reorg 正确性的关键验收点后文“实战开发注意事项”会再次强调。reorg 在 DDL 框架中的运行位置理解 reorg 首先要把它的“宿主环境”定位清楚。整体脉络是调度循环scheduler→ worker 步骤推进transitOneJobStep→ 作业执行runOneJobStep→ reorg 专用子流程runReorgJob / reorg worker。高层入口与工作池划分作业步骤推进pkg/ddl/job_worker.go 中的runOneJobStep与transitOneJobStep是 DDL 作业每一步实际执行与持久化的驱动器reorg 上下文与工具pkg/ddl/reorg.go、pkg/ddl/reorg_util.go 承载 reorg 期间需要的执行上下文reorgCtx、reorgInfo与辅助逻辑如从会话变量初始化ReorgMeta、按表 ID 估算表大小等工作池划分pkg/ddl/job_scheduler.go 会创建独立的 reorg worker 池reorgWorkerPool并把 reorg 型作业调度到该池执行。jobType枚举pkg/ddl/job_scheduler.go把 DDL 作业明确分成jobTypeGeneralgeneral与jobTypeReorgreorg两类reorg worker 池对应的 worker factory 是addIdxWorker可见“加索引”这类回填任务在框架层面就被当作独立的 worker 工种对待。reorg worker 池的规模计算job_scheduler.go中工作池大小的计算公式值得注意// reorg worker count at least 1 at most 10. reorgCnt : min(max(runtime.GOMAXPROCS(0)/4, 1), reorgWorkerCnt) s.reorgWorkerPool newDDLWorkerPool( pools.NewResourcePool(workerFactory(addIdxWorker), reorgCnt, reorgCnt, 0), jobTypeReorg, ) s.generalDDLWorkerPool newDDLWorkerPool( pools.NewResourcePool(workerFactory(generalWorker), generalWorkerCnt, generalWorkerCnt, 0), jobTypeGeneral, )reorg 并发度默认取GOMAXPROCS/4下限 1、上限 10上限常量reorgWorkerCnt 10定义于 pkg/ddl/ddl.gogeneral元数据类作业池的容量上限同样是 10generalWorkerCnt 10在 NextGen 内核模式下kerneltype.IsNextGen()还会额外创建 20 个backgroundWorker的池用于后台作业。这个数量级说明reorg 回填虽然吃 CPU/IO但 TiDB 刻意将单实例并发数控制在小规模配合分布式回填在更多 TiDB 节点上横向扩展而不是靠单机高并发。单步执行与错误重试语义transitOneJobSteppkg/ddl/job_worker.go是 DDL 每步执行的“心脏”它做的是开启事务 → 读取作业当前字节并做乐观并发校验GetJobBytesByIDWithSe防止与 cancel/pause/owner 变更并发修改冲突→ 调用runOneJobStep→ 依据错误类型决定回滚、提交与重试。几个关键语义若作业已done/rollbackDone/cancelled直接走handleJobDone若执行出错且作业既不在回滚中、也未回滚完成则w.sess.Reset()丢弃本次 KV 改动并允许重试可重试错误会 sleep 一段GetWaitTimeWhenErrorOccurred()再重试避免像死锁一样立即打转提交前通过checkBeforeCommit校验“当前实例仍是 DDL owner”非 owner 时禁止提交这是 owner 切换后安全退让的关键屏障。这解释了 reorg 为什么必须“可重试”——作业每步都可能在任意节点、任意时刻因错误或 owner 迁移而中断随后从持久化的进度继续。可恢复性resumabilityreorg 的灵魂约束文档强调 reorg 框架最重要的设计需求是可恢复性resumability具体表现为两条进度持久化reorg 必须把进度行数 row count、已扫描 range、checkpoint写入作业记录model.Job而不是只存在内存里断点续跑重试或 owner 切换后新 owner 的 worker 从已持久化的进度继续而不是从头扫描。源码中的进度持久化证据pkg/ddl/reorg.go 的runReorgJob展示了这一机制的实现骨架rc : w.getReorgCtx(job.ID) if rc nil { if job.IsCancelling() { return dbterror.ErrCancelledDDLJob } beOwnerTS : w.ddlCtx.reorgCtx.getOwnerTS() rc w.newReorgCtx(reorgInfo.Job.ID, reorgInfo.Job.GetRowCount()) w.wg.Run(func() { err : reorgFn() rc.doneCh - reorgFnResult{ownerTS: beOwnerTS, err: err} }) }回填工作以owner TSowner 租约时间戳打标签。当回填结果返回时若发现结果携带的ownerTS与当前reorgCtx.getOwnerTS()不一致说明期间发生过 owner 切换会直接丢弃本次结果并返回genReorgTimeoutErr()触发重试防止旧 owner 的 worker 把过期结果写进作业记录作业启动时用job.GetRowCount()初始化reorgCtx即从作业记录里已持久化的行数继续计数回填完成后把rc.getRowCount()、rc.getSnapshotVer()写回job.SetRowCount(...)、job.SnapshotVer随后由外层事务持久化。此外runReorgJob开头对job.ReorgMeta nil做了防御性兜底老测试或老版本作业不带 ReorgMeta 时补一份默认元数据见 pkg/ddl/reorg.go这也印证了“进度与元数据必须跨作业版本兼容”的要求——回填相关元数据DDLReorgMeta本身携带Version字段用于版本演进。DML 与回填的并发交互reorg 期间 DML 并未停摆写入路径会把新写入的行同步维护到正在创建的结构上而回填 worker 在扫描某段 range 时会与 DML 进行冲突检测/加锁。这也是为什么文档强调“回填期间 DML 继续执行with restrictions”。对应实现可进一步阅读 pkg/ddl/backfilling.gobackfillCtx的构造、表达式上下文、警告处理器等与 pkg/ddl/backfilling_txn_executor.go 中事务型回填执行器的细节。分布式回填dist task对于大表回填TiDB 支持利用dist-task 框架把回填工作分发给集群内多个执行节点从而突破单机 worker 并发度上限。注册与装配dist-task 的注册发生在 pkg/ddl/ddl.go 的NewDDL中taskexecutor.RegisterTaskType(proto.Backfill, func(ctx context.Context, task *proto.Task, param taskexecutor.Param) taskexecutor.TaskExecutor { return newBackfillDistExecutor(ctx, task, param, d) }, )也就是说proto.Backfill任务类型被注册为newBackfillDistExecutor的执行器调度侧则由pkg/ddl/backfilling_dist_scheduler.go负责把回填区间切分成子任务分发给各节点对应测试见 pkg/ddl/backfilling_dist_scheduler_test.go。设计定位同一 DDL 生命周期的“扩展”分布式回填并非独立的新 DDL 流程而是同一 DDL job 生命周期的扩展。阅读代码时应把握两点DDL job 仍然驱动 schema state 并持久化进度状态机、可见性切换、进度写入依旧走前文所述的单 owner 路径回填“苦力活”被委托给分布式执行器通过 dist-task 在多个节点并行执行proto.Backfill把数据扫描/写入压力摊开。两份权威设计文档值得配套阅读2022-09-19-distributed-ddl-reorg.md分布式 DDL reorg 设计2023-04-11-dist-task.mddist-task 框架设计分布式回填涉及的调度器/执行器文件在pkg/ddl下以backfilling_dist_*与backfilling_*命名聚集如 backfilling_dist_scheduler.go、backfilling_dist_executor.go、backfilling_clean_s3.go 等可作为深入某个子系统的起点。Ingest / 加速路径Lightning 后端对部分负载TiDB 还可以用ingest / Lightning-based 流水线加速回填——不再逐行走 KV 事务写入而是像导入一样批量构建 SST 再 ingest 进 TiKV。这类路径对执行环境更加敏感改动时需要格外小心磁盘空间与临时目录生命周期ingest 中间产物会落盘含本地及可能的 S3 临时目录磁盘打满会直接导致回填失败需检查清理逻辑与失败恢复checkpointing 与 resumeingest 模式同样要遵守“进度持久化 断点续跑”的铁律批次/文件级别的 checkpoint 语义与逐行模式不同ingest 与 non-ingest 模式的切换框架依据配置与集群能力是否支持 ingest、TiKV 版本能力等在两种模式间决策改代码时要保证切换条件与回退路径完备。代码集中在 pkg/ddl/ingest20 个 Go 文件设计依据见 2022-06-07-adding-index-acceleration.md。相关指标的观测如回填表维度的 metrics可参考 pkg/ddl/backfill_metrics.go 与 pkg/ddl/backfill_metrics_test.go。修改 reorg / backfill 代码时的验收清单文档给出的实战开发指引可落成一份可执行的自检清单。当你改动 reorg/backfill 代码时务必逐条验证进度确实被持久化且跨作业版本/重启兼容。重点检查job.SetRowCount/job.SnapshotVer的写入路径、ReorgMeta的版本字段以及回填中途 owner 切换后新 worker 是否从持久化进度续跑。工作可安全重试要么写入本身幂等要么有可靠的去重手段。注意transitOneJobStep中“出错即Reset丢弃本次 KV 改动”的语义——重试不是简单地再跑一遍而是从上一持久化点重新执行。取消/暂停/恢复语义被尊重不要无视 job 状态迁移。例如runReorgJob中job.IsCancelling()且无reorgCtx时直接返回ErrCancelledDDLJobrc.doneCh收到ErrCancelledDDLJob时要移除 reorgCtx 并终止。owner 切换的 TS 校验ownerTS ! curTS即报超时错误也属于这一类“别把过期结果写进去”的保护。状态边界的 schema 版本同步仍正确reorg 完成/取消后必须让集群所有节点同步到新 schema 版本否则会出现部分节点仍按旧元数据读写、部分节点已按新元数据访问的结构不一致问题。transitOneJobStep返回的schemaVer非零时需要等待其他节点追上就是这一同步机制的体现。改动较大或引入新模式时补充设计文档优先在 docs/design 下新增简短设计文档并把它链入 docs/agents/ddl/README.md方便后续维护者建立完整认知地图。阅读路径速查把上述锚点汇总为一张“按需深入”的速查表均为当前仓库路径关注点建议阅读路径reorg 触发场景与作业分类docs/agents/ddl/06-add-index.md、docs/agents/ddl/07-modify-column.md、docs/agents/ddl/08-partition-ddl.md作业步骤推进状态机驱动器pkg/ddl/job_worker.gorunOneJobStep/transitOneJobStepreorg 上下文与进度持久化pkg/ddl/reorg.go、pkg/ddl/reorg_util.goreorg worker 池划分与容量pkg/ddl/job_scheduler.go、pkg/ddl/ddl.goreorgWorkerCnt 10分布式回填dist taskpkg/ddl/ddl.go、pkg/ddl/backfilling_dist_scheduler.go、pkg/ddl/backfilling_dist_executor.go分布式/加速设计文档2022-09-19-distributed-ddl-reorg.md、2023-04-11-dist-task.md、2022-06-07-adding-index-acceleration.mdingest 加速路径pkg/ddl/ingest回填指标pkg/ddl/backfill_metrics.go、pkg/ddl/backfill_metrics_test.go【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考