暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

CockroachDB如何实现在线Schema变更

CockroachDB 2021-07-14
665

传统数据库做Schema变更的时候,第一件事就是锁表,直到Schema变更操作完成。这么做的好处是实现简单,不需要考虑事务并发带来的影响(例如数据的一致性保证,系统性能的抖动等);带来的问题也比较明显,所有涉及到该表的事务都被阻塞。但是对CockroachDB这样一个支持EB级的分式数据库来说,长时间锁表是不可接受的。CockroachDB参考Google的《Online, Asynchronous Schema Change in F1》,实现了在线、异步的Schema变更。


在线Schema变更需要在Schema变更过程中,保持数据的可用,保证数据的一致性,并最大限度减小对系统性能的影响。对分布式系统来说,最大的难点在于Schema变更无法同步,也就是说不同的节点会在不同的时间点切换到新的Schema。其次,保证不同节点缓存的Schema信息一致性非常困难。同时分布式数据库的表数据量都非常大,回填数据耗时也是一个难题。

Schema在线变更实现方法


CockroachDB通过维护多个版本的表结构信息,并允许在旧版本结构信息仍在使用的情况下生成新版本的表结构信息,同时在不加锁的情况下异步回填(或删除)表数据。而在变更过程中(生成新版本的表结构信息过程中),CockroachDB和Google F1采用同样的Schema变更思想,在变更过程中引入多个中间状态,把变更过程由传统的一次性变更划分成多个相互兼容的步骤来完成。


例如在创建一个新索引时,CockroachDB会在变更过程中(生成新版本表结构信息过程中),逐步开放DML语句针对该索引的删除和读写权限。

因此,创建一个索引需要遵循以下步骤:

  • 赋予删除权限

  • 赋予写权限

  • 回填索引数据

  • 赋予读权限


同样,删除一个索引时则反向执行相应步骤:

  • 吊销读权限

  • 吊销写权限

  • 清除索引数据

  • 吊销删除权限


为了保证正确性(每个节点缓存的Schema信息一致),每一种权限的赋予或删除开始操作后,只有当整个集群都已经完成该权限的操作变更且权限已经生效,才能赋予或删除下一个权限。


由上图可以看到,在Add a structural element的变更操作中,在完成操作前,需要Backfill element(回填变更之前已经存在的数据)。CockroachDB通过启动事务来回填数据,但是会写入大量的write intent,被其他事务Abort的话,restart的代价太高。因此CockroachDB通过把回填操作拆分成一系列的小事务,降低回填数据失败的代价。回填操作从Primary Index的第一条记录开始扫描,回填过程中如果发现重复的数据,直接覆盖写。同时回填操作是以事务的方式回填数据,避免了其他事务正在执行的更新、删除操作导致的数据不一致问题。

Schema在线变更中间状态

  • DELETE_ONLY :UPDATEDELETE 操作只允许删除Index项或者Value,读操作无法访问相应的Index或者列。

  • WRITE_AND_DELETE_ONLYINSERTDELETEUPDATE都允许访问相应的Index或者列,读操作无法访问相应的Index或者列。

  • PUBLIC:所有操作都可见(即最后赋予读权限)。


DELETE_ONLY状态:避免虚假索引条目

删除权限的授予通过将索引设置成DELETE_ONLY状态实现。在该状态下,DELETE命令能完全作用于该数据行和所涉及的索引条目,UPDATE命令可以删除旧的索引条目,但不写入新的索引条目。INSERT和SELECT会直接忽略,跳过该新索引。


WRITE_AND_DELETE_ONLY状态:避免丢失索引条目

写权限的授予通过将索引设置成WRITE_AND_DELETE_ONLY状态实现,此时拥有针对该索引的删除和写入权限。INSERT,UPDATE和DELETE命令都可以正常运行,并按需添加或删除索引条目,SELECT命令需要读取权限因此会忽略该索引。

