1
0
0
0
专栏/.../

天下武功,唯快不破 : TiDB DDL 语句执行性能整体提升 10 到 50 倍

 Benjamin  发表于  2025-01-03

作者:居佳佳,郭铭浩,熊亮春

概要:

去年,我们成功将 TiDB DDL 创建索引的性能提升了 10 倍,为用户带来了显著的体验改善。然而,随着越来越多 SaaS 用户深入挖掘 TiDB 的潜力,我们发现,在多租户大规模部署场景下,传统单机数据库的管理复杂性问题仍困扰着用户。TiDB 的强大扩展性虽然能很好地解决这个问题,但我们也发现了一些限制了扩展性天花板的瓶颈。

为了进一步释放 TiDB 的扩展潜力,我们在过去半年对 TiDB DDL 语句执行流程进行了全面的优化和架构重构。这些优化为未来实现 TiDB DDL 的真正分布式执行奠定了坚实基础。本文将详细介绍我们在这一过程中的技术细节和取得的成果。

关键字:

元数据:用来定义并指导数据库系统如何解析与处理用户存在数据库中数据的数据。

General DDL 语句:这类 DDL 语句的完成,只涉及元数据修改即可。

Reorg DDL 语句:这类需要处理用户实际数据的 DDL语句。

背景

TiDB 在线 DDL 变更功能曾有效缓解了用户在数据库使用和演进过程中的痛点。然而,随着用户规模和数据量的不断增长,创建索引的性能瓶颈日益凸显。我们通过优化索引构建流程,将索引创建速度提升了 10 倍。随后,我们将索引创建任务迁移至分布式框架,进一步提升了大表索引的构建效率。

随着 SaaS 用户对 TiDB 的深入应用,百万级表的场景对 DDL 框架提出了更高要求。一方面,我们需要显著提升框架的吞吐量,以在有限时间内完成大量 DDL 操作;另一方面,需要保证框架在高并发、高负载下的稳定性与扩展性。为此,我们在过去半年里,重点优化了框架的扩展性,提升了 DDL 语句的执行效率。

优化思路简介

为了让大家更清晰地了解我们的优化思路,我们将在本部分详细介绍 TiDB DDL 语句,尤其是在线 DDL 变更的执行流程。 通过对原有流程的深入剖析,将更好地阐述我们在优化过程中所面临的挑战以及采用的解决方案。在这部份最开始,我们先给大家展示一下我们优化前后的效果曲线,给大家一个直观的感受。

在我们 v8.3 完成了上述发现的优化之后,

  1. TiDB v8.2 General DDL 性能优化效果显著

通过对比 TiDB v8.1 和 v8.2 的 DDL 任务执行 QPS,我们可以清晰地看到,v8.2 版本的性能得到了大幅提升。在 v8.1 中,DDL 任务的平均 QPS 约为 7,而 v8.2 则达到了 38,峰值更是达到了 80,性能提升了约 5 倍。这表明,TiDB 团队在 v8.2 版本中对 DDL 语句的执行效率进行了深入优化,取得了显著成效。

  1. TiDB v8.3 在 DDL 性能方面取得了显著提升

与 v8.2 版本相比,TiDB v8.3 在 DDL 任务执行的 QPS 上实现了大幅提升。v8.3 的最大 QPS 达到 200 左右,平均 QPS 也提升至 180 左右,性能表现更加稳定。这表明 TiDB 团队在 v8.3 版本中对 DDL 语句的执行效率进行了持续优化,并取得了令人瞩目的成果。

启用 Fast Create Table 优化后,DDL 操作的每秒查询次数(QPS)可提升一倍,显著提高系统的整体吞吐量。

TiDB DDL 运行过程简介

在深入探讨 TiDB DDL 优化之前,我们先来了解一下 TiDB DDL 语句的执行过程。TiDB 作为一款支持在线 DDL 的分布式数据库,其 DDL 操作能够在不影响业务的情况下进行。我们将重点介绍在线 DDL 变更的执行流程,包括 SQL 解析、Job 创建、后台执行等环节,为后续的优化讲解打下基础。

DDL 语句任务运行流程

TiDB DDL 执行流程

