分布式学习笔记_04_复制模型

常见复制模型

使用复制的目的

在分布式系统中,数据通常需要被分布在多台机器上,主要为了达到:

  • 拓展性:数据量因读写负载巨大,一台机器无法承载,数据分散在多台机器上仍然可以有效地进行负载均衡,达到灵活的横向拓展
  • 高容错&高可用:在分布式系统中单机故障是常态,在单机故障的情况下希望整体系统仍然能够正常工作,这时候就需要数据在多台机器上做冗余,在遇到单机故障时能够让其他机器接管
  • 统一的用户体验:如果系统客户端分布在多个地区,通常考虑在多个地区部署服务,这样用户就可以就近访问他们所需的服务,获得统一的用户体验

这样看下来,分布式系统中为了保证整体架构中不因一台机器故障而导致整体瘫痪,需要对故障的机器中的数据进行转储保存,这时候就需要对数据进行复制

复制的目标是保证若干个副本上的数据是一致的,此处的“一致”概念不唯一,可能指不同副本的数据在任何时候都严格保持完全一致,也可能指不同客户端不同时刻访问到的数据一致,这里一致性的强弱也可能不同,可能是不同客户端同一时间访问到的数据相同,也有可能是一段时间访问的数据不同,但这段时间后访问的数据却相同

为什么会出现这么多一致性强弱不同的情况呢,主要是因为需要同时考虑 性能复杂性 相关的问题

  • 性能 而言,我们为分布式系统中每个机器上的数据做冗余本就不止是为了实现系统的高可用,同时存在降低延迟和实现负载均衡相关的关于性能的目的,因此如果过分地强调数据一致性,反而会导致我们最初的目标中的提高性能这一部分不能达到满足,适得其反
  • 复杂性 而言,分布式系统中存在比单机系统中更加复杂的不确定性,节点之间通过不稳定的网络传输,系统时间与内存地址无法共享统一等,这些分布式系统的本质缺点会导致数据统一这个需求变得难以满足

总结而言,根据实际情况,我们在通过复制实现数据统一时,也要考虑其他影响因素而采用不同强度的一致性策略

数据复制模式

主从模式

对于复制而言,最直观的方法就是将副本赋予不同的角色,其中包含一个主副本,主副本将数据存储至本地之后,将数据更改的情况通过日志/流的方式发放到各个副本(也称节点),这种模式下对主副本进行的写请求就会同步到所有其他副本上,而由于数据在所有副本都是统一的,因此读请求可以让任意副本满足,这样就可以使读请求的处理具有拓展性,并对其进行了负载均衡

同样地,主副本通过日志/流的方式与其他副本同步,这步的数据传输是通过网络传输的,因此这段时间不能忽略,会发生在任何请求处理的任何阶段

前面提到的读请求会任意选取一个副本进行处理,写请求只会交给主副本处理并同步给其他副本,这样就可能存在主副本处理好了新的写请求,但尚未同步给其他节点,此时其他节点可能正在处理读请求并返回了结果,在客户端侧给出的请求是先写再读,然而这种情况下结果就可能是先返回读的结果然后副本才同步写的内容,导致读写不一致(类似数据库中的脏读)

为了处理这一问题,有两种角度的解决方案:

  • 让客户端只从主副本读取数据,这样正常情况下所有客户端读到的数据必定一致
  • 采用同步复制,但这种情况下每次同步数据都需要等待所有非主副本成功同步,如果存在故障情况,则所有副本都陷入阻塞状态,无法正常运行

这两种方案各有弊端,在不同实际情况下需要选择不同方案

在主从模式下,存在一些关键的功能:

增加新的从副本
  • 在Kafka中,采用新建副本分配的方式,以追赶的方式从主副本中同步数据
  • 在数据库中,采用快照+增量的方式实现

