尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TiDB 分布式 DDL Reorg 设计解析:让建索引回填突破单机资源上限

TiDB 分布式 DDL Reorg 设计解析:让建索引回填突破单机资源上限 TiDB 分布式 DDL Reorg 设计解析让建索引回填突破单机资源上限【免费下载链接】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本文对应的设计文档位于 docs/design/2022-09-19-distributed-ddl-reorg.md。文章以其为骨架结合当前仓库中已落地的实现pkg/ddl/backfilling_dist_scheduler.go、pkg/ddl/backfilling_dist_executor.go 等进行纵深展开。本文系统讲解 TiDB 如何将 DDL 中开销最大、耗时最长的 reorg数据回填阶段从仅 DDL Owner 单机执行演进为集群内所有 TiDB 节点按资源情况协同抢占子任务的分布式方案读者将理解 reorg 的角色分工reorg worker 与 backfill worker、子任务subtask的切分与元数据表设计、抢占/租约/断点续传/故障切换等核心机制并结合源码掌握该设计在仓库中的落地形态与关键开关。1. 动机reorg 阶段为什么需要分布式在 TiDB 的 DDL 体系中一个 DDL Job 可以划分为两大类普通 Jobgeneral job主要涉及各 schema state 的变更推进、等待所有 TiDB 完成 schema 版本同步等Reorg Job回填类 Job如ADD INDEX、MODIFY COLUMN、ADD COLUMN回填默认值等需要真正扫描已有数据并按目标 schema 逐行重建。设计文档明确指出无论把 DDL 性能优化理解为优化全部 DDL Job 的耗时含各 schema state 变更、所有 TiDB schema state 更新成功的等待时间还是优化 reorg 阶段耗时当前真正耗时、真正消耗资源CPU、磁盘 IO、与 TiKV 之间的网络的阶段都是 reorg 阶段。在设计提出之前即单机 reorg 时代TiDB 虽然已经允许 DDL Owner 在本地并行处理多个 DDL Job也能在一个 Job 内用多个 backfill worker 并发回填但存在两个结构性痛点单机资源天花板无论并行度多高算力都被限制在 DDL Owner 这一台 TiDB 上无法利用集群中其他 TiDB 节点的空闲资源资源竞争Owner 节点上的回填会与业务写入影响 TPS 的日常事务争夺 CPU 与 IO 资源导致 DDL 执行既快不了、又吵到邻居。因此该设计的核心命题是在不牺牲稳定性的前提下让 reorg 阶段的子任务可以被所有 TiDB 依据自身资源使用情况认领claim执行——仍保留DDL Owner 管理 Job 生命周期的主逻辑但把最重的搬数据动作分布到全集群。2. 现状回顾Owner 单机 reorg 是怎么跑的为便于对比设计文档以ADD INDEX为例梳理了改造前 DDL Owner 在 reorg 阶段需要执行的简单步骤按 region 将整张表的数据范围[startHandle : endHandle]切分每个 backfill worker 扫描对应 range 内的数据做合法性检查后写入索引全部 backfill worker 完成第 2 步后检查是否还有待处理数据还有则回到第 2 步继续回填没有则结束整个 reorg 阶段并更新相关 meta 信息。图 1当前实现中 add index 在 DDL Owner 上的回填流程从当前仓库源码看这一Owner 独占约束依然清晰可见pkg/ddl/reorg.go#L626-L646 中的isReorgRunnable方法在判断回填是否可运行时对非分布式场景会要求本节点必须是 DDL Owner否则返回ErrNotOwner// If isDistReorg is true, we neednt check if it is owner. if isDistReorg { return nil } if !dc.isOwner() { // If its not the owner, we will try later, so here just returns an error. logutil.DDLLogger().Info(DDL is not the DDL owner, zap.String(ID, dc.uuid)) return errors.Trace(dbterror.ErrNotOwner) }这段代码恰好是两种时代的分水岭isDistReorg true时回填不再要求我是 Owner为分布式执行解除了枷锁。3. 总体设计角色解耦与四步主流程3.1 Prepare两个角色彻底解耦分布式设计的首要前提是把职责拆成两个互不相关的角色Reorg Worker运行在 DDL Owner 节点上负责管理 Job——推进 schema state、切分 Job、检查完成情况、处理取消/失败Backfill Worker运行在任意 TiDB 节点上负责干活——从集群中认领并执行子任务subtask即 Job 在 reorg 阶段被切分出的 DDL 小任务。backfill worker 通过构建自己的 worker pool 来并发处理子任务。3.2 Process总体流程设计文档给出的完整流程如下切分DDL Owner 拿到 reorg Job 后由 reorg worker 推进其各 state 变更直至 reorg 阶段随后按数据 key 把 Job 切分为多个 subtask并把相关信息写入元数据表。巡检reorg worker 周期性定时器检查所有 subtask 是否处理完成与原有逻辑类似的巡检方式并顺带做取消等其他管理动作。认领与执行所有 TiDB 上的 backfill worker无论该 TiDB 是否为 DDL Owner都会去认领 subtask 来处理从各自的 backfill worker pool 中取出相应数量的 worker并行处理子任务与原有逻辑类似后续可再优化每个 backfill worker串行地领取子任务、逐个执行直到全部处理完成后退出。收尾第 2 步检查确认所有 subtask 均已处理后更新相关 meta 信息并进入下一阶段若存在失败 subtask则取消其余 subtask最终将 DDL Job 转入回滚流程。图 2分布式 reorg 的整体流程图对比图 1 与图 2 可以看出分布式改造的本质是把扫描-回填的循环从 Owner 进程内搬到了一张集群共享的任务表 所有节点的 worker 池上Owner 只保留切分与巡检职责。4. 元信息设计如何描述一个可被多节点认领的子任务分布式 reorg 依赖元数据把任务的进展从 Owner 的内存中搬进集群共享的存储。设计文档提出了三块核心元数据。4.1 DDLReorgMeta 增加分布式标志位在mysql.tidb_ddl_job表的DDLReorgMeta结构中新增字段用于记录该 reorg Job 是否走分布式type DDLReorgMeta struct { ... // Some of the original fields IsDistReorg bool // Determine whether do dist-reorg }这一字段在当前仓库中已经落地且被显著扩充。查看 pkg/meta/model/reorg.go#L83-L110DDLReorgMeta实际包含type DDLReorgMeta struct { SQLMode mysql.SQLMode json:sql_mode Warnings map[errors.ErrorID]*terror.Error json:warnings WarningsCount map[errors.ErrorID]int64 json:warnings_count Location *TimeZoneLocation json:location ReorgTp ReorgType json:reorg_tp IsFastReorg bool json:is_fast_reorg IsDistReorg bool json:is_dist_reorg UseCloudStorage bool json:use_cloud_storage ResourceGroupName string json:resource_group_name Version int64 json:version TargetScope string json:target_scope MaxNodeCount int json:max_node_count // ... 还包含 AnalyzeState、Stage、UseNewCollate 等 Concurrency atomic.Int64 json:concurrency BatchSize atomic.Int64 json:batch_size MaxWriteSpeed atomic.Int64 json:max_write_speed }其中Concurrency、BatchSize用于控制回填并发度与批大小可用admin alter ddl jobs动态调整见字段注释MaxNodeCount用于约束参与分布式任务的节点数TargetScope用于限定任务可调度的节点范围。字段上方的ReorgMetaVersion0/CurrentReorgMetaVersion常量见 pkg/meta/model/reorg.go#L175-L181则负责 reorg meta 的版本兼容。4.2 新增集群共享子任务表设计文档分析了一个关键点如果把全部 subtask 信息追加进TiDB_ddl_reorg.reorg字段会引入锁竞争问题。因此改用独立的系统表承载子任务并给出了初版表结构草案含id、namespace、key编码ele_key, ele_id, ddl_job_id, sub_id、ddl_physical_tid、type、exec_id、exec_expired、state、checkpoint、start_time、state_update_time、meta等列其中exec_id记录当前正在执行该子任务的执行者exec_expired记录租约过期时间TSOcheckpoint记录处理进度断点。从当前仓库看该表已实际落地并演进完整建表语句位于 pkg/meta/metadef/system_tables_def.go#L939-L962表mysql.tidb_background_subtask实际包含更丰富的列step、ordinal、concurrency、end_time、error、summary等并建立了idx_task_key、idx_exec_id索引与uk_task_key_step_ordinal唯一键。对比可见设计稿与实现存在自然演进由于该表同时服务于更通用的后台子任务/分布式任务框架能力因此比设计初稿多出了step、ordinal、summary等字段。系统元数据定义中也能看到对应的保留表 IDTiDBBackgroundSubtaskTableID、TiDBBackgroundSubtaskHistoryTableID见 pkg/meta/metadef/system.go。4.3 BackfillMeta描述一次回填的范围设计文档还提出在BackfillMeta中补充回填任务自身的元信息type BackfillMeta struct { CurrKey kv.Key StartKey kv.Key EndKey kv.Key EndInclude bool ReorgTp ReorgType ... *JobMeta // parent job meta }其中StartKey/EndKey/EndInclude定义本次回填的数据范围CurrKey用于断点续传时定位从哪一行接着干*JobMeta指向父 Job 的元信息以便回溯上下文。4.4 历史表与定期清理由于子任务按表/region 粒度切分量级可能很大设计文档提出新增mysql.tidb_background_subtask_history表记录已结束含失败状态的子任务结构同tidb_background_subtask并定期清理历史表记录以防无限膨胀。当前仓库中该表已按此思路实现建表语句见 pkg/meta/metadef/system_tables_def.go#L964-L985并额外建有idx_state_update_time索引以支撑按更新时间清理。5. 核心运行原则谁负责管谁负责干设计文档把 reorg 的整体逻辑归纳为两个部分管理 reorg JobDDL Owner 节点上的 reorg worker 完成按需切分 reorg Job 并写入 subtask检查 reorg Job 是否处理完成含失败等状态。处理 subtask 并更新相关元数据可由所有角色完成由 backfill worker 执行执行完成后把 subtask 移入 history 表。对于第 1 部分的完成检查step 1.b设计文档当时的计划是让 reorg worker 通过定时器周期巡检同时考虑通过 PD 同步子任务的完成状态来做主动检查。这对应了后文Communication Mode中与 PD watch 结合的构想。6. 子任务认领与通知机制6.1 认领规则backfill worker 认领claim子任务遵循三条规则空闲定时唤醒TiDB-server 上空闲的 backfill worker 由定时器唤醒尝试抢占剩余子任务租约机制Lease正在处理子任务的 TiDB backfill worker 需持续更新exec_expired字段keep-alive若长时间未更新导致租约过期其他 TiDB 的 backfill worker 可以抢占该子任务Owner 指定执行模式reorg worker 先把 reorg Job 切分为子任务再根据子任务总量决定仅本地原生执行还是全节点分布式执行切分出的子任务总数小于minDistTaskCnt时全部标记为 native让 Owner 所在节点优先执行避免小任务也引入分布式调度开销否则所有节点按前两种方式抢占任务。设计文档同时指出后续可以支持更灵活的任务切分与认领策略如按资源均衡。6.2 通知方式子任务就绪后如何通知各节点的 backfill worker设计文档给出了两种模式主动方式ActiveOwner 节点通过 chan 通知本机backfill workerOwner 节点通过修改注册在 PD 中的信息通知其他节点的 backfill worker被动方式Passive所有节点周期性自行检查是否存在待处理任务。6.3 通信模式设计文档提出backfill worker 获取子任务、reorg worker 检查子任务完成本质上都是定期检查 处理可考虑结合PD watch进行通信使各节点能及时感知任务表变化而非仅依赖固定周期的轮询。7. 接口设计让 backfiller 与 backfillWorker 更通用分布式化要求把取任务/跑任务/报进度抽象成显式的接口。设计文档对两个核心对象提出了接口调整方案。7.1 backfiller 接口保留原有接口BackfillDataInTxn、AddMetricInfo并新增批量取任务、更新任务等能力// backfiller existing interfaces: func BackfillDataInTxn(handleRange reorgBackfillTask) (taskCtx backfillTaskContext, errInTxn error) func AddMetricInfo(float64) // backfiller new interfaces: // get batch tasks func GetTasks() ([]*BackfillJob, error){} // update task func UpdateTask(bfJob *BackfillJob) error{} func FinishTask(bfJob *BackfillJob) error{} // get the backfill context func GetCtx() *backfillCtx{} func String() string{}7.2 backfillWorker 接口原实现中 reorg worker 与 backfill worker 通过 chan 传结果并调用run执行任务。新框架需同时适配两种场景与原逻辑一致同一 TiDB-server 内通过 chan 与 reorg worker 通信新增场景跨 TiDB-server 之间通过系统表传递子任务。为兼顾早期兼容设计文档建议两种适配分开实现场景 1 继续用原run函数场景 2 使用新增的runTaskfunc (w *backfillWorker) runTask(task *reorgBackfillTask) (result *backfillResult) {} // updatet reorg substask exec_id and exec_lease func (w *backfillWorker) updateLease(bfJob *BackfillJob) error{} func (w *backfillWorker) releaseLease() {} // return backfiller related info func (w *backfillWorker) String() string {}此外还需新增类似WorkerPool的 backfill worker 池后期考虑与既有 WorkerPool 统一且上述接口会在项目第二阶段进一步泛化。8. 可靠性设计断点、故障、失败与取消分布式把 reorg 从单机一个进程内的循环变成多节点共享一张表上的协同因此可靠性机制是设计重点。8.1 断点续传Breakpoints Resume当某个 backfill worker 所在 TiDB 发生网络分区或异常退出时对应子任务可能无人继续。设计草案采用租约标记执行者是否仍然有效的方案backfill worker 处理子任务时在tidb_background_subtask表中记录当前DDL_ID可能需要追加worker_type_worker_id后缀作为exec_id并周期性刷新exec_expired与curr_key即回填到哪一行若非 DDL Owner的 TiDB 遇到该问题当其他 TiDB 认领到该子任务并发现其exec_expired已过期例如exec_expired lease早于当前时间就更新该子任务的exec_id与exec_expired并从curr_key处继续回填若问题出在DDL Owner所在 TiDB则走下方Changing Owner流程。8.2 Owner 切换Changing OwnerDDL Owner 所在 TiDB 可能异常导致需要切换 DDL Owner。新 Owner 上的 reorg worker 会检查 reorg 信息以确认该 reorg Job 的子任务完成情况若未完成进入reorg Job 切分阶段然后进入检查 reorg Job 完成情况流程后续步骤不重复若已完成直接进入检查 reorg Job 完成情况流程后续步骤不重复。设计文档在此标注了一个待解决问题新框架下没有 Owner 也可以继续执行 backfill 阶段的子任务——因为子任务的执行权已经分布到了所有节点的 backfill worker 上Owner 切换不应中断已在其他节点上运行的回填。8.3 失败处理Failedreorg 阶段回填出错时的处理流程reorg worker此处文档原文用 reorg worker 描述处理者处理某子任务报错时将该子任务在tidb_background_subtask表中的状态改为 failed并退出对该子任务的处理DDL Owner 巡检时除了检查是否全部完成还会检查是否存在执行失败的子任务将未处理的子任务移入tidb_background_subtask_history表当没有可处理子任务时把错误传递给 Job 生成逻辑按原有逻辑把该 DDL Job 转为 rollback Job各节点的 backfill worker 在领取子任务时若发现某任务已不存在说明该 reorg 任务已失败、Owner 已清理其子任务则正常退出后续操作走既有回滚流程。8.4 取消Cancel用户执行admin cancel ddl job后Job 按原逻辑被标记为 canceling。DDL Owner 上的 reorg worker 巡检时发现该标记其后续处理流程与上文失败处理的第 36 步类似清理未处理子任务、迁移到 history 表并收尾。8.5 历史表清理Clean up由于子任务按各表 region 切分mysql.tidb_background_subtask_history表可能变得非常大因此需要增加定期清理函数。这也与上文第 4.4 节提到的idx_state_update_time索引相呼应。8.6 进度展示与监控Display进度显示第一阶段可通过各子任务内部 row count 汇总出整个 DDL Job 的进度展示逻辑与原先一致后续可提供百分比等更直观的展示帮助用户更好地理解 reorg 阶段处理情况。监控更新并新增部分日志与指标metrics以观测分布式回填的健康状况。9. 当前仓库中的落地形态值得说明的是该设计文档是 2022 年提出的方案关联 tracking issue 为 pingcap/tidb #41208从当前仓库源码结构看分布式 reorg 已经按照这一思路完成落地并进一步与 TiDB 通用的**分布式执行框架dxf**融合子任务切分/调度pkg/ddl/backfilling_dist_scheduler.go 中的LitBackfillScheduler实现了 dxf 的scheduler.Scheduler接口通过OnNextSubtasksBatch生成下一批处理计划、按表生成 read index plan 并对 range 进行分批子任务执行pkg/ddl/backfilling_dist_executor.go 中的backfillDistExecutor实现 dxf 的 executor 接口负责在具体节点上执行回填步骤newBackfillStepExecutor、GetStepExecutor、IsIdempotent、IsRetryableError等开关注入pkg/ddl/reorg_util.go#L134-L163 中初始化 reorg meta 时m.IsDistReorg vardef.EnableDistTask.Load()即由全局开关tidb_enable_dist_task决定是否启用分布式任务执行同时还会计算并发度与MaxNodeCounttidb_max_dist_task_nodes为 -1 时按集群 store 数自动推算并限定系统库mysql 等上的 DDL 不使用分布式执行Owner 校验放宽如第 2 节所示pkg/ddl/reorg.go#L626-L646 的isReorgRunnable对isDistReorg场景跳过 owner 校验元数据表mysql.tidb_background_subtask与mysql.tidb_background_subtask_history已在 pkg/meta/metadef/system_tables_def.go 中正式定义。相关的系统变量在 pkg/sessionctx/variable/sysvar.go 中注册tidb_enable_dist_task全局布尔开关与tidb_max_dist_task_nodes全局/会话级取值范围 -1128。其底层还依赖tidb_enable_fast_reorgEnableFastReorg等快速回填能力的配合——在 pkg/ddl/reorg_util.go 中可以看到约束若IsDistReorg为真而IsFastReorg为假会直接返回ErrUnsupportedDistTask即分布式 reorg 当前建立在加索引加速ingest/lightning 风格路径之上。读者若想验证上述机制可重点阅读 pkg/ddl/backfilling_dist_scheduler_test.go 与 pkg/ddl/backfilling_test.go 中的测试用例——它们直接构造了IsDistReorg: true的 reorg meta 来驱动调度与执行链路。10. 展望Further设计文档在结尾列出了后续演进方向这些方向在架构上也适用于更通用的分布式后台任务场景优化 backfill 处理子任务的调度策略使用更灵活、更合理的子任务切分与抢占机制结合资源管理功能防止小 reorg Job 被大 reorg Job 阻塞框架更通用当前的形态与接口已经相对通用但仍较简单未来可改进使该框架可用于更多将 DDL reorg 视为较慢后台任务的场景考虑移除 DDL Owner 的设计移除 reorg worker 层每个 TiDB-server 仅保留一个 DDL worker 用于 schema 同步等工作。小结分布式 DDL Reorg 的本质是把Owner 单机把一张表扫完、建完索引重构为Owner 切分与巡检 全集群 backfill worker 通过共享元数据表抢占执行的两段式协同。围绕这一转变需要解决子任务元数据建模DDLReorgMeta.IsDistReorg、tidb_background_subtask/history 表、认领与租约exec_id、exec_expired、curr_key、故障与取消恢复、历史表清理等一整套工程问题。当前 TiDB 仓库中该设计已与通用分布式任务框架融合落地读者可通过tidb_enable_dist_task等相关开关与 pkg/ddl 下的调度器/执行器源码进一步追踪其行为。【免费下载链接】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),仅供参考
返回列表