本章主要介绍数据复制的技术,问题及挑战,解决方案。

复制或多副本技术的意义:

  • 高可用:避免单点故障。
  • 低时延:将数据放置在靠近用户的地方,从而实现更快的交互。
  • 扩展性:采用多副本读取,提高系统的吞吐量。

主从复制(一主多从)

最主流的复制方案,许多关系数据库都内置支持主从复制,包括PostgreSQL(9.0版本之后)、MySQl、Oracle Data Guard和SQL Server的AlwaysOn Availability Groups,非关系数据库如MongoDB、RethinkDB和Espresso也支持主从复制。还有一些分布式队列如Kafka和RabbitMQ都支持。

同步复制和异步复制

同步复制的好处是一旦向用户确认,复制已经完成了,缺点是阻塞所有的写操作。异步复制就反过来了。

sync or async copy

不过,同步和异步并不是那么泾渭分明,还有一种半同步模式,它的做法是将其中一个副本设置为同步的,其余的为异步,这样至少有两个节点(主节点和这个节点)拥有最新的副本。

增加新节点

快照文件+修改日志的方式

节点失效

如果从节点失效,恢复后可以比对日志,追赶主节点。

如果主节点失效,需要选出一个新的主节点,这个过程称为切换

切换涉及的步骤:

  • 探活:一般基于超时的心跳机制。
  • 选举新的主节点。
  • 配置系统使新主节点生效。

典型问题,比如脑裂,是说有两个节点都认为自己是主节点的情况。超时时间如何设置,长的话,恢复时间比较长,短的话,会造成不必要的切换。

具体的复制技术

基于语句

比如SQL语句记录,但是会存在如下问题:

  • 非确定函数,比如NOW()RAND(),可能会在不同的副本上产生不同的值。
  • 使用了自增列,或者依赖现有数据,例如UPDATE … WHERE。
  • 有副作用的语句,比如触发器、存储过程、用户定义的函数等,可能会在不同的副本上产生不同的副作用。

基于预写日志(WAL)

比较偏底层,一个WAL包含了哪些磁盘块的哪些字节发生变化,诸如此类的细节。

基于行的逻辑日志

逻辑日志与具体存储引擎解耦,更有利于向后兼容性。

  • 行插入,日志包含所有列的新值。
  • 行删除,日志里有行的唯一标识,通常为主键,如果表没有主键,就包含所有列的旧值。- 行更新,日志里包含行的唯一标识,和列的所有新值。

MySQL的binlog(配置为行复制)就属于这一类。

复制滞后问题

使用最终一致性系统时,最好事先就思考这样的问题:如果复制延迟增加到几分钟甚至几小时,那么应用层的行为会是什么样子?如果答案是“没问题”,那没得说,否则就得考虑提供一个比最终一致性强的保证。

读自己的写

用户在写入不久即查看数据,新数据还未到达从节点。对用户来讲,看起来似乎刚才提交的数据丢失了。对于这种情况,需要“写后读一致性”。

read after write

典型方案:

  • 用户访问可能会修改的内容,从主节点读取,否则,从从节点读取。比如,社交网络的个人主页从主节点读,他人主页从从节点读。
  • 跟踪最新的修改时间,如果是一分钟之内,从主节点读取。
  • 如果副本分布在多数据中心,需要先把请求路由到主节点所在的数据中心。

如果允许用户多设备登录,还需要考虑:

  • 更新时间戳需要做到全局共享。
  • 多数据中心部署的应用,还要将多设备的请求都路由到主节点所在的数据中心。

单调读

用户看到了新内容后,又读到了过期的内容,好像时间回拨,此时需要单调一致性。这是一个比强一致性弱,又比最终一致性强的保证。

monotonic read

实现方式之一是,确保用户总是固定的从某个节点读取,比如对用户id进行哈希。

前缀一致读

必须先看到“问题”,再看到“回答”,而不能反过来,这种保证叫做前缀一致读。

consistent prefix read

一个解决方案,确保任何具有因果顺序关系的写入都交给一个分区来完成,但该方案真实实现效率会大打折扣。

基于触发器

可以配置自定义代码在某种事件下。

灵活性最高,性能最差。

多主节点复制

主从复制有主节点单点故障的缺陷,因此自然有人想到设置多个主节点,每个主节点都可以接受写入,被写入的主节点再将数据同步给其他主节点。