当用户通过客户端向 TiDB 提交一条 CREATE TABLE 语句时,TiDB 会依次执行以下步骤:

  1. SQL 解析: TiDB 的 SQL 解析器会对该语句进行解析,生成相应的执行计划。
  2. 任务创建: 系统会为该 DDL 操作创建一个新的任务,并将任务信息添加到 DDL 任务队列中。
  3. 任务调度: DDL Owner 会从任务队列中取出待执行的任务,并交由调度器进行分配。
  4. 作业执行: 调度器会为该任务分配一个 Job Worker,每个 Worker 负责执行一个任务。对于 reorg DDL 类型的任务,通常需要多个 Worker 并行处理。
  5. 状态更新: 各个 Worker 会将执行状态反馈给系统,系统会实时更新任务的状态。
  6. 结果返回: 一旦任务执行完成,系统会将执行结果先返回给接收 DDL 任务的 TiDB 节点
  7. 结果返回:再由 TiDB 节点将任务执行结果返回给客户端。

在线 Schema 变更简介

前面我们介绍了 TiDB DDL 任务的整体执行流程。接下来,让我们聚焦到在线 Schema 变更的细节上。当一个 Job Worker 接收到一个在线 DDL 任务时,它会按照以下步骤逐步完成任务:

  • 执行单步变更: Job Worker 会根据任务定义,执行一次在线 Schema 的变更。每一次变更都代表着 Schema 向目标状态迈进了一步,即进入下一个状态,可能的状态包括 write-only 和 delete-only 等。
  • 状态更新: 完成单步变更后,Job Worker 会将当前的 Schema 状态更新到元数据中。

为了保证 schema 变更的正确性,online-schema 算法维持了以下的不变性:在任何时间,针对某个 schema 对象,整个集群中最多只有两个相连的状态存在。因此在执行完单步变更后,Job worker 需要等待系统中的所有 TiDB 节点都推进到改动后的状态,然后才能执行下一步,直到 Schema 达到最终的目标状态。

因此,在线 Schema 变更会循环执行以下几个步骤:

  1. 推进变更状态: Job Worker 会根据任务定义,将 Schema 向目标状态推进一步。这相当于为 Schema 的演进打了一个补丁。
  2. 通知 PD: 变更完成后,Job Worker 会立即通知 PD(Placement Driver),告知其 Schema 版本已更新。PD 作为 TiDB 集群的“大脑”,负责协调各 TiDB 节点的状态。
  3. 触发 TiDB 节点更新: PD 会通过 ETCD 的 Watch 机制,实时监控 Schema 版本的变化。一旦检测到版本更新,PD 会立即通知所有注册到 PD 上的 TiDB 节点,要求它们更新本地 Schema 信息。这就像广播通知一样,确保所有 TiDB 节点都保持一致的 Schema 信息。
  4. MDL 检查: TiDB 节点接收到更新通知后,会加载对应的 schema 变更,之后会触发 MDL(Metadata Locking)机制进行检查。MDL 就像一把锁,确保在 Schema 变更过程中,不会有其他操作同时修改 Schema,从而避免数据不一致。
  5. 反馈执行结果: MDL 检查通过后,TiDB 节点会将检查结果反馈给 PD,确认 Schema 更新已生效。
  6. 等待所有节点同步: Job Worker 会等待所有节点都确认 schema 更新已生效,即保证上面的不变性,之后 Job Worker 会继续处理该 Job 后续的变更任务。

思考问题:

  • 为什么需要 PD 参与? PD 作为集群的协调者,负责通知各个 TiDB 节点更新 Schema 信息,确保集群的一致性。
  • ETCD 在这个过程中扮演了什么角色? ETCD 作为分布式键值存储,存储了 TiDB 集群的元数据信息,包括 Schema 版本。PD 通过观察 ETCD 中的 Schema 版本变化来触发节点更新。
  • MDL 机制为什么重要? MDL 机制保证了在 Schema 变更过程中,不会有多个操作同时修改 Schema,从而避免数据不一致。你可以想象成在修改一个文档时,需要先锁定文档,防止其他人同时修改。

通过以上步骤,TiDB 能够安全、高效地执行在线 Schema 变更,保证业务的连续性。

工程与最佳实践探索

工程思考

