数据复制
主从复制分为同步和异步分支。
同步复制:在同步操作执行完毕之前,操作一直都是阻塞的。这样已经确认同步就会保证主节点与从节点之间的数据一致性。但是缺点也同样明显,就是当节点(主/从)发生错误时(宕机,网络故障等),那么在恢复之前这些节点一直都是处于阻塞状态,不会继而处理其它请求。
异步复制:异步复制则是在开始同步操作时,只需要像客户端确认了写操作,就会返回处理其它的请求。这样节点即使挂掉了,主节点都能继续处理其它请求,这样吞吐率更好。但是伴随而来的是也就无法保证节点之间的数据同步性(一致性)。
所以有些同步措施会采用 ”半同步“ 机制:将其中一个从节点设置为同步复制,其它节点为异步复制,这样在同步节点变得不可用时,另外的异步节点就会选择一个上升为同步节点,这样就保证了至少两个节点拥有最新的数据副本。这样做的好处是,当主节点不可用时可以及时切换另一个同步从节点为主节点。
高可用复制过程设计:
- 在指定一个时间点对主节点的数据进行一致性快照,此时就确定了快照与系统的复制日志索引号相关联。
- 将快照复制到新的从节点
- 根据前面获取的日志索引号,从节点请求主节点,将快照之后所发生的数据更改一一进行 “补偿”,同步数据。这一过程被称为追赶。
高可用还要处理失败场景:
从节点失效:我们知道节点是随时可能发生崩溃或者其它故障的,我们可以根据前面的系统复制日志的索引号来查出节点故障前的最后一步执行到哪,然后接着进行 “追赶式恢复”。
主节点失效:当主节点发生崩溃错误失效时,这是我们可以通过选举将其中一个从节点恢复到主节点继续执行复制。这个过程被称为节点切换。
如何实现复制日志
第一种就是通过转发相同的操作语句(基于语句的复制)。主节点记录所执行的每个写请求并将该操作语句作为日志转发给从节点。以数据库作为例子,我们每个 INSERT/UPDATE/DLETE 语句都会转发给从节点。
这种方式很简单,但是限制的条件有很多,特别是带状态(副作用)的语句,如获取当前时间 NOW()
,不同的节点在同步操作产生的数据也会不一致。一般解决方案就是将计算后的一个确定的值转发给其它节点,但是却很难做到将一个带状态变化为一个无状态的。
第二种是通过预写日志操作。简而言之就是将所有操作预先记录在日志中,然后依依复现。
带来的新问题
主从复制肯定会带来 “滞后问题”。因为执行同步的时间不固定,特别是在主节点在写操作之后,从节点请求主节点复制数据同步完成之前,读请求过来就会导致数据不一致的情况。这是正常也是必然的,这是一个暂时的状态,但是我们如何将这种情况降至最低呢?
读自己的写
首先要确认一件事,我们以 Mysql 的主从复制场景举例。像我们做读写分离设置主从库时,一般将主库为写库,从库作为读库。当时我们读取数据在同步数据之后自然能看到数据的一致性。但是如果读取数据发生数据同步完成之前,那么就会产生令人错误的结果。所以这个时候我们需要 “写后读一执行”,这种机制表示用户查询的数据都是自己提交写的数据,能看到最新的数据。那么如何实现写后读一致性呢?
首先我们可以将数据提前分类,即分清楚那些数据是不需要实时性敏感的,把这种数据我们都用从库来读。而那种实时性非常敏感的,需要看到修改后的数据,我们则可以继续用主库读取数据。
还有一种方案,就是将数据 ”时间轴化“,将密集型的数据(如根据更新时间的大与小来决定)用主库来查询,如查询更新后的一分钟内用主库查询。但是这又带来了另一种(更新时间)负面状态信息,特别是在分布式下,不同的机器是无法知道另一机器的更新时间的,所以我们需要将这些信息共享化,如存放到 redis。
单调读
继续思考这种场景:在分布式场景中,假设有一个主节点,3 个从节点。现在主节点插入了一个数据,此时从节点1 数据复制完成,用户查询节点 1 能查到主节点插入请求的数据。而此时节点 2 和节点 3 的用户查询该数据却为空。特别是当写操作的主节点的用户与查询节点2 和 3 的用户都是同一个人,那问题就体现得更严重的。其解决方案就是读数据时,单调读保证:即确保每个用户都是从固定的统一副本读取的,当这个节点发生失效时,用户查询则就转角给其他工作的另一个节点。
前缀一致性读
数据读写一般都具有顺序性,而前缀一致性读就是保证了对于一系列按照某个顺序发生的请求,在读取的时候也是按照当时写入的顺序。
主从复制的缺点:系统只有一个主节点,而所有写入都要经过主节点。如果由于某种原因,主节点不可用(如于主节点之间的网络中断,主节点宕机等),那么主从复制方案就会影响所有的写入操作。
多主复制如何解决写冲突
- 首先要尽可能的避免发生冲突,这里的解决方案可以通过网络分区,就近的将不同区域的用户将流量调度到各自的数据中心。这样其结果就相当于一般的主从复制一样了。
- 如果无法避免的话就必须要解决冲突了。
- 我们可以通过给每个写入请求编号或唯一ID,选择编号大的作为成功写入请求,代价就是会带来数据丢失。
- 还可以以某种方式将多个请求合并起来,通过一定的顺序规则显示出来。
- 将冲突结果都保留下来,交给用户自己去处理。
检测并解决冲突在分布式系统中还有一种通过版本向量的方式,详情可参见:https://github.com/MarsonShine/MS.Microservice/blob/master/docs/patterns-of-distributed-systems/Version-Vector.md