使用场景:

  • 多数据中心
  • 离线客户端操作,比如日历软件,一个设备就相当于一个数据中心。
  • 协作编辑,每个正在编辑用户都是一个数据中心。

处理写冲突

多主的一大问题是写冲突

configs in multiple master

一种方法是避免冲突,比如将修改的请求都路由到特定的数据中心。

第二种方法是使状态收敛于一致的状态。具体实现方法:

  • 给每个写入都分配一个唯一的ID,比如时间戳、一个比较长的随机数,挑选最高的ID获胜,这种技术被称为最后写入者获胜(last write wins, LWW)。
  • 以某种方式合并值。比如上文中,合并的标题类似于“B/C”。
  • 利用预定义的格式保存所有值,提示用户解决冲突,类似于git,和cf。

拓扑结构

环形、星形、全部-至-全部。

multi-leader topology

环形、星形有中间节点单点故障问题,全部-至-全部由于每个主节点的延时不同,有类似于前缀读的问题。

arrive wrong order in multi-leader

对于多主节点复制,某些副本上可能会出现错误的写请求到达顺序,可以使用版本向量解决。

无主节点复制

放弃主节点,允许任何副本直接接受来自客户端的写请求。一些实现是客户端直接将请求发送给多副本,另一些实现,由一个协调者代表客户端写入。

代表产品:Dynamo、Riak、Cassandra、Voldemort,因为Dynamo后面几种都是受Dynamo启发设计的,所以称之为Dynamo风格数据库

失效节点重新上线后的两种处理机制:

  • 读修复。客户端读取多个副本时,检测到掉线节点版本较旧,就用新值更新。这种方法适合频繁读取的场景。
  • 反熵。后台进程不断查找副本之间的差异,将缺少的数据从一个副本复制到另一个副本。

read repair

读写quorum

如果有n个副本,写入需要w个节点确认,读取需要r个节点确认,w + r > n,读取的节点中一定包含最新值。

当一个集群的节点大部分失效,已经无法满足仲裁的需求,这个时候面临着:

  • 直接向用户报错。
  • 将请求导向一些临时节点,等恢复后再将数据回传回来,这些节点不属于原来的n个节点。

后一种措施就是宽松的(sloppy)quorum

所有的Dynamo风格的数据库都支持sloppy quorum。Riak默认启用,Cassandra和Voldemort默认关闭。

dynamo wrong order

检测并发写

一种实现最终收敛的方法是最后写入者获胜LWW。但LWW会导致数据丢失问题。

在分布式环境中并发的含义是两个操作没有依赖关系,比如happen-before。基于此,两个操作A和B只有三种关系,A在B之前发生,B在A之前发生,A和B并发。依赖关系可以覆盖,并发不能覆盖。

对于不能覆盖的情况,需要合并值。方法是一种基于版本向量的算法。

version vector

以两个客户端并行操作购物车为例,它的算法运行步骤如上图所示:

  1. 客户端A首先添加了milk,服务器返回(version: 1, value: [milk])。
  2. 客户端B随后添加了eggs,服务器返回(version: 2, value: [eggs])和(version: 1, value: [milk]),客户端将值进行合并,并选择所有值最高的版本号,(version: 2, value: [eggs, milk])。
  3. 接着客户端A又添加了flour,注意他是在version1的基础上添加的,因此他发给服务器的是(version: 1, value: [milk, flour]),服务器意识到version1的值得到了更新,所以覆盖老的值,最终返回(version: 3, value: [milk, flour])和(version: 2, value: [eggs]),客户端A收到值进行合并(version: 3, value: [milk, flour, eggs])。
  4. 接着客户端B也添加了新的物品ham,和步骤3类似,他是在version2的基础上添加的,也就是发给服务器的是(version: 2, value: [eggs, milk, ham]),服务器更新version2的值,最终返回(version: 4, value: [eggs, milk, ham])和(version: 3, value: [milk, flour]),客户端B合并值,得到(version: 4, value: [eggs, milk, ham, flour])。
  5. 这时客户端A又添加了bacon,也就是在version3的基础上加上bacon——(version: 3, value: [milk, flour, eggs, bacon],服务器覆盖version3的值,返回(version: 5, value: [milk, flour, eggs, bacon])和(version: 4, value: [eggs, milk, ham],客户端A最终得到(version: 5, value: [milk, flour, eggs, bacon, ham])。

上面示例的因果关系如下所示。

relations between evens