在分布式系统中,数据通常需要被分布在多台机器上,主要为了达到:
这样看下来,分布式系统中为了保证整体架构中不因一台机器故障而导致整体瘫痪,需要对故障的机器中的数据进行转储保存,这时候就需要对数据进行复制
复制的目标是保证若干个副本上的数据是一致的,此处的“一致”概念不唯一,可能指不同副本的数据在任何时候都严格保持完全一致,也可能指不同客户端不同时刻访问到的数据一致,这里一致性的强弱也可能不同,可能是不同客户端同一时间访问到的数据相同,也有可能是一段时间访问的数据不同,但这段时间后访问的数据却相同
为什么会出现这么多一致性强弱不同的情况呢,主要是因为需要同时考虑 性能 和 复杂性 相关的问题
总结而言,根据实际情况,我们在通过复制实现数据统一时,也要考虑其他影响因素而采用不同强度的一致性策略
对于复制而言,最直观的方法就是将副本赋予不同的角色,其中包含一个主副本,主副本将数据存储至本地之后,将数据更改的情况通过日志/流的方式发放到各个副本(也称节点),这种模式下对主副本进行的写请求就会同步到所有其他副本上,而由于数据在所有副本都是统一的,因此读请求可以让任意副本满足,这样就可以使读请求的处理具有拓展性,并对其进行了负载均衡
同样地,主副本通过日志/流的方式与其他副本同步,这步的数据传输是通过网络传输的,因此这段时间不能忽略,会发生在任何请求处理的任何阶段
前面提到的读请求会任意选取一个副本进行处理,写请求只会交给主副本处理并同步给其他副本,这样就可能存在主副本处理好了新的写请求,但尚未同步给其他节点,此时其他节点可能正在处理读请求并返回了结果,在客户端侧给出的请求是先写再读,然而这种情况下结果就可能是先返回读的结果然后副本才同步写的内容,导致读写不一致(类似数据库中的脏读)
为了处理这一问题,有两种角度的解决方案:
这两种方案各有弊端,在不同实际情况下需要选择不同方案
在主从模式下,存在一些关键的功能:
具体实现可能是:在某一个时间点产生一个一致性的快照 -> 将快照拷贝到从节点 -> 从节点连接到主节点请求所有快照点后发生的改变日志 -> 获取到日志后,应用日志到自己的副本当中,称之为追赶 -> 循环多次
从节点失效 - 追赶式恢复
从节点失效处理方式比较简单,通常采用追赶式恢复,对于数据库和Kafka而言,都是通过节点崩溃前最后一次记录的情况与当前主节点最后一次记录的情况比较,将这些缺少的变更应用到从节点中,只不过数据库记录变更使用事务,而Kafka使用checkpoint之间的日志
主节点失效 - 节点切换
主节点失效处理略微复杂,需要经历三个步骤:
这样仍然会存在问题:
前面的主从模式存在这样的弊端:所有写请求都只能通过主节点处理,这样会导致很严重的性能问题,而且无法对其通过横向拓展解决,除此之外,如果客户端来源于不同的地区,则不同客户端之间感受到的服务响应时间的差距可能会很大
因此可以对主从模式进行拓展延伸,采用多个主节点同时承担写请求,主节点接到写入请求之后将数据同步到从节点,不过这里的主节点仍然有可能是其他主节点的从节点,大概像:
两个主节点接到写请求后,分别将自身处理的情况同步至同一个数据中心的从节点,除此之外,该主节点还将不断同步另一数据中心节点上的数据,由于每个主节点同时处理其他主节点的数据和客户端写入的数据,还需要再在模型中增加一个冲突处理模块
由于存在多个主节点,可以在不同主节点上同时进行写请求,因此可能存在两个写请求互相冲突(比如请求a将id为1初值为x的数据改变为值为y,请求b却将其改变为z,这样请求a在主节点1上成功写,并同步至处理请求b的主节点2,主节点2同此,这样两个同步请求就都会发现对方这个数据初值不为x,于是这两个同步请求就都失败了,于是发生了冲突)
解决思路大致如下:
解决方案中的细节:
这样从事件本身推断因果关系和并发的算法,可能会导致被删除掉的数据仍然出现在最后的结果,为了应对这种情况,可以对被删除的数据进行标记,在最后拼接组合的时候对相应的数据进行删除
这种模式去除了主节点,任何节点都能够接收来自客户端的写请求,也有可能会存在一个代表客户端进行统一写入的协调者的角色(与主节点不同,只负责协调,不负责顺序的控制)
如果发来一个写请求,节点集群会寻找能够正常处理请求的n个节点处理,写请求类似,此时可能会收到不同的响应,可以用类似于版本号的方式来区分数据的新旧,不过这里存在一个问题:节点恢复之后可能会因为这种请求处理方式而一直落后于其他节点,一直缺失数据
为此,提出两种思路:
在这种复制模式下,想要保证读取的是写入的新值,每次读取写入多少个副本需要精心打算,此处的核心思路就是让写入的副本和读取的副本存在交集,这样就能保证读取的数据是最新的
为此我们可以实现 读写Quorum (法定人数机制):
w+r>N
N为副本的数量,w为每次并行写入的节点数,r为每次同时读取的节点数
一般配置中:w=r=⌈(N+1)2⌉
这里w/r/N的关系决定了能够忍受多少的节点失效,一般N个节点可以容忍可以容忍 ⌈(N+1)2⌉−1 个节点故障
通过这个公式的配置可以实现无主节点复制中节点失效的情况应对和读最新数据的保证,但是同样还存在一些局限性:
总体而言,这种Quorum复制模式可以达到一个相对高的一致性,但仍需要共识算法的帮助
文档参考