索引回填仅在写权限在整个集群生效之后才会执行。且在回填过程中,任何节点上接收到INSERT命令创建的新行都将直接带有合法索引条目,不需要依赖索引回填过程来创建索引条目。

 

PUBLIC:最终状态

在此状态下会授予读权限,此时所有命令都能完全使用上索引。


那么Schema变更任务如何保证Schema切换到下一个状态的过程是安全的?针对这个问题CockroachDB引入了Table Lease机制。Table Lease分为Write LeaseRead Lease


Read Lease

对任一张表进行读写访问时,CockroachDB需要对该表的Schema申请一个Read Lease。申请Read Lease时,会往系统表system.lease中插入一条关于该表的Lease记录,该记录包含Lease超时时间、TableIDTableDescriptor的版本号以及节点ID

 

Write Lease

执行Schema变更操作前,必须先获取TableWrite LeaseWrite Lease确保针对同一张表同一时刻只能有一个Online Schema Change任务执行(也就是说TableWrite Lease同一时刻只能被一个任务持有)Write Lease的超时时间在Online Schema Change任务执行过程中会不断被延长。


Schema准备切换到下一个状态的时候,Schema变更任务会检查前一个版本的Schema上的Read Lease,直到前一个版本的Schema上的Read Lease全部超时(也就是说CockroachDB只允许一个Schema最多同时存在两个版本)


Schema元数据同步

CockroachDB中,表的Schema信息由一个名为TableDescriptor的内存结构来描述。为了加速对TableDescriptor的访问,尽量减少访问KV存储的次数,每个节点都会Cache访问过的TableDescriptor。而为了保证每次修改完表结构后,TableDescriptor的信息变更能及时同步到整个集群,CockroachDB使用Gossip协议传播TableDescriptor的变化情况。


对应Schema变更的中间状态,每一次状态更改,TableDescriptor都会变更且传播到集群的所有的节点。


例如一个添加索引的过程如下:

  • 添加索引到TableDescriptor上,但是该TableDescriptor的索引被标记为DELETE ONLY

  • 等待TableDescriptor的变更被广播到所有的节点上,而且在老版本的TableDescriptor上的操作已经结束

  • TableDescriptor的索引标记成WRITE AND DELETE     ONLY

  • 等待TableDescriptor的变更被广播到所有的节点上,而且在老版本的TableDescriptor上的操作已经结束

  • 回填索引

  • TableDescriptor的索引标记成PUBLIC

  • 等待TableDescriptor的变更被广播到所有的节点上,而且在老版本的TableDescriptor上的操作已经结束


Schema在线变更异常

Schema变更是一个长时间执行的过程,执行过程中不可避免会出现一些异常情况,例如:CREATE INDEX执行过程中执行节点挂掉。CockroachDBSchema变更为了让变更操作可重做和续作,变更操作并不会在Gateway节点执行,而是把SQL语句转发到持有该表的第一个rangerange lease的节点(该节点称为“Table lease holder”)执行。例如,客户端连接到Gateway节点Node1T1表上添加索引,执行CREATE INDEX;而T1表的第一个rangerange lease持有者为Node3,那么Node1会把CREATE INDEX语句转发给Node3来执行。当 Lease Holder节点挂掉重启之后,会重新加载Schema元数据TableDescriptor,重新执行Schema变更操作或者回滚所有的Schema变更。


CockroachDB通过上述方法实现了在线表变更操作,在操作过程中不会和平常数据库一样产生锁表,因此不会对读写产生阻塞,其他表变更操作类似。这对于普通用户和DBA都是一个很好的体验。




 关于我们:我们是百度DBA团队,团队有两位CockroachDB PMC Member及一位Contributor, 目前正积极推动NewSQL在百度内部以及外部的发展。除了NewSQL, 我们在MySQL, PostgreSQL, GreenPlum有多年的内核开发经验及实践经验,对数据库和大数据领域有疑问或者需求欢迎联系我们,同时欢迎有志青年加入我们!



关注我们 


 





文章转载自CockroachDB,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论