Online DDL 是分布式数据库领域的重要基础,可以说与事务模型、共识协议一样重要。最近重读F1的Online Schema Change这篇 paper [1],也希望能用通俗易懂的方式帮助更多人理解它。因此有了这篇短文,实际上是个人的 Paper 阅读笔记。
理解这篇文章的原理的关键点: 每个状态能正确运行的前提条件是什么;每个状态的DML能保证什么。然后就能理解状态之间的兼容性:所谓不兼容,就是违背了某个状态的前提条件。
这里没有覆盖文章中所有章节,着重在于说明上述的关键点。如果想知道所有细节,还需要读原文。
Ch1 大致介绍了下问题的背景以及状态间兼容性的概念;Ch2 以添加一个二级索引为例,介绍 F1 的 Online DDL 的一些关键点和流程;Ch3 简述了删除一个二级索引的过程;Ch4 简略描述了下 Optional 对象的操作简化。
Online DDL这篇文章,针对分布式数据库中,如何在不破坏数据完整性、不禁止DML的前提下,低开销地完成DDL 的问题,提出了一种软着陆方法。这个方法不涉及任何大范围锁,能保证数据的完整性,且对应用性能影响小,可以认为是在飞行过程中完成了自身的软变身。
这篇文章对应的是 F1 的模型,即系统由分布式KV存储 +多个无状态的 Server 组成,这个模型也是现在分布式数据库的主流架构。Server是指SQL运算节点:可动态伸缩、状态无需持久化,Server之间独立,也没有强的成员状态管理。
F1 采取的 K/V 模型简述如下:
表示这行的Row KV;
;举个例子,考虑一个已经存在的表,如果要给这个表加一个二级索引。那么直观方法是:
上述过程实际上要禁止 DML,让所有的 Server 都暂停服务。但是对于 Google F1 这种大规模分布式数据库,基本没法做到。因为:
这里说的异步,是指多个 Server 间异步地做状态变更。假设一个表之前没有二级索引,现在新添加新的索引 E。Index E 创建前后的状态,可以理解为"0"和"1"两个状态。

考虑两个 Server S0 和 S1。S0 的 Schema 在 “0” 状态,S1 的Schema在"1"状态。假设按照下面步骤执行操作:
以及
;
,不会删除
。那么上面的
就成了一个孤儿 Index,破坏了数据的完整性。
上面的状态 "0" 和 "1" 实际上不兼容的,所以它们不能共存。所谓状态间兼容,是指两个或者多个 Server 分别按照Schema对象的不同状态并发执行DML,也不会破坏数据一致性。
以索引为例,可能破坏数据一致性的异象包括:
存在,对应的
却不存在,IndexKV成了垃圾数据;
存在,但是没有对应的
。导致用户查不到数据。不同的Schema 对象,有不同的异象,这里不一一列举。
1.4 OnlineDDL一文大致思路
文章的思路:把从“0”到“1”的突变,替换为一系列相互兼容的小状态变化,实现软着陆。
表、表的某个Column、二级索引等,都可以称为 Schema 对象。这里用新增二级索引 E为例,说明一下具体有哪些小状态。增加其他 Schema 对象类似。
2.1 四个状态概述
对于新增 Index 这种 Schema 变更,状态变更如下图所示。注意,Absent 和 Public 都是理论上可以长期存在的;Delete Only 的存在时间一般是一个 Lease 长度。而 Write Only 的存在时间,则是一个 Lease 长度 + Reorg 的时间;Reorg 的执行时间取决于既存数据的量(后面有说明 Reorg)。
DDL 只有一个执行者,它的状态按照下面的顺序变更。而其他 Server 会定期(按照Lease)刷新schema 状态,因此最多落后一步。在同一时刻,最多有两个共存的状态,二者之间必须能够兼容。

1. Absent 状态
2. Delete Only 状态
时,如果
存在,要一并删除;
时,不允许插入
;
时,只允许删除既存的
,但不能插入新的
。3. Write Only 状态
4. Public 状态
其中的 Delete Only 状态,看起来好像比较诡异,却是必须的。因为它能跟 Absent 兼容。
这几个状态,实际上都隐含了自己能运行的前提条件,以及自己在运行中能保证什么。
我们用下面的表来表示。其中:

Delete Only 本身没啥要求,只要满足Absent的前提条件即可。
2.3.2 为什么 "Delete Only" 和 "Write Only" 能够兼容?
2.3.3 为什么 "Absent" 和 "Write Only" 不兼容?
2.3.4 "Write Only" 能否与 "Public" 兼容?
如何使得 E 的 IndexKV完整呢?表里面可能有历史既存数据,它们并没有对应 IndexKV;而 Delete Only, Write Only 都没有解决历史数据问题。答案是下面的 Reorg。
仔细看下前面的状态变化图,在进入Write Only状态后,不是等待一个 Lease 就可以自然切换到 Public,而是在等待一个 Lease后,启动 Reorg,然后等待 Reorg 完成,才能切换到 Public。
对于 Add Index 这种操作,Reorg 就是为某个时间点(T3)前写的数据,补上 IndexKV。
所以,从T4(Reorg完成),所有的Index都是完整的,可以切换到 Public。此后Write Only 和 Public状态的Server共存没有问题。
2.4.2 为什么"Reorg" 与"Delete Only" 的 DML 不能并存?
在具体实现中,Reorg是按照快照时间戳干活的,所以需要底层KV能支持多版本。这里不展开了。
删除Schema对象与新增Schema对象相比,状态变更是反方向的,但是要注意执行 Reorg 的阶段不同。例如,删除一个二级索引:
这几个状态的前提条件不变,兼容性的原理也相同。例如:"Delete Only" 仍然与 "Absent" 相邻、相互兼容;"Public" 仍然与 "Write Only" 兼容。"Delete Only" 仍然不会与 "Public" 兼容。
如果新增的对象(比如新增一个 Column),它的值允许为Null,即它的值是可选的(Optional)的。那么它的 Public 状态不要求所有行都有该 Column 的数据。既然 Public 状态不要求数据完整,那么前提条件也就变成了"无所谓"。
一旦是无所谓,就简单多了。 Public状态 可以直接跟 "Delete Only" 兼容,可以省掉 "Write Only" 和 Reorg。
如果新增的一个Column不允许为Null,则不能省略中间状态或步骤。
在前面讨论为什么 "Absent" 和 "Write Only" 不兼容时,我们举了个例子,说明会产生孤儿IndexKV(异象)。那么,异象产生的本质是什么?
事实上,在DDL的状态变化中,"Delete Only", "Write Only"都是向前兼容的,它们对历史数据没有任何假设。
既然它们向前兼容,为什么在Add Index的例子中,"Absent" 和 "Write Only"共存,会产生异象呢?是因为后面提到的逆序修改。
还是以Add Index为例,考虑下面的场景(Server A状态领先,进入"Write Only" 状态; Server B还在 Absent 状态):
场景1:
1) Server B 按照 Absent 插入行x;
2) Server A 按照“Write Only" 修改行x。
场景2:
1) Server B 按照 Absent 插入行x;
2) Server A 按照“Write Only" 删除行x。
场景3:
1) Server A 按照 "Write Only" 插入行x;
2) Server B 按照 Absent 修改行x (可能正好修改了E索引的列,但是Server B不知道E)。
场景4:
1) Server A 按照 "Write Only" 插入行x;
2) Server B 按照 Absent 删除行x。
场景1 、2并没有产生问题;
场景3可能导致IndexKV(E, x)无效,因为Server B并没有去更新IndexKV(E, x);
场景4则会导致IndexKV(E, x)变成了孤儿。
所以,真正导致上述两个不兼容状态共存时异象的,是因为同一行先按照 "Write Only" 修改,后按照 Absent 修改,而Absent 状态对既存的历史数据是有要求的,所以产生了异象。
两个不兼容的状态共存时,只有出现不兼容状态间的逆序修改,才会产生异象。
参考文献:
[1] Ian Rae, Eric Rollins, Jeff Shute, Sukhdeep Sodhi and Radek Vingralek, Online, Asynchronous Schema Change in F1, VLDB 2013
原文作者:;如有侵权,请联系管理员删除。