如里程碑简介中图所示,我们针对 General DDL 类型的优化制定了一条清晰的路线图。考虑到 TiDB DDL 执行框架经过多年的迭代已相对稳定,为确保优化过程的安全性和高效性,我们在优化之初确立了以下几条原则:

  • 客户需求驱动: 以客户需求为导向,每次优化都聚焦于解决最迫切的问题,避免大范围的重构。这种方式能有效保证优化质量,并快速交付给客户,提升客户满意度。
  • 小步快跑: 将大型优化任务拆分成一系列小型的、可独立交付的子任务。这种方式不仅降低了开发难度,也便于快速验证优化效果,更符合敏捷开发的理念。
  • 最小化影响: 每个子任务的设计都应尽量减少对现有系统的干扰,同时为后续的优化打下基础。

实践证明,这些原则在 DDL 优化项目中取得了良好的效果。我们成功将一个复杂的优化任务分解为多个可管理的子任务,并通过持续迭代的方式,逐步提升了 DDL 执行性能。

具体来说,我们通过以下方式来实现这些原则:

  • 需求细分: 针对客户提出的 DDL 优化需求,我们进行深入分析,将其拆分为一系列具体的优化点。
  • 独立子任务: 每个优化点都对应一个独立的子任务,并制定详细的开发计划和测试用例。
  • 持续迭代: 每个子任务完成后,都会进行充分的测试和验证,并快速交付给客户。

在接下来的章节中,我们将详细分享我们在 DDL 优化过程中遇到的挑战、采用的解决方案以及取得的成果

里程碑简介

从上图可以看出,在 TiDB v7.5 左右,我们面临着一个严峻的挑战:用户希望在一个 TiDB 集群中管理百万级表。然而,我们的测试结果显示,TiDB v7.6 在创建 50 万张表后性能急剧下降,无法支撑更大规模的建表需求。这表明 TiDB 在大规模建表场景下存在严重的性能瓶颈,阻碍了其在海量数据场景下的应用。为了解决这一问题,我们团队投入了大量精力,对 TiDB 的核心组件进行了深入优化。

基于上述工程思考原则,我们深入探索并结合实际情况,制定了如下优化路线图:

目标明确:优化起点

首先,我们发现创建表在 DDL 语句中较为特殊,因此将其作为优化起点。通过深入分析,我们将创建表的需求独立出来,以便集中优化。

聚焦关键:优化策略

同时,为了避免过度定制化,我们着力于识别通用优化点。经过仔细分析,我们确定将“快速建表”作为突破口,并从 v7.6 版本开始落地。我们的目标是快速、稳定地交付给客户,提升产品竞争力。

效果显著:性能提升

经过优化,我们在 v8.1 版本实现了在 4 小时 17 分钟内创建 100 万张表,我们并没有止步于此。

持续改进:深入优化

在 v8.2 版本,我们在此基础上进一步深入优化通用 DDL 操作,对优化点进行了更细致的定位分析,并取得了显著的性能提升。v8.3 版本,我们继续优化,将创建一百万张表的时间缩短至 1.5-2 小时。

夯实基础:代码重构

为了更好地支持后续优化,我们在 v8.4 和 v8.5 版本中对 DDL 代码进行了重构,提升了代码的可维护性。

展望未来:分布式革新

未来,我们将重点打造一个分布式原生的 DDL 执行框架,实现极致的 DDL 执行性能。通过并行化 DDL 执行、分布式事务支持和智能化资源调度等技术,我们将充分发挥分布式系统的优势,提升 DDL 操作的效率。

下图是我们到 8.1 版本时,优化的效果展示:

Table Num TiDB v7.5 TiDB v7.6 TiDB 8.0 Perf enhancment ratio
100k 3h30m 26m 9m39s 21.76
300k 59h20m estimate > 6 hours 30m38s 118

快速优化点介绍

通过对建表流程的深度优化,TiDB v8.1 版本的建表速度相比 v7.6 版本极致提升了 118 倍。我们主要从以下三个方面入手:

  • 加速 DDL 语句执行: 通过优化任务调度和执行流程,显著减少了建表等待时间。
  • 优化元数据操作: 引入缓存机制,减少磁盘 I/O,提升元数据操作效率。
  • 并行化建表: 将建表任务拆分,充分利用系统资源,加速大量表的创建。

