近一年我们在 DDL 方面做了一些优化,可能跟原来介绍 DDL 实现的文章有许多出入。所以这次介绍一下近期的一些优化,这些优化是在之前分享过一篇关于 TiDB schema 异步变更实现的文章的基础上做的。看优化前建议先看一下之前的实现。
原来我们根据 Google F1 的在线异步 schema 变更算法实现的 DDL 功能,兼容了一下常用的 MySQL DDL 语句。目前在实际应用中有一些不足,主要分为两个部分,一方面是功能上支持的不够全面,一方面是运行时间上有些慢。
就刚提到的第一个方面,我们也在根据用户的要求添加功能,当前功能欠缺的情况可以参考与兼容 MySQL 情况。当然我们非常欢迎社区朋友向 TiDB 提 DDL 相关的 PR。 关于第二个方面,其实是因为用户的需求也是与时俱进的,从一开始业界几乎停业务更新 schema 的状况,到 TiDB 支持异步更新 schema,现在希望尽可能快的在线异步更新 schema。当然这是一个符合发展规律的需求,也是 TiDB 一定会满足的需求。那么我们是怎么来满足这个需要,首先需要详细介绍一下,原来在这方面的具体不足。
这里会简要说明一下在优化前执行 DDL 操作遇到的一些性能问题。 从整体角度上看,每个 DDL 操作的时间都是依赖 lease 的设置,每个状态变更需要 2 * lease,那么每个 DDL 操作至少是 2 * lease。而且 lease 为了安全起见至少是 1s,有些线上用户一开始会谨慎的设为 10s。那么一个 create table 操作,什么也没做,这个操作至少要执行 2s(lease=1s)。 从某些特殊操作,例如 add column 或者 add index,因为可能涉及实际的修改数据,由于数据量大的原因,整个操作会特别慢。 接下来我们会具体展开,从整体优化和特殊操作优化分别介绍优化的内容,当然也少不了对将来优化的规划。
将 owner 选举放在 PD 上处理,并用 PD 通知所有 TiDB DDL 状态变更的情况。前者可以略微减少 TiKV 压力,也解决用本地时间选取 owner 的隐患,后者可以减少每个 DDL 操作处理的等待时间。
每个 DDL 对应一个竞选 owner 的 goroutine,它用来判断此 DDL 是否为 owner。也就是说与原来逻辑一样,每个 TiDB 的 DDL 知道自己是否为 owner,不知道其他 TiDB 的 owner 信息。 具体方法是用 PD 中内嵌的 etcd 接口,通过 Session 创建的 Election 调用 Campaign 接口进行 owner 选举。如果选举成功,那么将此 DDL 设置为 owner ,并监听这个 owner 对应的路径,那么当它不是 owner 时,可以收到通知,更新自己为非 owner 的信息。如果选举不成功,那么它为非 owner, 并一直排队,等到选举成功后做选举成功的流程。
分两方面:
准备工作
第一台启动的 TiDB,在 PD 建立一个 latest version 信息路径,里面存有最新的 schema version 号。 所有 TiDB 启动时需要做如下操作:
对比原逻辑
对于具体流程的细节就不在这里描述了,对应原来逻辑分别是:
当然这个还涉及许多细节,也需要更新 TiDB 原本对 information schema 的有效性的处理,由于内容有点多,就不在这里细说了。
将断联的 TiDB Sever 信息从 PD 中移除,减少由于某台 TiDB Server 出现网络分区或者挂机的情况,导致 DDL 处理都需要等待 2 * lease。首先在一开始的时候将 TiDB Server 的信息改存为 TTL 的结构,设置超时为 max(10 min)。其次在 TiDB 接收到退出信号时,主动像 PD 发出清理请求。
第一个优化,可以减少每个 TiDB 在一秒内对 TiKV 的访问次数。第二个优化,将一个 DDL 操作的执行时间从原来几秒到几十秒(具体依据 lease 的设置)减少到现在的几十毫秒。
对于原来 drop schema、drop table 和 drop index 等操作,在真正删除数据时,用 delete range 的方式替换原来放到后台处理的操作流程。 在前面举例的操作结束时,将对应的表信息和 range 信息(即数据的起始 key)记录到内部的特定表中。真正的清除数据的操作,是由 TiDB GC worker,在清理其他过期数据时同时清理。这个操作的优点,其一是把原来 background 部分的代码可以全部清理,不需要维护两部分类似代码;其二是它是直接调用 TiKV 的接口直接清理数据,不需要 TiDB 一批一批串行的清理数据,即方便又省各种资源。
这个优化效果会特别显著,因为实际上我们最后没有存那些数据。那么整个操作就不关心表的数据行数,整个操作只需要进行 5 个状态的变更即可。此操作前后做了两个优化: 新加列的 Default Value 是一个空值,那么就不需要实际的去填充。之后对此列的读取时,从 TiKV 返回的列值为空时,查看此列的元信息,如果它是 NULL 约束则可直接返回空值(这逻辑会在 Coprocess 处理)。 新加列的 Default Value 的值为非空的情况下,也不用将 Default Value 存储到 TiKV,只需将此默认值存到一个 schema 的字段(Original Default Value)中。在之后做读取操作时,如果发现 TiKV 返回此列的的值为空,且这个 schema 字段中的值为非空,那么将此字段中的值填充给这一列,然后返回(这逻辑会在 Coprocess 处理)。
原先 add index 最后填充数据就是通过批量处理。这样做是为了防止此操作的事务与其他在操作此 index 的事务发生冲突,导致整个 add index backfill 的操作重试从而进行分批处理。但是这个批量不是并发处理,只是为了减少冲突域做的。优化前串行逻辑是先扫一批 key,扫完之后对这批 key 的值进行修改。针对这个操作我们目前做了两次主要的优化。
此次优化主要分两部分:
此优化也分为两部分:
在数据连续的情况下,能减少约 22% 的耗时(数据量在 1 亿行,4 个 TiKV,1 个 PD,2 TiDB)。但是 handle 可能被批量删除过或者插入时就是非常离散等原因,还是会使整个操作变慢,所以新的优化在计划中。
希望查看当前 DDL 正在运行、等待运行以及已执行完成的 DDL job 时可以使用的语句。具体 SQL 语句如下:
ADMIN SHOW DDL JOBS
由于目前 DDL 操作都是串行执行的,操作人员由于手误或者手动重试,导致在一个大表上多次执行 add index 这种耗时比较久的操作,希望取消这个 DDL 语句的执行时,可以使用这个功能。具体 SQL 语句格式如下:
ADMIN CANCEL DDL JOBS 'job_id' [, 'job_id'] ...
性能优化部分,我们目前在计划中的是 DDL job 按 table 级别并行执行,以及 add index 操作的加速工作。前者考虑从适当的添加处理的 worker 开始。后者会从分离读写数据操作,进行并行处理;读取数据时考虑剪枝;以及考虑更准确的方式估测 handle 区间等多个方面着手。当然还有兼容性方面,目前我们还是会根据用户实际需求进行更全面的兼容 MySQL 的 DDL 操作。另外,DDL 测试方面也会添加更多针对性的集成测试。
如果大家有发现 DDL 请求处理慢的情况,可以查看可能原因,如果都不是可以第一时间联系我们(如果是外部人员,可以提 issue),也希望大家能保存当时的日志。这里再说明一下关于 MySQL 的兼容性情况。
最后,PingCAP 在持续火热招聘中,欢迎大家加入我们,也非常欢迎大家给 TiDB 提 PR,不管是入司还是在外部的人员,特别感谢!