我经常需要更改表结构,其中主要是为表添加列。当然这用alter命令很容易就能实现,但是,目前我的表已经达到40,000,000行之多且还在不断地增长,这使得执行alter命令往往需要个把小时。因为我用的是亚马逊的RDS服务,所以我不能用主从的策略来实现。因此我在思考能否做到在最小化的宕机时间条件下做到这一点。当然我也不介意有用户愿意花费几小时甚至几天的时间来进行这样的操作……
——— 摘自 Stack Overflow 2010年8月26日 serverfault
以上这条提问是在2010年发布的,但是关于进行这种操作问题的焦虑至今却依然存在。
我们在设计CockroachDB表结构更改引擎的时候总想把它做得更好以提供一个简单的修改表结构的方式(比如只要运行ALTER TABLE命令就可以)使得应用不需要承担宕机带来的任何负面后果。我们也希望更改表结构是CockroachDB的一项内置功能而不需要任何外界工具、资源或特定的操作步骤来支持且对应用程序的读写没有任何影响。
我将用以下的篇幅来解释我们在线更改表结构方案的机制并讨论在不宕机的前提下对结构元素(如列和索引)的更改。
我们做了什么
更改表结构通常涉及到更改结构本身和随着结构的更改而增加或删除的数据关系,而分布式数据库的两个基本特性使得这一点实现起来并不那么容易:
- 高性能:为了优化数据库性能,表结构必须缓存跨库节点,而维护分布式缓存的一致性往往是非常困难的
- 大表:分布式数据库中的表往往非常巨大,与表结构更改相关的任何表数据的回填或删除都将花费一定时间,要在不禁用数据库访问的前提下实现这一点也非常困难
对于第一个问题,我们给出的解决方案包括使用版本化的结构,并允许在旧版本结构仍在使用的情况下发布新的结构和对大表支持在不加锁的情况下回填(或删除)表数据。这个解决方案由谷歌的F1团队在工作中总结得出。
创建安全结构
和表结构元素(可以是索引或列,但在本文的剩余部分中我们将着重关注索引)相关联的数据都可以通过SQL DML命令删除或读写(比如 INSERT, UPDATE, DELETE, SELECT)。CockroachDB使用的策略是建立新索引时逐个而非同时地对其赋予以上命令描述的删除和读写权限。
因此,建立一个新索引需遵循以下步骤:
- 赋予删除权限
- 赋予写权限
- 回填索引数据
- 赋予读权限
在新的结构中,新授予的权限将与旧版本中已赋予的权限一起被授予。为了保证正确性,一种新的权限在只有当整个集群都使用包含所有已赋予权限的结构时的情况下才能被赋予,因此,整个过程将在每一步执行前暂停并允许在下一项权限赋予前将已赋予的权限应用到整个集群。
删除索引时也同样需要遵循相应的步骤:
- 吊销读权限
- 吊销写权限
- 清除索引数据
- 吊销删除权限
同样,一种操作权限被吊销时也需确保整个集群对已赋予的权限进行了同样的操作。
删除操作权限:避免虚假索引条目
当某位置建立了DELETE_ONLY状态的索引时即赋予这种权限,具有这种权限的SQL DML命令具有自我约束的特性:
- DELETE:这种删除操作将完全作用于该索引所涉及到的行和基础索引数据
- UPDATE:会删除旧的索引条目,并限制自身不会写入新的索引条目
- INSERT 和 SELECT 会忽略索引
在下一阶段中对索引被赋予写权限的节点将信任整个集群使用索引的删除权限。也就是说,当节点收到一个INSERT命令需要为一行数据插入索引条目时,另一个只拥有删除权限的节点在收到针对相同行的DELETE命令时会准确地删除该行的索引,这个索引将避免因悬空索引而出错。
另外,当删除索引时,相关的索引数据也仅在写入权限被集群回收后才会被删除,并且也只有当集群拥有删除能力时才允许其进行安全的删除操作。
写入权限:避免丢失索引条目
当某位置建立了WRITE_AND_DELETE_ONLY状态的索引时随即赋予这种同时具有删除和写入的权限:
- INSERT,UPDATE和 DELETE 命令都正常运行,并按需添加或删除索引项
- SELECT 命令需要读取权限因而会忽略索引
索引回填仅在整个集群都可写入时才可以运行。在回填过程中,任何节点上接收到的 INSERT 命令都将创建一个带有合法索引项的新行,并且不依赖单独的回填过程来为该行创建索引项。如此一来,可以保证直整个过程都不会丢失索引项。
读权限
最后一个权限是通过索引激活来赋予的,并且可以被所有命令完全使用。
快速的表结构迭代
在表结构更改的每一个阶段中都将允许整个集群的表结构向最新版本看齐。一般的表结构缓存机制都使用5分钟的存活时间,这也导致在修改进程信任最新版本的表结构是独有的且操作能力被完全赋予或吊销之前被强制要求等待几分钟。在CockroachDB中,我们开发了表结构版本的租约来加速集群表结构更新到最新版本以加快表结构更新的进程。
当要对表进行SQL DML命令操作时,运行该命令的节点会获取一个具有有效时间(以分钟为单位)的读取租约。被更改了的表结构版本被激活的消息会被广播到整个集群以通知节点更新到新的版本并主动释放旧版本的租约,此时如果一些不健康的节点未能主动释放,迭代机制将等待租约的过期以及延期迭代。
租约机制通过遵循以下两个规则使得表结构更改策略更简单:
- 最新的结构版本才可以签发新租约
- 有效的租约只存在于最新的两个版本之内
有关表租约更详细的讨论请在我们的Github 库的文档中查看。
准确可靠的表结构迭代
表结构的更改由节点执行的相关SQL命令来引导完成,这个过程往往耗费较长时间,假如过程中节点宕机则需要重启整个过程。每个节点也都会运行一个能执行任何不完整的结构更改过程的协程,在更改结构过程运行前,这个协程会获取相关表的一个独有的写入租约,这也是唯一可以引导更改成功的许可证。
小结
在线表结构的更改操作在CockroachDB中非常容易实现并且安全、快速、可靠。改变是不可避免的,而现在你再也不必担心了!
在此感谢谷歌F1团队发布的在线表结构更改的类似实现,我们从中也获得了很多灵感。