这些优化使得 TiDB 在 SaaS 场景下能够快速响应用户需求,提升用户体验,增强了 TiDB 的竞争力。

  • DDL 语句就在接受任务的节点执行(这个也是我们分布式执行的终态目标)

    •   我们直接在接收任务的节点执行 DDL 语句,充分利用了分布式架构的优势。由于创建表语句的特殊性,我们可以跳过复杂的任务队列,直接在执行节点完成操作,从而显著减少任务处理时间。
  • 使用唯一索引来建表唯一性检查

    •   为了实现全局唯一性检查,我们在 TiDB 的元数据中引入了一个专门的索引结构,用于存储所有表的名称。在执行创建表操作之前,系统会先查询该索引,判断是否存在同名表。如果存在,则拒绝创建。
  • 批量执行可以提高资源利用率,降低系统开销:

    •   为进一步提升系统性能,我们对建表任务的执行方式进行了优化。通过将同一节点上的多个建表任务合并为一个事务进行批量提交,我们可以减少事务的提交次数,降低系统开销,同时充分利用计算资源,显著降低单个表的创建时长。

深度优化过程详解

经过对创建表流程的深度优化,TiDB v8.1 版本的性能得到了显著提升,迅速赢得了客户的认可。然而,我们并未止步于此。我们依然有一些遗留问题需要我们持续优化演进,其中最为突出的是:

  1. 优化是针对创建表定制的, 做成进一步能够演化所有 general DDL 语句的优化,还有很多其他工作需要准备;
  2. 不太适合继续做深度优化;

为了进一步提升 TiDB 的整体 DDL 性能,我们从 v8.2 版本开始,将优化目标转向了更通用的 DDL 语句执行路径。通过对公共执行路径的系统性优化和 DDL 框架的重构,我们旨在为所有 DDL 操作提供更优异的性能。

梳理性能优化瓶颈点

经过对 DDL 任务执行全流程的深入分析,我们发现,在集群表数量庞大的场景下,由于历史原因导致的快速迭代实现,原有框架的执行流程存在多个明显的瓶颈点。这些瓶颈点主要集中在以下几个方面:

  • 对于 DDL 任务调度方面
  • 库/表的存在性判断路径的效率方面
  • 计算资源的利用率方面
  • 广播机制的效率优化

对于 DDL 调度的优化

经过对 DDL 任务调度流程的深入分析,我们发现原有的以 DDL 语句执行状态机步骤为单位的调度策略存在优化空间。 为此,我们进行了如下改进:

  • 调整调度粒度: 将调度粒度由状态机步骤调整为整个 DDL 任务。这样一来,每个 DDL 任务都能在调度器中得到更完整的处理,减少了不必要的调度开销,从而提升了调度效率。
  • 提升并发度: 优化调度策略,允许在每一轮调度中并行执行多个相互独立的 DDL 任务。通过这种方式,我们最大化地利用了系统资源,缩短了整体的 DDL 执行时间。
  • 增加执行资源: 为了更好地支持并行执行的 DDL 任务,我们扩大了执行 general DDL 任务的 worker pool 容量。这使得多个 DDL 任务能够同时进行,显著提升了系统的吞吐量。
  • 简化调度逻辑: 通过优化调度算法,我们简化了判断 DDL 任务是否可执行的逻辑,降低了调度过程的复杂度,提高了整体的调度效率。

通过上述优化,我们显著提升了 DDL 任务的调度效率,降低了系统资源的消耗。

优化库/表的存在性检查

目前,当运行创建表/模式作业时,我们使用两种方法检查它的存在,这取决于当前节点的所有者的模式版本是否等于TiKV中的版本,参见:

func checkTableNotExists(d *ddlCtx, t *meta.Meta, schemaID int64, tableName string) error {
        // Try to use memory schema info to check first.
        currVer, err := t.GetSchemaVersion()
        if err != nil {
                return err
        }
        is := d.infoCache.GetLatest()
        if is.SchemaMetaVersion() == currVer {
                return checkTableNotExistsFromInfoSchema(is, schemaID, tableName)
        }

        return checkTableNotExistsFromStore(t, schemaID, tableName)
}