具体实现可能是:在某一个时间点产生一个一致性的快照 -> 将快照拷贝到从节点 -> 从节点连接到主节点请求所有快照点后发生的改变日志 -> 获取到日志后,应用日志到自己的副本当中,称之为追赶 -> 循环多次

处理节点失效

从节点失效 - 追赶式恢复

从节点失效处理方式比较简单,通常采用追赶式恢复,对于数据库和Kafka而言,都是通过节点崩溃前最后一次记录的情况与当前主节点最后一次记录的情况比较,将这些缺少的变更应用到从节点中,只不过数据库记录变更使用事务,而Kafka使用checkpoint之间的日志

主节点失效 - 节点切换

主节点失效处理略微复杂,需要经历三个步骤:

  • 确认主节点失效,通常采用超时判定,节点之间会定期互相发送心跳,如果某个节点长时间无反应则认定为失效(具体到Kafka而言,其通过和ZooKeeper之间会话保持心跳,让ZooKeeper来监控Kafka节点是否失效)
  • 选举新的主节点,选举时可以使用共识算法或特定的组件指定特定的节点,在选举时需要尽可能让新的主节点与原先的主节点差距尽可能小,从而最小化丢失数据的风险
  • 重新配置系统使新的主节点生效,可以理解为对集群的元数据进行更改,让其他从节点知道新的主节点的存在并保证后续旧的主节点(现在是从节点)无法再处理写请求

这样仍然会存在问题:

  • 如果采用异步复制(还存在复制滞后的问题),在失效后,旧的主节点与新的主节点存在差异,但在重新配置系统成功之前,可能旧的主节点仍然会处理写请求,导致新旧主节点数据不同步
  • 如果旧的主节点由于故障无法得知自己不再是主节点,仍然继续处理写请求并尝试与其他从节点同步,则会出现“脑裂”的情况
  • 对超时时长的设计十分复杂,如果超时时间过短则可能会导致系统负载较高时频繁切换主节点导致负载进一步加重甚至系统瘫痪
多主节点复制

前面的主从模式存在这样的弊端:所有写请求都只能通过主节点处理,这样会导致很严重的性能问题,而且无法对其通过横向拓展解决,除此之外,如果客户端来源于不同的地区,则不同客户端之间感受到的服务响应时间的差距可能会很大

因此可以对主从模式进行拓展延伸,采用多个主节点同时承担写请求,主节点接到写入请求之后将数据同步到从节点,不过这里的主节点仍然有可能是其他主节点的从节点,大概像:

两个主节点接到写请求后,分别将自身处理的情况同步至同一个数据中心的从节点,除此之外,该主节点还将不断同步另一数据中心节点上的数据,由于每个主节点同时处理其他主节点的数据和客户端写入的数据,还需要再在模型中增加一个冲突处理模块

使用场景
  • 多数据中心部署:一般采用多主节点复制,都是为了做多数据中心容灾或让客户端就近访问(异地多活),比起主从复制模型存在 性能提升(吞吐量增加 + 延迟降低)和 容忍数据中心失效 的能力
  • 离线客户端操作:存在一种应用场景是,如果存在离线的客户端仍然进行写请求,那么在联网之后会将本地的更改应用到其他节点上并且将其他节点的更改同步到本地,这样就实现了离线情况下的请求处理与恢复后的数据同步
冲突解决

由于存在多个主节点,可以在不同主节点上同时进行写请求,因此可能存在两个写请求互相冲突(比如请求a将id为1初值为x的数据改变为值为y,请求b却将其改变为z,这样请求a在主节点1上成功写,并同步至处理请求b的主节点2,主节点2同此,这样两个同步请求就都会发现对方这个数据初值不为x,于是这两个同步请求就都失败了,于是发生了冲突)

