Redis 可以单机部署,但会带来单点问题和性能瓶颈。为此 Redis 提供了主从复制、哨兵和集群的方式来解决这些问题。
一、主从复制
主从复制可以将两台或者多台服务器之间的数据同步,这样在主服务器下线后,从服务器可以继续对外提供服务,保证了系统的高可用;另外主从复制也可以进行读写分离,主服务器只提供写操作或少量的读,把多余读请求通过负载均衡算法分流到单个或多个从服务器上。
Redis 的复制功能分为同步和命令传播两个操作,同步操作用于将从服务器的状态更新至主服务器当前的状态;命令传播操作用于在主服务状态被修改时,让主从状态重新保持一致。
同步
可以通过使用SLAVEOF <host> <port>
命令来让一个服务器成为另一个服务器的从服务器。此时从服务器会自动向主服务器发送 SYNC 的命令进行同步操作,步骤如下:
- 1)主服务器收到 SYNC 命令后执行 BGSAVE 命令,在后台生成一个 RDB 快照文件,并用一个缓冲区记录从现在开始执行的所有写命令。
- 2)从服务器从主服务器接收到 RDB 文件后,丢弃所有旧数据,载入主服务器发来的快照文件。
- 3)主服务器把记录在缓冲区的命令发送给从服务器,从服务器执行这些命令,同步完成。
但是这种同步方式会带来一个断线后重复值效率低的问题:如果从服务器短暂掉线后重连主服务器,从服务器会再次向主服务器发送 SYNC 的命令,主从之间不得不重新同步所有数据,这是没有必要的,因为从服务器仍然保留了大部分数据。Redis 2.8 推出了部分重同步的方式解决了这个问题。
部分重同步(2.8)
部分重同步用于处理断线后复制的情况。Redis 部分重同步的实现由以下三个部分构成:
- 复制偏移量 (offset):复制偏移量表示当前从服务器从主服务器中接收了多少字节的数据。
- 复制积压缓冲区:复制积压缓冲区以 FIFO 的形式保存了最近的写命令。
- 服务器 id (runid):用于唯一标识服务器。
当 slave 初次复制 master 时,master 会将自己的 runid 发给 slave 进行保存,而当 slave 断线重连之后,会向 master 发送一个PSYNC <runid> <offset>
的命令,offset 即当前 slave 的复制偏移量,runid 是 slave 记录的掉线之前的 master 的 runid。master 在收到 PSYNC 后,会首先对比自己的 runid 和传过来的 runid 是否一致,如果一致,说明这个 slave 之前和自己同步过数据,然后根据 offset 的差值把复制积压缓冲区的数据同步给 slave。这样就实现了增量更新。
部分重同步(4.0)
2.8 版本推出的部分重同步我们称为 psync1,它解决了网络闪断的问题。但它无法解决 master 更替和 slave 重启的问题:1)当 master 更替以后,主节点的 runid 会发生变化,其它 slave 切换过去时,由于 runid 不同,无法进行部分重同步;2)slave 重启时之后,原先保存的 runid 和复制偏移量都会丢失,无法进行部分重同步。针对以上两个问题,redis 在 4.0 版本优化了部分重同步的策略,称为 psync2。
psync2 把 psync1 中由 slave 保存的 master 节点的 id 记为 replid,同时又增加了一个 replid2 字段,这个字段表示这个节点关联的上一个 master 的 id。对于从节点来说,replid 和 replid2 分别代表上一个和当前关联的主节点的 id,而对于主节点来说,replid 是自己的 id,而 replid2 是上一个主节点的 id。当主节点切换以后,原先主节点的从节点切换到新的主节点上时,新的主节点会判断从节点的 replid 是否等于自己的 replid2,如果相等,说明这两个节点曾经属于同一个主节点,那么就可以尝试部分重同步(当然还需要新的 master 在还是 slave 的时候就开启了复制积压缓冲区功能)。
对于 slave 重启的问题,psync2 也做了优化。当 redis 关闭时,replid 和复制偏移量会被保存进 rdb 文件,重启时,redis 会读取这两个值,然后和主节点进行部分重同步。
命令传播
在同步完成之后,主从服务器就进入了命令传播阶段。在这个阶段,主服务器发生写操作后,会把相应的命令发送给从服务器执行,这样主从之间就保持了数据一致性。
从服务器也会向主服务器发送命令REPLCONF ACK <replication_offset>
,这是一个心跳检测命令,每隔 1 秒就会发一次,它有以下两个作用:
- 检测主从服务器的网络连接状态:主服务器会记录从服务器上次心跳检测的时间,如果超过 1 秒,就说明连接出了故障。如果主服务器和大量从服务器之间的连接出了故障,比如有 3 台以上的从服务器超过 10 秒没有心跳检测,则会拒绝执行写命令。
- 检测命令丢失:在 2.8 以后,心跳检测会复用主服务器的复制积压缓冲区,主服务器在接收到心跳检测后会检查这个偏移量是否和自己的一致,如果不一致会补发缺失的数据。
REPLCONF ACK 命令和复制积压缓冲区都是 Redis 2.8 版本新增的,在 Redis 2.8 版本以前,即使命令在传播中丢失,主服务器和从服务器都不会注意到,主服务器更不会向从服务器补发丢失的数据,所以为了保证复制时主从服务器的数据一致性,最好使用 2.8 或以上版本的 Redis。
二、Sentinel(哨兵)
Sentinel(哨兵)是 Redis 高可用的解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器以及这些服务器下的所有从服务器。在主服务器进入下线状态时,自动将下线服务器下的某个从服务器升级成主服务器。
Sentinel 系统是由 Sentinel 实例组成的,彼此间通过命令连接相互通信:
Sentinel 实例会以每秒一次的频率向自己监控的主服务器发送 PING 命令,主服务器会回复该命令,以此来监控主服务器的在线状态。当 Sentinel 实例发送 PING 命令之后等待超过配置指定时间之后,Sentinel 实例会判定该主服务器处于主观下线状态。
当一个 Sentinel 实例判定自己监控的某个主服务器为主观下线之后,它会询问其它监控该服务器的 Sentinel 实例,当超过配置指定数量的 Sentinel 实例也认为该服务器已下线时,Sentinel 实例会判定该服务器为客观下线。
当有超过配置指定数量的 Sentinel 实例认为该服务器客观下线之后,监视该服务器的各个 Sentinel 会进行协商,通过 Raft 算法选举出一个领头 Sentinel 对下线主服务器执行故障转移操作。
故障转移操作包括三个部分:
- 1)在已下线主服务器属下的所有从服务器中挑选一个从服务器,将其转换为主服务器。
- 2)让其它从服务器改为复制新的主服务器(发送 SLAVEOF 命令)。
- 3)将原来的主服务器设置为从服务器。
三、集群
Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片的方式进行数据共享,并提供复制和转移功能。
集群中的节点可以通过向其它节点发送CLUSTER MEET <ip> <port>
命令邀请其它节点加入集群。
集群的整个数据库被分为 16384 个槽,数据库中的每一个键都属于这 16384 个槽中的一个,只有当集群中的每个槽都有节点在处理时,集群才处于上线状态。
我们可以通过CLUSTER ADDSLOTS [slot...]
命令将一个或多个槽指派给节点。每个节点会记录自己和集群中其它节点被指派的槽。
节点在接到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责,如果不是的话,节点将向客户端返回一个 MOVED 错误,MOVED 错误携带的信息可以指引客户端转向正确的节点。
除了可以指派槽以外,我们还可以通过 redis-trib 这个软件将槽重新分片。重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。
在重新分片期间,如果客户端向原来的节点请求键 k,而 k 已经被转移到另一个节点时,节点会返回一个 ASK 错误,指引客户端到新的节点。
MOVED 错误表示槽的负责权已经永远转移了,而 ASK 错误只是两个节点在槽迁移时的临时措施。
节点通信
Redis 使用 Gossip(流言)协议进行通信,这是一种分散式的通信协议,与之相对的是集中式。集中式是把集群的元数据(节点信息,故障,等等)存在一个节点上,然后由其它节点去读取;而分散式是在节点之间不断进行通信,通过不断通信来互相同步彼此的状态。相比于集中式,分散式的通信主要有以下两个优点:1)压力分散,不需要主节点承担全部压力;2)容错性好,可以避免单点故障。但同时,分散式也具有信息滞后的缺点。
Redis 集群中每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口。同时每个节点内部会维护一份集群节点名单,来保存集群中其它节点的信息(套接字、创建时间、节点状态、ip、端口号、主从标记、槽信息等)。节点之间会通过 TCP 发送消息,消息的内容包括故障信息,节点的增加和移除,哈希槽等。
Gossip 协议包含多种消息,包括 MEET,PING,PONG 和 FAIL 等等。他们的发送时机和传达内容如下:
- MEET:MEET 消息相当于一个握手消息。我们可以通过向节点 A 发送
CLUSTER MEET <ip> <port>
命令让其将另一个节点 B 添加到集群中。节点 A 在收到命令后会向节点 B 发送一个 MEET 消息,之后会触发握手,握手之后节点 A 和节点 B 会将对方添加进自己内部的集群节点名单,然后节点 A 会将节点 B 的信息通过 Gossip 协议传播给集群中的其它节点,让其它节点也与节点 B 进行握手,最终,经过一段时间之后,节点 B 会被集群中的所有节点认识。 - PING:PING 消息相当于心跳消息。每个节点都会频繁给其他节点发送 PING 消息,其中包含自己的状态还有自己维护的集群元数据。每个节点每秒会执行 10 次 PING,每次会选择 5 个最久没有通信的其他节点。节点在 PING 时会带上自己和 1/10 个(最少 3 个,最多总节点-2 个)其它节点的信息。
- PONG:PONG 消息相当于一个响应消息,一般用于收到 PING 或 MEET 之后进行回复。PONG 消息内部也会包含节点自身的信息。
- FAIL:节点在发出 PING 超过一段时间后没有收到对方的 PONG,就会判断对方下线,此时会发送 FAIL 消息给其它主节点,告诉它们指定的节点下线了。当某个主节点发现集群中有超过半数主节点认为某个主节点疑似下线,它就会把这个主节点标记为下线,并广播 FAIL 给集群中全部其它的节点(主节点+从节点)。
故障转移
为了集群的高可用,集群中的节点也分主节点和从节点,从节点复制主节点的数据。
主节点之间通过 PING 消息来互相确认对方的在线状态,如果没有在规定时间内收到对方的 PONG,则会把对方标记为疑似下线状态,并将这个消息告知给集群中的其它主节点。当某个主节点发现集群中有超过半数主节点认为某个主节点疑似下线,它就会把这个主节点标记为下线,并广播给集群中其它的节点(主节点+从节点)。
当从节点发现自己正在复制的主节点已下线时,会通过 raft 算法选举出新的主节点。选举的候选节点是已下线主节点的所有从节点,投票节点是其它所有主节点。最后一个从节点会被推选为新的主节点,接管由已下线节点负责处理的槽。