我们使用单个任务执行 worker 串行运行通用DDL作业,因此在大多数情况下,存在检查将通过第一个分支,即通过模式缓存,除非有另一组 worker 运行的重组作业。

如果我们想同时运行通用DDL作业,node的架构版本可能比TiKV中的版本更小,因为并发作业可能增加了版本但没有等待架构更改,所以检查更有可能转到第二个分支。集群中的表越多,第二个分支越慢。

我们实际上可以删除第二个分支,使用缓存的信息模式进行检查就足够了。基本原理是:

  • 信息schame缓存在owner节点上正确同步,因此架构缓存中的现有表首先被正确检查。我们在运行作业之前计算作业依赖关系(参见上面的优化作业依赖计算部分,我们需要检查表/架构名称的依赖关系),因此不会有两个创建同一表的作业同时运行。
  • 如果有两个所有者AB在短时间内同时运行,假设我们有两个连续的作业J1J2正在创建同一个表。当A先看到J1B先看到J2时,这两个作业可能同时运行。但是,如果B先看到J2J1必须已经完成并同步,并从表中删除tidb_ddl_job,因为我们按作业ID的顺序查询作业(见上文优化作业依赖计算部分)。在同步J1时,B应该已经同步了模式缓存中的表,所以当B运行J2时,检查存在将失败,J2将失败。
  • 此外,当TiDB成为DDL所有者时,在我们开始处理作业之前,我们应该重新加载架构以确保它是最新的,以防由于网络问题导致此节点不同步并突然成为所有者。

我们目前采用串行方式执行通用 DDL 作业,并通过模式缓存进行快速检查。这种方式在大多数情况下能满足需求。然而,当多个作业并发执行时,由于节点的架构版本可能滞后于 TiKV,导致检查逻辑进入较慢的第二分支。

为了提升性能,我们建议取消第二分支,直接依赖模式缓存进行检查。原因如下:

  1. 模式缓存的准确性: 模式缓存会在所有者节点上得到准确同步,因此通过缓存检查表是否存在是可靠的。
  2. 作业依赖关系: 我们在执行作业前会计算作业之间的依赖关系,确保不会同时创建相同的表。
  3. 作业顺序: 即使存在多个节点同时执行创建相同表的作业,由于我们按作业 ID 的顺序查询作业,后一个作业在执行前会同步模式缓存,从而避免重复创建。
  4. 所有者节点的架构同步: 当节点成为 DDL 所有者时,我们会重新加载架构,确保其与 TiKV 保持一致。

通过以上优化,我们可以显著提升 DDL 作业的执行效率,并提高系统的稳定性。

更多优化措施:

  • 索引优化: 为了加快表/模式名称的查询速度,我们计划在后续的 Data Dictionary 项目中为表/模式名称添加索引。
  • 容错机制: 尽管模式缓存通常是可靠的,但为了应对极端情况下的同步问题,我们可以在检查逻辑中加入额外的容错机制。

计算资源的利用率方面

TiDB 的模式同步机制最初采用定时轮询的方式,即在模式变更后,系统会以固定的间隔反复检查模式是否同步完成。这种方式存在明显的性能问题:当模式变更频繁或 TiDB 节点加载模式信息较慢时,大量的无效检查会严重拖慢系统。

相比之下,ETCD 的 Watch 机制提供了一种更为高效、实时的方式。通过在 ETCD 中设置一个表示当前模式版本的键值,并对该键值进行 Watch,一旦模式发生变化,ETCD 就会立即通知订阅者。订阅者接收到通知后,即可触发模式同步,不再需要等待固定的时间间隔。这种方式显著提升了系统的响应速度,避免了不必要的资源浪费。

替换掉 DDL 结束时的广播机制

前面的详解中介绍到了 DDL 任务的提交和执行分开的,只有 owner 节点负责执行,DDL 提交线程会定期(目前默认的最小定时周期为 500ms)或者根据事件来查询历史表来判断该 DDL 是否完成执行。当 DDL job 结束时,如果该任务是在 owner 节点提交的,owner 会通知对应的线程,在最初的实现中,owner 会将 DDL 完成的事件发送到队列中,但是所有的提交线程都会尝试处理,这会导致该事件可能被错误的的线程处理,导致该 DDL 提交线程不能尽快结束,而是要等一个定期检查,这会降低整体 general DDL 的执行吞吐。

