CockroachDB|CockroachDB 如何实现在线Schema变更


概述 传统的关系型数据库在执行Schema变更的时候,首先需要锁表,锁表期间任何读写操作都会阻塞,直到Schema变更结束,释放表锁.遮掩做的好处是显然易见的,那就是处理起来简单,不过弊端也很明显,那就是其他的DML操作都会阻塞,影响正常的业务.对于一个可以支持EB级数据存储和处理的分布式数据库来说,这个方案显然不合适.
在线Schema变更的难点在于由于网络的原因,我们无法保证集群中所有的节点在同一时刻接收到新的Schema. 这样无论什么样变更方案,总是存在一个时间段,集群中既有新的Schema又有原来的Schema,那么如何保证数据的可用性和一致性就是一个非常苦难的事情,另外在变更完成后数据回填时,也会因为数据规模巨大而需要耗费大量的时间.在数据回填结束之前,读请求的结果会被变更的Schema干扰(从一致性的角度讲,这个是不允许发生的).
事实上在Google发表论文《Online, Asynchronous SchemaChange in F1》宣布它旗下的著名数据库产品F1实现了在线异步Schema变更之前,人们还没有找到解决这个难题的办法.CockroachDB在线Schema变更就是参考这篇论文实现的.
Google F1的探索实践 跨国公司的难题
有一家跨国公司, 它的员工遍布全球各地,公司内部通过email进行通信,某一天公司管理层决定统一采用QQ作为通信工具.那么问题就来了,公司全球各地的员工不能保证可以在同一时刻都切换到QQ,一方面大家的时间不能绝对完全一致,另一方面不能保证不同地区的员工都可以获知通信工具的变更.于是就会存在一个时间段,有的员工已经在使用QQ了,有的员工还在使用email,这会造成消息丢失.
从上面的描述中我们知道总是存在一个时间段,公司内部部分员工使用QQ,部分员工使用email,但是我们也知道经过一个足够长的时间,公司所有的员工最终都会使用QQ.足够长的时间因为无法度量,因此在现实层面没有太大的价值,我们需要把足够长变成确定长的时间,这样我们就能知道从发布更换通信工具之后多久,所有员工都已经使用新的通信工具了.
办法也很简单,我们规定所有的员工每隔半小时从公司的官网上查询一次通告,如果查询失败,十分钟之后仍然查询失败,那么终止使用当前的通信工具直到查询成功为止.因此如果我们在12:00发布通信工具变更通告,那么半个小时之后绝大部分员工都接收到了变更通知,切换到QQ,可能还有少量员工没有,我们再等20分钟,那么50分钟之后,我们就可以确定所有的员工的通信方式时QQ了.
【CockroachDB|CockroachDB 如何实现在线Schema变更】到此为止,我们仍然没有解决问题,但是如果我们的通信方式在从email变更成QQ之后,我们再次变更通信方式为微信,仍然采用上述办法,那么50分钟之后所有的员工都已经在使用微信了.我们发现不存在这样一种场景,既有email又有微信.我们把QQ看作一个中间过渡状态,在过渡期间,允许两种通信工具同时存在.那么我们的问题就引刃而解了.F1就是采用这样的算法来解决在线异步Schema变更.
算法实现 租约
Table的schema一般会持久化存储, 大部分情况下我们会在网关节点(负责SQL解析)缓存一份schema,这样就不用每次都访问磁盘存储,一方面schema不一定存储在本地需要跨网络访问,另一方面磁盘读取速度太慢,如果每次请求都访问磁盘的话,那么性能会非常差.我们给缓存一个租约,通常是数分钟,缓存节点在租约到期前需要从磁盘上重新获取schema信息,如果续租失败,那么节点停止服务即可.当然即使是每次都访问磁盘仍然不能保证同一时刻所有的节点都拥有最新的schema.
租约保证了我们在确定时间内,集群中所有的节点都可以持有最新的schema信息.
中间状态
我们把schema变更拆解成若干个递进的中间状态,经过仔细的分析推演,我们无须为所有的schema变更设计不同的状态,我们只需要两个中间状态即可.即delete-only和write-only.
delete-only指schema的变更只对删除操作可见.对于update可以拆解为delete+insert,那么在delete-only状态时只执行delete,忽略insert.这里的delete和insert并不是针对整个SQL语句,而仅仅是针对schema变更部分的操作,比如给某一列添加一个索引,在delete-only状态下,client执行一个update语句修改一行数据,那么此时client并不能感知新的schema,因此在执行update操作时,涉及到对这个索引的操作时,只执行delete,丢弃后面的insert.
write-only指schema的变更只对写操作可见,包括insert, delete, update.
需要说明的对于schema中未发生变化的部分,任何操作都不会受到影响.
接下来我们通过模拟添加索引来推演整个schema变更过程.完整的变更过程如下:
Init ->delete-only -> write-only -> backfill (回填)-> public
Init状态时,集群中只有一个schema A,现在开始schema变更,我们给其中某一列增加一个索引,此时新发布的schema B的状态是delete-only.按照我们之前的描述,集群中部分节点接收到了新的schema B,部分节点仍然是老的schema A,那么此时无论是拥有Schema B的节点执行SQL请求还是拥有schema A的节点执行SQL请求,都不会产生这个新添加的索引的任何数据,因为delete-only状态只允许删除新的索引,不允许添加新的索引,而老的schema上没有这个新的索引,因此也不会产生新索引的数据.也不会因为删除而造成新的索引数据没有归属.
一段时间之后,集群所有节点的schema都变更成新的schemaB,但是状态仍然是delete-only,此时我们变更schemaB的状态为write-only,仍然是根据前面的结论,此时集群中部分节点schema的状态是delete-only,部分节点schema的状态是write-only,但是没有节点持有schema A了.在write-only状态下,可以产生新的索引数据,但是不能被读到.在这种混合状态下也不会产生数据不一致,因为delete-only状态下肯定会删除新的索引,write-only状态下即可以产生新的索引数据也可以删除这些索引数据.
一段时间之后,集群中所有节点的schema的状态都变成了write-only,这时我们开始回填原来的旧数据,补齐索引信息,这个过程也不会阻塞正常的服务,即使回填的数据和正常的修改冲突没能保证正确性,因为无论是回填还是正常的SQL都可以写新的索引.回填并不会修改原来的数据,仅仅变更变更的schema的数据.因此回填是安全的,不会造成数据不一致.
数据回填结束之后,schema就算正式完成了,schema的状态变成public,即开发完全的读写权限,新添加的索引可以读了.感兴趣的同学可以按照这个算法自行推演schema添加一列的状态变化.
CockroachDB在线Schema变更 CockroachDB参考Google F1在线异步Schema变更实现自己的在线schema变更.CockroachDB引入两个中间状态:
DELETE_ONLY状态: 变更的Schema只有删除权限, update语句拆解成delete+insert,只执行delete,丢弃insert.
WRITE_AND_DELETE_ONLY状态:变更的schema只有写权限,没有读权限.即变更只对insert, update, delete可见,对select不可见.
仍然以添加索引为例,CockroachDB的schema变更步骤如下:
第一步. 赋予删除权限,即状态DELETE_ONLY
第二步. 赋予写权限,即状态WRITE_AND_DELETE_ONLY
第三步. 回填索引数据
第四步. 赋予读权限,即状态PUBLIC
对于算法中提到的租约,CockroachDB引入了Table Lease机制.Table Lease分为Write Lease和Read Lease。
Read Lease:
对任一张表进行读写访问时,CockroachDB需要对该表的Schema申请一个Read Lease。申请Read Lease时,会向系统表system.lease中插入一条关于该表的Lease记录。
Write Lease:
执行Schema变更操作前,必须先获取Table的Write Lease。Write Lease确保针对同一张表同一时刻只能有一个schema变更任务运行。Write Lease会在schema变更任务执行过程中会不断被续租。
当Schema准备切换到下一个状态的时,Schema变更任务会检查前一个版本的Schema上的Read Lease,直到前一个版本的Schema上的Read Lease全部超时,这样CockroachDB就满足算法要求的集群中最多允许同时存在两个不同状态(这两个状态是递进的)的Schema。
正如算法中描述的那样,CockroachDB的节点上缓存了Schema,为了及时获得Schema变更,CockroachDB使用gossip协议在集群中广播Schema的变更.
在线schema变更通常是一个耗时较长的过程,不可避免的会出现各种异常导致中断,因此为了实现schema变更的高可用,schema变更会由Table lease holder节点负责执行,这个节点就是table的第一个range的leaseholder所在的节点.因此网关节点接收到一个DDL,会转发到table lease holder节点执行.如果table lease holder发生异常,那么新产生的table lease holder会重新加载变更任务,继续执行.如果变成schema失败,那么只要启动逆向回填任务就可以完成回滚.
在数据回填时,因为CockroachDB定位是支持EB级数据的存储,因此单表的数据量可能会非常大,因此回填也会转变成一个分布式任务,加快回填过程,多个节点并行执行回填,每个节点负责其中的一部分数据.数据回填过程为了尽量避免和正常业务发生事务冲突,会把回填过程拆分成很多小的事务去执行.
参考资料 http://disksing.com/understanding-f1-schema-change
http://www.cockroachchina.cn/?p=1162
https://segmentfault.com/a/1190000009707788?utm_source=tag-newest
https://github.com/ngaut/builddatabase/blob/master/f1/schema-change-implement.md
https://zhuanlan.zhihu.com/p/43088324
http://zimulala.github.io/2017/12/24/optimize/
https://blog.csdn.net/hnwyllmm/article/details/96429859
http://www.cockroachchina.cn/?p=1080
https://blog.csdn.net/solotzg/article/details/80976744

    推荐阅读