再读raft

背景

raft以前看过, 认为自己懂了, 偶尔再回忆又发现不懂了, 感觉还是没看到本质, 再做下简单总结。

是什么

  • raft实际是日志复制一致性算法,特别是当服务器宕机之后仍然可以保证高可用以及一致性。
  • 核心思想是通过大多数一致来实现高可用以及一致性。当有三个节点的时候允许挂1台几点,当有5个节点的时候允许挂2个节点,由此来保证高可用;也主要是通过大多数来保证一致性,当主挂之后重新选主的时候需要大多数的同意才可以成为主, 让其它节点同意就必须包含最新的日志,最新日志由term以及index来定义, 这样就保证了不会丢掉已经提交的日志, 从而确保了一致性。
  • raft为了保证易读性,拆分了几个模块:选主;日志复制;安全性。也是通过这几个模块的相互约束来协同实现可用性和一致性。对这几个模块分别进行了讨论。后面也介绍了成员变更的操作,日志压缩等。

选主

  • 节点启动的时候是follower的角色。为什么是follwer,而不是candidate、leader另外两种角色?我想加入的时候大多数面临着集群是好的,也就是有leader,作为follower启动, 可以不用给其它节点发消息来影响其它节点也同时可以获得leader的心跳包以及进行日志复制开始,追上最新的日志。如果一启动就选择成为candidate, 需要发起投票,很可能会影响其它节点也很可能浪费了这一步的操作,并且即使可以正常选举也因为没有最新正确的日志而导致选主失败。leader是必须经过大多数同意才可以成为leader,因此一启动没有资格成为 leader。总的来说,1)新节点加入的时候是有leader的,作为follower加入性价比比较高; 2) 新加入的节点没有最新或者最准确的日志,需要作为follower追加上最新的日志。
  • 选主有可能投票失败,比如两个候选者各自都获得了一半的投票。为了避免死循环, 通过加入随机时间来控制。
  • 为了安全性,不是每个节点想当主都可以的。必须要获得大多数的同意,同意的规则是比较最后一条日志,如果候选者的term大则胜出;如果term相等,则index大则胜出;

日志复制

  • 正常情况下, 主接收到了客户端的修改请求之后需要把该日志发给其它所有节点,当大多数节点都接受该日志之后就可以提交该日志到状态机。由于是大多数,当少部分节点比较慢的时候不影响整体性能。
  • 为了保证日志的完整性与正确性, 主从来不删除日志, 同时日志复制的时候会携带该日志的上一条日志的term以及index,如果从没有该日志,则往前回溯, 一直到匹配之后才开始接受,并且覆盖掉冲突的日志,保证日志与主的一致。该行为可以证明主从的某一条日志的term以及index相等,那么之前的所有日志都会一样。
  • 主启动的时候有可能提交过时的日志,这可能导致该数据丢失,因此启动的时候会提交一个空的日志,只有当该空的日志提交成功才会提交老的日志。比较难理解的是为什么提交老的日志可能会导致丢失该数据:试想有一条老的日志且叫它为v1在节点node1,在没有复制到大多数的时候主挂掉,然后一个新的节点node2不包含该v1日志的节点被选中为主, 同时产生了一条日志v2, 也没有复制到大多数的时候挂掉。再选主的时候node1被选中,如果复制老的日志v1到大多数并且提交,但是node2没有收到这条日志,这个时候node1再挂并且没提交最新的日志,node2因为包含了最新term的日志v2被选为主,这个时候node2作为主是没有v1这条日志, 最终导致了这条日志的记录丢失。
  • 日志冲突有多种情况。可能少了;可能多了;可能有少的也有多的日志。1)少很正常, 因为复制到大多数, 少数节点落后很正常. 2)多是在主复制日志没到大多数的时候挂掉,新主不包含这个日志, 那么这条日志就多了,会被删掉。3) 也有多的和少的,少是因为复制的时候成为了少数节点,多是因为该日志没被提交的时候主挂了。这两种情况可以在同一个节点同时出现。

时间控制

  • 在raft里面有几个时间:广播时间, 根据网络的不同,一般在(0.5ms, 20ms)之间; 选举超时时间;MTBF,也即单个服务再次不可用的间隔时间,一般平均为几个月左右。
  • MTBF以及广播时间是由机房网络以及机器硬件决定, 需要人为控制的是选举超时时间, 大概规则是broadcastTime << electionTimeout << MTBF。 因此electionTimeout一般设置为10ms到500ms之间。
  • 其中electionTimeout要大于broadcastTime容易理解, 因为不满足的话很容易陷入重新选主的境况,从而可用性大打折扣。electionTimeout要小于MTBF是为了什么呢:如果主挂了, 那么将在electionTimeout之后才会发生选举, 如果设置时间太长,可用性也会非常低,因此应该远小于MTBF才对。

成员变更

  • 背景: 线上存在替换节点以及修改复制个数的情况,例如从3个改为5个。由于各个节点接收到新配置的时间点有差异, 很有可能导致选出双主, 从而影响数据的正确性。比如从3变为5, 新加的2个节点有最新的配置,老的节点只有一个接收到了新的配置, 那么这个时候由于2套配置,很可能选出2个主来。如何避免这种情况呢, 容易想到的是全部停机更新再重启, 但是这必然影响到线上的可用性,很多服务是不能停机的。
  • raft提出join consensus。两阶段提交, 分别提交C(old,new)和C(new). 只有当C(old, new)提交成功之后才会提交C(new), 这样可以避免C(old)和C(new)同时生效, 同时生效的要包含C(old, new), 假如同时存在C(old, new)和C(old), 也只能选出一个主来, 因为C(old, new)既要old中的大多数同意也要new中的大多数同意, 同理可以推出C(old, new)和C(new)也只能选出一个主来。
  • 几个重要原则: 1) 任何节点接到了配置相关的日志,无论是否提交, 都按照这个去做决策。 2) C(old, new)表示决策的时候既要old中的大多数同意也要new中的大多数同意。
  • 优化1: 新加入的节点由于没有最新的日志,导致在C(old,new)中提交日志会非常慢(必须把以前的日志补齐才行),那么将导致不可用,因此新节点在加入的时候是作为非投票成员加入, 仅仅从主接收日志,等到日志补齐之后再加入。
  • 优化2: C(old,new)提交之后,主可能不在C(new)中,但是仍然作为主来服务, 只有当C(new)提交之后才转变为follower,否则可能导致C(old)中过时的成员被选为主。
  • 优化3: 不在C(new)中的节点被移除之后,不再收到心跳包, 会一直触发超时选举,影响新的节点,从而影响对外服务。新的节点接收到选主请求需要判断是否在选举超时时间内有没收到主的心跳, 如果收到则拒绝投票。

日志压缩

  • snapshot机制 , 比较简单, 不再详述

你可能感兴趣的:(再读raft)