共计 6421 个字符,预计需要花费 17 分钟才能阅读完成。
近日,TDSQL 新敏态引擎重磅公布。** 该引擎可完满解决对于敏态业务倒退过程中业务状态、业务量的不可预知性,实现 PB 级存储的 Online DDL,能够实现大幅晋升表构造变更过程中的数据库吞吐量,有效应对业务变动;其独有的数据状态主动感知个性,使数据能依据业务负载状况实现主动迁徙,打散热点,升高分布式事务比例,取得极致的扩展性和性能。
**
与此同时,TDSQL 新敏态引擎还具备对分布式事务残缺反对的个性,撑持了下层计算引擎多主读写架构的实现,并与计算引擎联合实现了计算下推、分布式事务一阶段优化等多维度优化,进一步实现分布式数据库系统性能极致晋升,无效适配企业新敏态业务需要。在腾讯外部业务实际中,TDSQL 新敏态引擎可撑持业务在放弃高性能且间断服务的根底上,一个月内实现高达 1000 次表构造在线变更。
在高频的表构造变更过程中,如何缩小对在线业务申请的影响,甚至使得用户可能以原生、不阻塞业务的形式进行,这就成为了 TDSQL 新敏态引擎面对的技术挑战。本期将由腾讯云数据库高级工程师赵东志,为大家深度解读 TDSQL 新敏态引擎 OnlineDDL 的原理与实现。以下是分享实录:
Instant DDL
TDSQL 新敏态引擎的外围架构 。SQLEngine 是计算层,次要负责 SQL 的解析、散发,包含数据查问,将 SQL 转为 KV,再将 KV 收集的后果转化为 SQL 能获取到的后果,最初传输到客户端等环节。其中,DDL 也是计算层负责的局部之一。
过来在单机零碎下 DDL 的执行形式分为两种:MySQL 对反对的局部 Online DDL,不反对的局部则通过内部组件 pt 工具对每个 DB 节点做 DDL。在集群规模比拟大时,运维会变得更加简单,须要用内部工具保障多个节点之间 DDL 的原子性,每个节点还须要预留两倍存储空间。
基于上述起因,TDSQL 新敏态引擎的设计指标定为三个方面:
● 保障 Online 性质,做到不阻塞业务读写申请。
● 保障多节点缓存一致性,使得 Crash-safe 等在 TDSQL 零碎中自治。
● 兼容 MySQL,不便业务迁徙。
咱们以加列为例来介绍 Instant DDL。下图中的 Client,蕴含一个表构造,是一条映射语句。在 F1 列的根底上,插入一个 pk=10、F1= 1 的数据行。插入后再进行加列操作,退出 F2 列,加列后的表如下图 TDstore 所示。这时如果须要读取前述两行数据,就会遇到问题。在读取 pk=11 这行时,能够用最新的表构造间接解析数据行,但在读取 pk=10 这行时,咱们须要晓得该数据行里是否蕴含 F2 列。
为此咱们在表构造中引入版本号概念。比方初始列表的版本为 1,做加列操作后,schema 变为 2,插入时再将版本 2 写入到 value 字段中。读取数据时须要先判断数据行的版本,如果数据行版本为 2,就用以后的表构造解析;如果数据行版本为 1,比以后版本小,确定 F2 列不在该版本的 Scheme 中后,可间接填充默认值再返回到客户端。
通过版本号概念的引入,在整个加列过程中,只须要更改元数据,即 Scheme 的信息并未更改数据行,加列过程变得更加疾速高效。同样的形式也可作用于 Varchar 扩大长度等无损数据类型的转换,Index invisible 等其余 DDL。但并不是所有的 DDL 都能够仅批改元数据,局部 DDL 还须要生成局部数据能力实现,比方加索引操作。因为索引的生成是从无到有的过程,因而必须要生成局部数据,无奈通过间接批改表构造来实现。
Add/Drop Index
以加索引为例,下图右边所示为 TDSQL 新敏态引擎索引数据库存储构造。图中有两行数据,有一个主键和一个索引。在 TDStore 中,每个索引都会有全局惟一的 index ID,比方主键为 index1,二级索引为 index2。主键数据由 index ID+pk 组成,造成 key,value 为其余字段。在索引中,它的组成为索引 indexID+ 索引信息 + 主键信息。
如果要进行 alter table、add index 操作,从无索引状态变为索引,则须要扫主键数据,组建索引的 KV 模式,插入 index 中进行扫描,再批改元数据,以实现索引的增加。如果在扫描主键、批改元数据的同时,存在并发事务如 delete 或 insert 等操作,就会产生扫描回填的索引过程与用户事务并发之间的问题。
针对 DDL 和用户申请的并发问题咱们能够将 DML 分为 delete、insert、update 三种来加以探讨。
对于 delete,咱们能够 scan 任意一行数据,再按索引模式将其插回到 TDstore 中。假如存在一个并发,两数据行为同一行,删除操作相当于插入一个类型为 delete 的 key。指标是在主键上删除该数据行,在索引上也删除该数据行。如果不计后果直接插入,就会遇到问题。比方删除后,又插入到该数据行后,最终的后果是,key 被删除后在索引上再次出现。
为解决上述问题,咱们引入了托马斯写机制,在插入时先查看版本,看是否存在更新的写入,如果有更新的写入,则该条 key 就不能再被写入。这里采纳工夫戳的比拟机制。在 scan 时,基于 TDStore 提供的全局一致性读,咱们在读取时会获取一个工夫戳,比方 1。在事务中插入时,其工夫戳也通过 TDStore 来获取,读取数据所用工夫戳也会带进去,即在该工夫戳读,写时也用同一时间戳,TS 为 1。在同一条 key 中,如果发现存在比本人更大的 ts,阐明该 key 已被用户更改过,则 put 不失效,以此来解决并发问题。
对于 insert,如果插入一条新数据,与以后数据行无抵触,即以后数据行无该条数据,这时只须要在索引上也插入该行数据即可。update 相当于 delete+insert 的组合,在 delete 和 insert 问题解决后,update 问题也会天然解决。咱们通过托马斯写规定机制解决回填索引与用户事务的并发问题。
在分布式系统中咱们还会面临另一个问题,即多个计算节点之间的缓存一致性问题。因为在 TDSQL 中,下层计算节点能够有很多个,且每个计算节点还会有本身的缓存。以索引为例,假如某个 DDL 在 SQLEngine1 上执行一个 add index idx_f1,此时 SQLEngine1 上并发的执行一个插入操作,则会在主键,索引上别离插入一行 kv,如果这时另一个计算节点 SQLEngine2 因为缓存更新不及时,获取到的表构造没有 idx_f1,如果接到删除申请,在解析完该表构造后,该计算节点只会删除主键上的数据,而不会删除该条索引记录,最终导致主键上和索引上的数据不统一。
单机零碎个别不会呈现上述问题。假如将两个节点设想成两个线程,比方 thread1、thread2,线程 1 想要进行表的原数据批改,能够获取一个的元数据锁,将所有的申请先挡住,再到内存中的表构造。能够看出单机零碎依附 mutex 能够实现多线程互斥,不存在两个线程应用不同版本的 t1 的状况。
一个简略的想法是将单机零碎中的锁扩大成分布式锁。这种做法在原理上可行,但会存在时耗不可控的问题。以下图为例,假如 sqlengine1 想发动申请锁的申请,它能够在本身节点申请,也能够在其余节点如 sqlengine2、sqlengine3 上申请。但因为分布式系统中网络不太可控,sqlengine 数量十分多,可能会存在网络异样问题,比方 sqlengine3 存在网络异样,回复工夫就会比较慢。网络工夫的提早导致不可控问题。如果等到所有节点都申请胜利,再去做更改,用户申请的阻塞工夫就会被拉长。
分布式锁的实现还有很多计划,比方引入超时机制,但同样也会存在其余问题,例如超时工夫定义为多长?太长对用户业务会有影响,太短则可能存在误判。咱们进一步思考,是否不依赖分布式锁达到同样的目标。
咱们采纳 GoogleF1 论文中引入的过渡态的思维。前述问题呈现的起因是有的计算节点无奈感知到该索引,有的计算节点感知到该索引并去写索引,这就产生了数据不统一问题。F1 的根本思维是在分布式系统中,在没有锁的状况下,无奈同时从某个状态迁徙到下一个状态,这时就能够引入中间状态。比方某个节点能够先进入到下一个状态,但该状态与上一个状态互相兼容。如图所示,假如目前为 v1 状态,先进入 v2,但 v2 与 v1 能够兼容,相当于还有局部节点处于 v1 状态,两者能够并存一段时间,等所有节点都进入 v2 后,再进入 v3,状态两两兼容,最终推动到残缺的过程。但如何保障两两之间不超过两个状态也成为了一个新的问题?假如有个节点 1 先进入到 v2,节点 2 在 v1,过段时间后节点 1 想进入 v3,但要如何确定是否所有节点都进入 v2 呢?
F1 中还提到 lease 机制。假如 sqlengine 是一个执行 DDL 的节点,如果想进入下一个状态,就须要等 2t 的工夫。所有 sqlengine 节点,每隔一个 t 周期,都会看本人的 scheme 是否过期,如果过期就会从新加载,通过 2t 和 t 的穿插,保障推动时其余节点必然将新 scheme 退出进来。如果局部节点加载不上来出现异常,就会被动下线。但如果单纯的 lease 还是不牢靠的。比方在下图中比方,节点 1 距离 2t 工夫进入 v2,再距离 2t 进入 v3。假如节点 2 在 v1 时进行 put key 操作,但该申请在存储层面执行的工夫较久,刚好遇到了 io 100%,阻塞工夫较长,比方阻塞 5T 的工夫才把申请写下去。这时存在一个节点,在距离 2t 后误以为其余节点都曾经进入新状态,因而进入到 v3。这就违反前述规定,即同一时刻不能有两个相邻版本以外的写入并存。即便 v2 晓得本身超过 lease 抉择被动下线也没有用,因为写入申请曾经发到存储层,该写入的生命周期曾经由存储层来管制。对于上述问题,F1 中也提到能够引入 deadline 工夫来管制,然而目前咱们并没有这种机制,而是采纳了一种版本断定机制来解决这个问题。
从实质上来看,这个问题属于计算层与存储层联动的问题,因为该申请曾经发到 TDStore,咱们须要在推动版本前让 TDStore 感知到相干状况,具体流程如下:在进入下一状态前,须要先推一个版本上来。推下去后,存储层会感知到该节点想要进入 v2。与此同时,存储层发现 v1 状态下还有一个申请未实现,等该申请写完后存储层再返回批准。如果存储层中一旦存在旧版本申请没有实现,它会等到实现后再反馈。
在这种束缚机制下,只有 push 版本胜利,阐明存储层里曾经没有比 v2 更小的写入,即此时任意节点都没有过期版本正在写入,能够进入 v3 状态。同时在该机制下,存储层不会承受后续申请中比 v 小的读写申请。在极其异样的场景中,假如某一节点在 push 曾经胜利的状况下,发送仍处于 v1 状态的申请,这时存储层就会发现该申请比以后版本的 v 要小,只能回绝。通过存储层的版本校验机制,进一步保障了零碎中任意时刻的无效写入只能在两个相邻的状态之间。
最初对缓存与执行进行总结。咱们采纳 F1 的思维引入过渡态,将 Add Index 分成多个阶段,每相邻的两个阶段两两兼容,这样就无需依赖全局的分布式锁。在存储层进行该版本的有效性测验,进一步保障每时每刻的无效写入只能位于两个相邻状态之间。大多数状况下,咱们能够认为该版本测验有效。因为每个节点都能加载新的表构造,且能用新的表构造进行读写,版本测验仅实用于预防阶段场景,为避免此类极其场景对数据造成一致性的毁坏,保障整体算法运行的正确性。整体过程为:由计算层间接向下推送版本,演变为先向 TDStore push 以后版本,再进入下一状态,通过此类形式来实现整体的变更操作。
删索引则绝对容易,能够看成加索引的反向操作,具体过程如下图。
通用 Online DDL
在 Instant DDL 中,仅需更改表构造、批改元数据即可。在 Varchar 扩大长度等无损数据类型的转换中,还须要生成局部数据能力实现。要如何使得更宽泛的其余 DDL 通过 Online 形式执行,这就成了新的挑战。
为此咱们联合了 pt-online-scheme-change 的思维。pt 的原理为:在执行 OnlineDDL 时,会生成一个新的表构造即长期表,再将旧表数据拷贝到新表中,过程中还会进行建触发器等操作,保障拷表过程中的增量同步。在 TDSQL 新敏态引擎的设计中咱们借鉴了上述拷表思维。拷表过程中的新表的过程能够设想成在原表上加一个非凡的索引,即回归到托马斯写问题,针对拷表过程中的问题咱们也设计了过渡态问题的解决方案。
旧表为 status0,建设一张长期表为 tmp1,状态为 delete only。咱们会在外部建设一张新表,将旧表与新表进行关联,并且会将表 status0 上的删除相干的操作同步长期表 tmp1,接下来进入 write only 状态。write only 的过程与加索引过程雷同,会在执行过程中将 delete、update、insert 等新的增量同步到 tmp1 上。
筹备开始 thoma write 回填数据之前,须要在存储层推版本,确保以后没有处于 delete only 状态的节点,保障任何新的申请都会增量同步到新的长期表中。之后再进行 thomas write 操作依照加索引的形式,从 MC 获取工夫戳,再用工夫戳扫数据,从老表上将旧数据回迁到新表,thomas write 机制能够保障整体回迁过程与原表事务并发的正确性,最初再进行长期表命名。
在此之前,咱们还会进行其余的查看操作,比方查看旧表与新表数据的一致性。因为在这种拷表形式中,如果 alter 影响到主键,就容易引起数据方面的问题。假如原表的主键为一个 Varchar,属于大小写敏感类型,下面有 A 和 a 两条数据。如果变更字符序,将其变为大小写不敏感,在新表中 A 和 a 就会变成一条数据,从而笼罩掉原始数据。咱们须要通过相似的二次查看来确定是否存在该种状况,防止拷贝过程中的数据遗失。
查看实现后,咱们会进行 rename 操作,更改旧表表名,再将新表替换成原表表名,相当于将整个原表替换到新表的状态。咱们还会进行反向同步操作,因为可能有局部节点仍处于 status2,此时原表上还有读申请,咱们须要将这些申请转发到这张表上,保障处于该状态的计算节点仍能读到这些新增的数据申请。在这些申请转移实现后,再勾销关联,将版本推掉,最终将旧表用异步形式进行清理。
联合 pt-online-scheme-change 的思维,咱们将拷表的过程设想成增加一个非凡的索引,从而进一步推广到反对 MySQL 所有类型的 DDL。
Online DDL 原子性
在 TDSQL 新敏态引擎中,所有计算节点为无状态,长久化操作通过存储层来实现,DDL 的发动操作则在计算节点中进行。如果某一计算节点在执行 DDL 过程中挂掉,就会面临中间状态由谁来负责推动的问题。
实际操作中,每个计算节点在执行前,会在存储层长久化一个 DDL 工作队列。每次发展 DDL 工作时,就会将该 DDL 工作插入到 DDL 工作队列中。如果失常完结,就会将该工作删除。如果非正常完结如异步挂掉,其余的计算节点,会感知到工作队列中有未实现的工作,依据该工作以后执行信息,再去界定该 DDL 工作的下一步操作,例如持续推动或回滚。
在上述过程中,复原线程与工作线程之间通过 MC 的 lock 来互斥。这看似引入了分布式锁,但实际上该锁只作用于 DDL 之间。因为 TDSQL 新敏态引擎的整体设计准则是 DML 优先,在 DDL 过程中尽量避免影响 DML。
总结
综上所述,TDSQL 新敏态引擎 Online DDL 核心技术能够总结为四个方面:
● Instant DDL:通过多版本的解析规定,使得加列或 varchar 扩大长度等无损类型变更这些只需批改元数据的 DDL 霎时实现。
● Add/Drop Index:通过托马斯写机制,解决生成索引数据和用户事务的并发问题;采纳 F1 过渡态 + 存储层版本交验机制,解决多个节点间缓存一致性问题。
● 通用 Online DDL:形象出实用于所有 DDL 的 copy table 流程,进一步将 Online DDL 推广到可反对绝大多数 MySQL 的 DDL。
● DDL 原子性:通过工作队列 + 复原线程的工作机制,保障 DDL 整体的原子性。