在这次优化中,我们改成定向通知,只有跟对应 Job 匹配的提交线程能够接收到该事件。

DDL 框架代码重构

由于现有的 DDL 框架在设计和实现上存在诸多问题,导致其无法满足不断演进的业务需求,因此我们决定进行一次全面的重构。

问题分析

  • 设计老化: 框架的设计理念和技术选型已无法适应当前的业务场景和技术趋势,导致扩展性差、灵活性不足。
  • 代码质量降低: 经过多年的问题修复与局部优化,DDL 相关代码结构变的混乱,耦合度升高,可读性差,难以维护。大量的重复代码和冗余逻辑增加了代码维护的难度。
  • 测试覆盖率低: 缺乏全面的单元测试和集成测试,导致代码质量难以保证,引入新功能的风险较高。
  • 迭代效率低: 由于代码结构复杂,每次修改都可能引入新的问题,导致开发周期长,难以快速响应业务需求。

重构目标

  • 提升代码质量: 通过重构,将代码结构优化为模块化、松耦合的设计,提高代码的可读性、可维护性和可扩展性。
  • 增强测试能力: 建立完善的测试体系,包括单元测试、集成测试和端到端测试,保证代码质量,降低缺陷率。
  • 优化架构设计: 采用更先进的架构模式,如微服务架构或领域驱动设计,提高系统的可伸缩性、容错性和可维护性。

重构策略

  • 小步快跑: 将重构任务拆分成多个小步,每次只修改一小部分代码,降低风险,并及时验证修改效果。
  • 持续集成: 通过持续集成工具,频繁构建和测试代码,及时发现并修复问题。
  • 代码审查: 严格进行代码审查,保证代码质量,并促进团队成员之间的知识共享。

预期结果

  • 框架更稳定: 通过重构,可以提高代码质量,减少潜在的 bug,从而提高系统的稳定性。
  • 开发效率更高: 重构后的代码结构清晰,可维护性更高,开发人员可以更快地理解和修改代码,提高开发效率。
  • 扩展性更强: 优化后的架构设计可以更好地适应未来的业务需求,方便添加新的功能和模块。

潜在风险及应对措施

  • 风险: 重构可能引入新的 bug,影响系统的稳定性。
  • 应对: 采用小步快跑的方式,逐步推进重构,并进行充分的测试。
  • 风险: 重构可能导致开发进度延缓。
  • 应对: 合理规划重构任务,并与业务需求协调,确保重构不会影响正常的业务运行。

总结

DDL 框架的重构是一项复杂而艰巨的任务,但其带来的收益是巨大的。通过重构,我们可以打造一个更加稳定、高效、可扩展的 DDL 框架,为未来的业务发展提供坚实的基础。

优化效果

  1. 场景验收测试

为了全面评估 TiDB v8.5 版本中 DDL 性能的优化效果,我们在实验室搭建了一个专用的测试集群。该集群的硬件配置如下:

节点类型

数量

规格

PD

1

8C16G

TiDB

3

16C32G

TiKV

3

8C32G

测试结果如下:

Operations

v7.5

v8.3

Description

Create 100K tables

3h49m

11m (20X faster)

4m (50X faster) if Fast Create Table enabled

Create tables inside single DB

Create 1M tables

more than 2 days

1h30m (50X faster)

Create 10K schemas, each have 100 tables.

Create 100K schemas

8h27m

15m (32X faster)

100K add-column

6h11m

32m (11X faster)

All tables are created inside single DB

我们验证了 TiDB DDL 执行能力的显著提升,也是证明我们对于分布式 DDL 执行架构的演进方向和路径是正确且可行的。这为我们未来在分布式数据库领域更深入的探索奠定了坚实基础。

后续工作

目前,我们的DDL架构演进仍处于起步阶段。未来,我们将持续优化架构,使其更加清晰、简洁,并不断提升DDL服务的稳定性、性能和扩展性,最终打造一个具备高执行效率和高线性扩展能力的分布式执行子系统,为TiDB提供坚实的底层支撑。

1
0
0
0

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

评论
暂无评论