redis复制与etcd raft的区别
分布式系统的一致性和性能常常是鱼和熊掌不可兼得。追求高的一致性,必然会带来性能的损失,而想要追求高的性能,也只能妥协于一定程度的非一致性。以下图中的数据写入为例,不同的一致性级别要求写入的节点个数是不同的, 写入节点个数越多,显然客户端需要等待的时间就会越久。
一致性差异:
Raft中采用的是QUORUM, 即确保至少大部分节点(n/2+1)都接收到写操作之后才会返回结果给Client, 而Redis默认采用的实际上是ONE, 即只要Master节点写入成功后,就立刻返回给Client,然后该写入命令被异步的发送给所有的slave/follower节点,来尽可能地让所有地slave/follower节点与master/leader节点保持一致。
相同点:
Redis集群和Raft协议都采用了主从节点的结构, 只有主节点才能响应客户端的请求,从节点的唯一任务就是备份主节点的数据。当我们有多个节点的时候,什么时候选择主节点,选择谁当主节点就是两个重要的问题。前者就是Fault Detection问题, 后者才是Leader Election问题。
Fault Detection差异
在Raft中,所有的follower节点都是passive的, 即只接收来自leader的通信,彼此之间并不相互通信。所以每个follower都是单独去判断当前的leader是否已经故障。
但是这样做其实有个显而易见的问题, 就是如果A发现leader节点B好久都没发心跳信息给他了,到底是B节点真的故障了呢?还是说A和B之间发生了network partition呢?如果是后者,那么A就会发起一次无效的选举,因为其他的大部分节点都认为B还活着,自然会拒绝A的选举拉票。为了解决这个问题,Raft又引入了一种PreVote的状态,即每个节点发起选举之前,首先进入PreVote阶段,尝试获取一下选举,如果能够获得足够多的选票就进入真正的选举状态,增加Term开始选举。
Redis集群中对于Master节点是否故障是采用多数投票决定的。即当大部分节点都认为某个Master下线之后,该Master才被认为是真正的下线。与Raft中的HeartbeatTimeout参数作用类似, Redis集群有个NodeTimeout参数,当某个节点A在距离上次与Master节点B通信之后的NodeTimeout时间之后还未收到新的PING消息, 那么A就会标记节点B为PFAIL, 并把这个信息Gossip给集群中的其他节点。当某个节点发现集群中大部分节点都标记B为PFAIL时,就会认为B真正的下线了。标记B为FAIL,并同时广播给集群中的所有节点该信息以让大家对B节点保持一致的观点。
这样的做法实际上很好的避免的假阳性,即由于AB节点彼此间的网络问题错认为B节点下线。如果把这个做法应用到Raft协议中,就需要follower之间能够交换信息, 即当大部分follower节点都认为leader节点下线之后,follower才开始发起新一轮的选举。
Leader Election差异
Raft
拉票方:选举资格检查:首先检查自己是否能连接上集群的大部分节点来确定自己是否有资格发起选举
随机休眠: 休眠rand(150, 300)这么久的时间,以避免所有的节点同时开始选举
发起拉票: 休眠结束后向所有节点发起拉票请求,即RequestVote
投票方:投票资格检查: 如果本Term内已经投出去票了,那么不再具备投票资格拉票方数据
更新程度检查: 检查发起选举的节点的数据是否比自己的数据更新
投票或拒绝: 根据第2步的结果决定投票或拒绝
Redis
拉票方:选举资格检查:与Raft的检查规则不同,每个slave节点不尝试连接集群中的其他节点,而是检查自己是否已经与之前的master节点太久未进行通信。正常来讲,master节点会定期向slave节点发送ping消息,如果长时间未通信,那么该slave节点极有可能在数据上落后Master节点过多,于是放弃发起选举,否则就进入第二步
随机休眠: 与Raft统一随机休眠rand(150, 300)不同,Redis集群中每个参与选举的slave节点的休眠时间计算公式如下。
DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds + SLAVE_RANK * 1000 milliseconds.
其中的SLAVE_RANK即各个节点按照repl_offset的大小排序的结果。从休眠时间的计算公式上看,即数据更新的(repl_offset更大)的节点休眠时间短,最先醒来,也最有可能被选举。
发起拉票: 休眠结束后向所有节点发起拉票请求,即向其他节点发送CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息
投票方:投票资格检查: 和Raft一样,Redis集群中一个Term内每个节点至多只能投一票。如果本Term内已经投出去票了,那么不再具备投票资格拉票方数据
更新程度检查: 和Raft不同的是,Redis中只检查发起选举的节点的Term是否比自己的Term更大
投票或拒绝: 根据第2步的结果决定投票或拒绝
Log复制差异
Log Replication即数据同步。Raft和Redis一样,数据只从Master节点流向Slave节点,同时二者都有全量复制和部分复制的概念。二者都在内存中开辟一个缓存区缓存来自客户端的请求,同时在特定的规则被触发之后,将缓存区的数据持久化落盘。接下来我们分别介绍全量复制和部分复制。
全量复制
全量复制即把所有的数据都发送给子节点。我们分别看Redis和Raft的做法异同。这部分内容其实就是Raft中的Log Compaction。
RAFT
Raft会在某些规则触发之后把内存缓冲区中的所有log落盘,或者生成application state machine的snapshot。生成的这两类数据即为全量数据。当有新的节点加入集群或者某个follower节点落后leader节点太多时, leader节点就会将全量数据发送给follower节点做状态的初始化。我们以作者重点描述的snapshot为例, 当follower节点接收到来自leader的snapshot文件时,会与自己的数据进行比较。如果自己的log相对来说比较新,那么就会选择忽视掉该文件,否则就用leader的snapshot初始化整个application state machine。
REDIS
Redis有两种持久化措施,AOF持久化和RDB持久化。其中AOF持久化就类似于Raft中的log落盘, RDB持久化就是生成整个Redis数据库的快照文件。当一个新的节点加入成为slave节点或者当前的某个slave节点数据落后Master节点数据过多时,Master会生成RDB文件或者把已有的AOF文件直接发给slave节点进行状态初始化。我们同样以快照为例, 与Raft不同的是, 在Redis中,只要slave节点接收到了来自master节点的RDB文件, 就会把application state machine(即Redis内存数据库)全部清空,并用RDB文件来初始化。
部分复制差异
前面我们提到,Redis和Raft的master节点都在内存中维护一个buffer来缓存来自客户端的请求,同时在触发一些规则后把buffer的数据落盘。同时落盘的数据就从缓存中删除掉。那么当follower节点/slave节点的数据虽然落后于master节点,但是缺少的部分还被master节点缓存在内存中的时候,就会触发部分复制,即缺啥补啥。部分复制包含命令转发和补发过往数据。
Raft中数据是leader节点PUSH给follower节点的, 节点数据更新的进度是由Master节点维护的(即[]nextIndex数组), 即Master决定该同步给follower节点什么数据,而Redis中数据同步是通过slave节点从master节点PULL数据完成,同步进度(repl_offset)是由各个slave节点自己维护的,即Slave节点决定需要同步哪些数据。
Raft Leader节点维护每个follower节点的数据同步进度,并不断地去push数据给follower节点。
Redis当接收到来自客户端的数据更改请求时, 只要Master处理完该请求,就立马返回客户端,异步的把这些命令转发给所有的slave节点。但是转发的过程中可能因为网络问题导致数据丢失。于是master节点在内存中开辟一个默认大小为1M的环形缓冲区,在转发给slave节点用户请求的同时把该请求写入环形缓冲区。每个slave节点接收到一个命令之后会自增repl_offset值用来记录复制偏移量。同时master节点也会记录当前缓冲区的最新命令的repl_offset。salve节点会定期的向master节点发送PSYNC命令请求同步复制,master节点会检查slave节点的复制偏移量是否在当前master节点的缓冲区中,如果在则从缓冲区中发送命令,否则触发全量复制。
Log持久化Compaction
log compaction实际上就是持久化措施。这部分实际上在全量复制那里已经有涉及。就是日志持久化和application state machine的快照持久化。对应到Redis中就是AOF持久化和RDB持久化。
COW
快照持久化是个很耗时间的操作,Redis和Raft都采用fork一个子进程出来进行持久化。fork出来的子进程会拷贝父进程所有的数据,这样理论上当Redis要持久化2G的内存数据的时候,子进程也会占据几乎2G的内存,那么相关的进程内存占用就会达到4G左右,这在数据比较小的时候还不严重,但是比如你的电脑内存是8G,目前备份快照数据本身体积是5G,那么按照上面的计算备份一定是无法进行的,所幸在Unix类操作系统上面,做了如下的优化:在刚开始的时候父子进程共享相同的内存,直到父进程或者子进程进行内存的写入后,对被写入的内存共享才结束。这样就会减少快照持久化时对内存的消耗。这就是COW技术,减少了快照生成时候的内存使用的同时节省了不少时间。
总结
相比较raf协议,redis采用的一致性协议实际上是非常弱的,其核心原因在于二者的追求实际上是不一致的。raft追求的是一致性,而redis追求的是在不影响性能的情况下尽可能的保证一致性。