解决思路大致如下:

  • 避免冲突:尽可能不让这种情况发生,即让应用层保证对特定数据的请求只发生在一部分节点上,这样就不会发生冲突,但是会降低效率
  • 收敛于一致状态:但是冲突无法完全避免,对于多主节点而言,无法保证绝对顺序,因此可以为每个写请求都赋予一个无重复的uniqueID,最终选取其中ID最高/低的请求进行实现,但是分布式系统无法保证系统时间统一,因此如果将uniqueID设置为时间戳的话(也就是所谓“的最终写入者获胜”),就又会引起新的问题(时钟飘移)
  • 用户自行处理:让用户自己在读取/写入前进行冲突解决,选取最终采用的版本(Github就是这样做的)

解决方案中的细节:

  • 直接指定事件顺序:也就是通过设置uniqueID为时间戳并选取最终的写请求实现的话,可以解决问题,但是在对数据一致性要求很高的情况下,这种方式会引入风险
  • 从事件本身推断因果关系和并发:有这样一套算法,服务端为每个主键维护一个版本号,每当主键新值写入时递增版本号,并将新版本号和写入值一起保存;服务器写主键,写请求包含之前读到的版本号,发送的值为之前请求读到的值和新值的组合,写请求相应也会返回当前所有的值,这样一步步拼接;当服务器收到有特定版本号的写入时,覆盖该版本号或更低版本号的所有值,保留高于请求中版本号的新值(与当前写操作并发)

这样从事件本身推断因果关系和并发的算法,可能会导致被删除掉的数据仍然出现在最后的结果,为了应对这种情况,可以对被删除的数据进行标记,在最后拼接组合的时候对相应的数据进行删除

无主节点复制

这种模式去除了主节点,任何节点都能够接收来自客户端的写请求,也有可能会存在一个代表客户端进行统一写入的协调者的角色(与主节点不同,只负责协调,不负责顺序的控制)

处理节点失效

如果发来一个写请求,节点集群会寻找能够正常处理请求的n个节点处理,写请求类似,此时可能会收到不同的响应,可以用类似于版本号的方式来区分数据的新旧,不过这里存在一个问题:节点恢复之后可能会因为这种请求处理方式而一直落后于其他节点,一直缺失数据

为此,提出两种思路:

  • 客户端读取时对副本进行修复,如果客户端通过并行读取多个副本时,有落后的副本,可以在此时顺便将数据写入旧副本当中
  • 反熵查询,一些系统在副本启动后,后台会不断查找副本之间的数据diff,并将diff写入自己的副本,但是这样不保证写入的顺序,并可能引发明显的滞后

在这种复制模式下,想要保证读取的是写入的新值,每次读取写入多少个副本需要精心打算,此处的核心思路就是让写入的副本和读取的副本存在交集,这样就能保证读取的数据是最新的

为此我们可以实现 读写Quorum (法定人数机制):

w+r>N

N为副本的数量,w为每次并行写入的节点数,r为每次同时读取的节点数

一般配置中:w=r=⌈(N+1)2⌉

这里w/r/N的关系决定了能够忍受多少的节点失效,一般N个节点可以容忍可以容忍 ⌈(N+1)2⌉−1 个节点故障

通过这个公式的配置可以实现无主节点复制中节点失效的情况应对和读最新数据的保证,但是同样还存在一些局限性:

  • 对于一些没有很强数据一致性要求的系统,可以设计w+r<=N,这样能够减少等待节点处理并返回的时间,不过可能会读取旧值
  • 在w+r>N的情况下仍然可能存在一些一致性问题,比如两个写操作同时发生,最终数据合并时仍然可能出现数据丢失;如果读写同时发生,由于复制的滞后性,仍然不能保证一定能够读取到新值;由于副本部分写入失败,且总体写入成功的副本数少于w,那么写入成功的副本数据不会回滚,导致最终读取的时候仍然可能读取到写入失败的新值

总体而言,这种Quorum复制模式可以达到一个相对高的一致性,但仍需要共识算法的帮助

文档参考

你可能感兴趣的:(分布式,学习,笔记,架构,后端)