进阶数据库-分布式的在线Schema变更
最近在学习存储的知识, 在做pingcap 的 talent-plan 的 Proj3, 发现这套参考 google f3 实现的 在线Schema变更 机制很意思, 写文记录一下.
问题背景
goole 的 F1 是一个如下的架构 进阶数据库-分布式的在线Schema变更

F1 servers 对 kv 进行了提供结构化查询抽象, 但是所有数据都落在了一个可以看做一个实例的分布式kv 存储.
在这样的分布式存储系统中, 不停机 的变更数据表结构是一件困难的事情. 原因是是分布式系统中不同节点会在不同时间完成ddl 变更, 而造成不一致.
以删除索引X为例子, 我们希望的
| 指令起始 | 指令结束 |
|---|---|
| 所有的数据都有X的索引 | 所有的数据都没有X的索引 |
然而, 由于分布式系统的特性, 我们假设:
A 节点处于 指令起始, 还存在索引 X
B 节点处于 指令结束, 已经没有了索引 X
这时 B 节点插入了一条新数据D, B认为 索引X 失效了, 就没有更新对应的 X 索引.
而D 的查询请求到了A节点, A节点认为索引还在, 就用这个索引去查询. 这时候就造成了不一致.
原因分析
造成这种不一致的原因, 就是 状态不兼容.
A节点处于的状态 和 B 节点处于的状态直接缺少兼容.
类似的例子其实在业务开发的过程中也很常见, 比如 迁移存储. 一般来说一个业务系统想要更换存储模型, 是不能直接发一个读写新存储的版本上去, 因为在变更的过程中, 会存在有的节点只写了新存储 而 旧版本的节点读不到新存储的情况. 而其他节点写入的数据新版本的节点也读不到.
所以一般我们会采用双写的方案:
先发一个版本, 让所有节点都写新数据, 但不读.
发一个版本, 开始逐渐的读新版本,并把读旧的版本的逻辑下掉.
这种方案在业务开发中很常见, 其核心思想就是:
在不兼容状态中, 添加一个兼容的状态
google的 F3 也采用了类似的方案
解决方案
在 f3 中, 他们添加了两个兼容状态:
delete only: 处于该状态下, 结构只对删除操作可见. 例如索引可以因为数据变更被删除, 但是新插入数据不会添加新的索引了, 而查询数据也不能使用该索引
write only: 处于该状态下, 结构不可以读, 但要可以正常的写和更新. 例如索引可以新增,修改,删除, 但是查询的时候就不能使用该索引了.
除了这两个兼容状态, f3 还有初始, 完成, 执行三个普通状态:
absent: 初始状态, 可以认为该阶段没有任何指令生效
reorg: 执行状态, 可以认为这个状态在执行对历史数据的 ddl 变更,
public: 变更完成
添加了这两个状态后, f3 就得到的以下的状态机图
删除结构
absent --> write only --> delete only --(reorg)--> public
新增结构
absent --> delete only --> write only --(reorg)--> public
有小伙伴就会问了, 为什么没有 更新结构? 这里更新结构可以用 先删除, 在新增来解决, 所以理解这两个操作就行了.
删除结构
我们先看删除结构. 类似我们做业务搞双写一样, 双写是为了在有业务使用数据的前提下数据是正常维护的. 这里是我们希望在 还有节点依赖该索引状态下, 索引是在正常维护的.
而 write only 就可以保证, 在等待所有节点不在读取该索引之前, 索引的数据是正常维护的.
进入 write only 过后, 这个索引已经没有人读取了, 可以进行删除操作了, 然而这时还不能直接对历史记录进行全量的删除, 因为还有节点还在 write only 下维护索引, 无法获得准确的存量数据, 所以要增加一个状态 delete only. 等所有节点都不在维护索引时, 完全删除当前的存量数据, 完成变更
增加结构
增加结构的思路是类似的, 要保证结构在维护正确之前, 没有节点使用它.
所以先进入 delete only, 这时不会有任何节点使用读取他.
这时进入 write only, 即使有节点在处于 delete only 阶段下, 也不会有多余的索引存在. 只是还有该新增的索引没有新增. 当所有节点进入 write only 时, 对快照存量数据做新增索引, 完成变更即可.
分析
上面让我们读 ddl 变更有一个简单的认知, 下面我们从更完备的角度分析一下这个算法.
首先我们要分析一下, 我们要预防的不一致是那些情况. f3 给出了达到一致的条件:
1 所有的数据, 都归属于明确的列 2 所有的 列, 不能有未定义的数据 3 没有已经没有了对应数据的索引, 即没有失效索引 4 索引的数据是完备的, 即该被索引的数据要能索引到 5 索引索引的列是合法的 6 所有的约束都是满足的, 没有未定义值(可以认为是对前面的概况)
然后我们在回过头看"write only" 和 "delete only"这两个状态,
删除结构的过程中:
对索引的变更:
write only: 保证4, 数据都可以被索引到
delete only: 保证 3 没有失效索引
对列的变更:
write only: 保证2和 5, 用到列的时候列是合法的
delete only: 保证了 1, 数据被删除干净
新增结构的时候同理
优化
对于一些特殊情况, 可以无需这么复杂的兼容状态, 例如新增一个可以为空的列. 由于数据可以为空, 所以无需有 write only 的状态, 也不需要对历史数据做变更, 可以直接从 delete only 到 public. 因为 不会有不兼容的状态.
absent --> delete-only --> public
这里可以自己分析一下
参考资料
[1].https://github.com/ngaut/builddatabase/blob/master/f1/schema-change.md
[2].http://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/41376.pdf
[3]. https://zhuanlan.zhihu.com/p/120719499




