本文主要讲解Redis常见集群方案中涉及到的知识,在了解本文前,需要对CAP原理和Redis的持久化方案有所了解。
一、主从同步
Redis的持久化保证了即使是服务器重启也能恢复几乎全部数据(无论是RDB、还是AOF,除非每条指令保存一次,否则还是会有数据丢失的可能)。因为其持久化方案将数据保存到了磁盘上,那么重启的话就会从磁盘中加载原先的数据。但是这并不是绝对的安全,如果单台服务器发生宕机或者是磁盘损坏,数据发生丢失,那么造成的结果是很严重的。
为了避免单点故障,通常会将数据复制多份保存到其他服务器中。Redis支持主从同步,也支持从从同步,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其他数据库上。
1. 主从同步过程
Redis主从同步的过程如下:
- 首先从数据库向主服务器发起
SYNC
请求,主服务器收到SYNC
指令后,执行bgsave
进行一次 RDB 持久化并使用buffer将持久化过程中执行的修改性指令存储下来; - 主数据库 RDB 持久化完成,将快照文件发送到从服务器上,继续记录这期间主服务器上执行的修改性指令;
- 从数据库接收到快照后,会先将当前内存中的数据清空,然后开始加载快照;
- 主数据在快照发送完毕后,开始向从数据库发送先前 buffer 记录的修改性指令;
- 从数据库将快照记载解析完成后,开始接收请求命令,并执行来自主数据库中缓存的修改性指令;(此时快照同步完成)
- 主数据库每执行一次写命令就会向从数据库中发送相同的写命令,从数据库接收并执行相同命令;(循环进行增量同步)
当从节点刚刚加入到集群时,它必须先进性一次快照同步,同步完成后再继续进行增量同步。
1.1 增量同步
Redis中的增量同步是同步指令流,当主服务器执行对自己的状态产生修改性影响的指令后,会将其记录在本地的内存 buffer 中,然后异步将其同步到从节点中,从节点一边执行指令流来使得主机达到和主节点一样的状态,一边向主节点反馈自己自己同步到哪里了(偏移量)。
因为内存的buffer是有限的,所以Redis主节点不能将所有的指令都记录在内存buffer中,Redis的复制内存 buffer 是一个定长的环形数组,如下图:
如果数组内容满了,就会从头开始覆盖前面的内容。如果因为网络状况不好,从节点在短时间内无法和主节点进行同步,那么当网络状况恢复时,Redis的主节点中那些没有同步的指令在 buffer 中有可能已经被后续的指令覆盖掉了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到前面提及的快照同步了。
1.2 快照同步
快照同步需要 RDB 持久化的实现,首先需要进行一次 bgsave,将当前内存的数据全部存储到磁盘中,然后再将快照文件的内容全部传送到从节点,从节点接收到快照后,首先将内存中的数据清空,然后开始一次全量加载。加载完毕后通知主节点进行增量同步。
在整个快照同步进行的过程中,主节点的 buffer 还在不断复制,不断往前移动,如果从节点快照同步的时间过长或者用来复制指令的 buffer 太小,就会导致同步期间的增量指令在复制 buffer 中被覆盖,这样就会导致快照同步后无法进行增量复制,然后会导致从节点再次发起快照同步,如此极有可能陷入快照同步的死循环。
2. 无盘复制
从Redis 2.8.18 开始,为了降低主节点磁盘开销,Redis支持无盘复制,生成的 RDB 文件不保存到磁盘而是直接通过网络发送给从节点。这样可以减少快照同步对系统的负载产生的影响,特别是当系统正在进行 AOF 的fsync
操作时,fsync
将会被推迟执行,利用无盘复制可以提高主节点的服务效率。
主节点会一边遍历内存,一边将序列化的内容发送到从节点。而从节点还是和之前一样,先将接受到的内存存储到磁盘中,再进行一次性加载。
无盘复制适用于主节点所在机器磁盘性能较差但网络宽带较充裕的场景。但是需要注意的是,无盘复制目前依然处于实验阶段。可以在redis.config
中进行无盘复制的配置:
repl-diskless-sync no
repl-diskless-sync-delay 5
3. 优缺点
接下来我们简单介绍下这种模式的优缺点:
优势:
- 完成了数据备份,保证了单点故障时数据不会全部失效;
- 从节点可以为客户端提供只读操作,为主节点分担了部分压力;
- 无论是从节点还是主节点,在同步时都是以非阻塞的方式进行的,都能继续堆外提供服务。
劣势:
- 这种模式下Redis并不具有自动容错和恢复的功能,如果宕机,那么需要运维人员手动进行主从切换,这段时间内对Redis的部分读写请求无法进行响应;
- 如果主机宕机,且宕机前有部分数据未能及时同步到从机,那么切换 IP 后还会引入数据不一致的问题,降低了系统的可用性;
- 但多个 Slave 断线后需要重启的时,不能在同一时间段进行重启。因为只要Slave启动,就会发送 SYNC 到主机发起全量同步的请求,当多个 Slave 重启的时候,可能会导致 Master IO剧增从而宕机;
- 这种模式下 Redis 很难在线扩容,在集群容量达到上限时在线扩容会变得很复杂;
二、哨兵模式
不同与上一模式中当主节点宕机后需要手动切换,哨兵模式(Redis Sentinel)可以在故障发生时自动进行主从切换。在这种模式中的节点分为三种角色:主节点、从节点以及sentinel
节点,即哨兵节点。我们首先了解一下哨兵,哨兵是一个独立的进程,作为进程它会独立的运行,其主要任务是监控其他节点是否可用。
Sentinel负责持久监控主从节点的健康,通过发送命令,让Redis服务器返回其运行状态,包括主服务器和从服务器。当哨兵检测到 master 宕机时,会自动选择一个最优从节点切换成主节点。然后通过发布订阅模式通知其他的从服务器修改配置文件,让他们切换主服务器。
客户端来连接集群时,会首先连接Sentinel,通过Sentinel来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 Sentinel 要地址,Sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可完成节点切换。
1. 故障切换的过程
假设主服务器宕机,哨兵1先检测到这个结果,此时系统并不会立刻进行 failover 操作。仅仅是哨兵1主观认为主服务器不可用,这个现象称为主观下线。当之后其他的哨兵也检测到主服务器不可用并数量达到一定值时,那么哨兵之间就会进行一次投票,投票结果由一个哨兵发起,进行 failover 操作。切换完成后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
2. 哨兵的工作方式
- 每个哨兵进程以每秒一次的频率向整个集群中的 Master 主服务器以及 Slave 从服务器还有其他的 Sentinel 哨兵进程发送一个 PING 命令;
- 如果一个实例距离最后一次有效回复 PING 命令的时间超过
down-after-milliseconds
选项所指定的值,则这个实例会被 Sentinel 进程标志为主观下线(SDOWN); - 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主服务器的所有 Sentinel 进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态;
- 当有足够的哨兵进程(大于配置文件指定的值)在指定时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN),则Master服务器会被标记为客观下线(ODOWN);
- 在一般情况下,每个Sentinel进程会以每10秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送INFO命令,INFO命令主要是获取节点信息;
- 当主服务器被 Sentinel 进程标记为客观下线时,哨兵进程向下线的主服务器的所有从服务器发送 INFO 命令的频率会从10秒一次改为1秒一次;
- 哨兵和其他哨兵协商主节点的状态,如果主节点处于客观下线状态,则投票自动选出新的主节点,将剩余的从节指向新的主节点进行数据复制;
- 若没有足够数量的哨兵进程同意主节点下线,主节点的客观下线状态就会被移除。若主节点重新向哨兵进程发送 PING 命令返回有效答复,主节点的主观下线状态就会被移除。
那是不是判断完主节点已经下线,就立马开始故障转移了呢?其实不是。那么多哨兵经常必须选取出来一个领导哨兵,去做这件事情。
-
首先,每个在线的哨兵节点都有资格成为领导者。当其中一个哨兵节点A主观判断主节点下线时,就会向其他哨兵节点发送
sentinel is-master-down-by-addr
命令,并请求将自己设置为领导者。这里,就需要将runid设置为自己的runid。 -
收到命令的哨兵节点,如果没有同意过其他哨兵节点的
sentinel is-master-down-by-addr
命令,就会同意该请求,否则拒绝。因为,每个哨兵只有这么一张票,投了别人,就再没有投其他哨兵的机会了。 -
如果哨兵节点A发现自己的票数已经大于或等于半数,哨兵节点A就成为了领导者。
-
如果此过程没有选举出领导者,将进行下一次选举。一般,选举过程非常快,谁先完成客观下线判断,谁就是领导者。
3. 优缺点
哨兵模式可以说是基于主从模式的,所有主从模式所具备的优点,哨兵模式同样具有。除此之外,哨兵模式中的主从服务器可以指定切换,系统更加健壮,可用性更高。但是Redis哨兵模式依旧很难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。同时在这种模式下每个Redis服务器都存储了相同的数据,很浪费内存。
三、Cluster模式
Redis集群是一个由多个节点组成的分布式服务器群,它具有复制、高可用和分片特性;Redis集群没有中心节点,并且带有复制和故障转移特性,这可以避免单个节点称为性能瓶颈,或者因为某个节点下线而导致整个集群下线。
1. 槽位定位
先前提到Redis的哨兵模式很浪费内存,因为每个服务器都会去存储相同的数据。而Cluster模式很好地解决了这个问题,接下来我们就简单介绍下这个问题的解决方案:槽位定位。
首先Redis Cluster将所有的数据划分为16384个槽位,每个节点只负责其中的一部分槽位,如上图所示。客户端默认会对 Key 值使用 crc16 算法进行 hash,得到一个整数值,然后用这个整数值对16384进行取模来得到具体的槽位,然后再去操作槽位所在的节点。所以Cluster模式可以让每个节点只保存一部分的数据而不用全部保存,从而降低了内存消耗。
slot = CRC16(key) % 16384
当 Redis Cluster 的客户端来连接集群时,也会得到一份集群的槽位配置信息。这样当客户端要查找某个Key时,可以指定定位到目标节点。因此客户端为了可以直接定位某个具体的Key所在的节点,需要缓存槽位相关信息,这样才可以准确快速地得到相应的节点。同时因为可能会存在客户端与服务器存储槽位的信息不一致的情况,还需要纠正机制来实现槽位信息的校验调整。
另外,Redis Cluster的每个节点都会将集群的配置信息持久化到配置文件中,所以必须确保配置文件是可写的,而且尽量不要依赖人工修改配置文件。
2. 节点间通信
上面我们提到的Redis虚拟槽位解决的是数据拆分的问题,那么存放缓存数据的节点之间是如何通信的,就是我们接下来要讨论的内容。
缓存节点中存放着缓存的数据,在 Redis Cluster 的分布式部署下,缓存节点会被分配到一台或者多台服务器上。下图是新上线的节点(缓存节点2)和缓存节点1之间的通信:
缓存节点的数目也有可能根据数据量和支持的并发进行扩展。
- 新加入的节点会通过 Gossip 协议向老节点,发起一个“meet消息”。老节点收到消息后会回复一个“pong消息”;-
- 此后缓存节点2会定期向缓存节点1发送一个“ping消息”,同样缓存节点1也会定期回复一个pong消息;
节点之间会互相通过 Gossip 协议进行通讯。而节点之间的通信就是为了维护节点之间的元数据信息,元数据就是每个节点包含哪些数据,是否出现故障。
从传输信息的类型上看来,分为以下四种,分别是:
- Meet 消息,用于通知新节点加入。就好像上面例子中提到的新节点上线会给老节点发送 Meet 消息,表示有“新成员”加入;
- Ping 消息,这个消息使用得最为频繁,该消息中封装了自身节点和其他节点的状态数据,有规律地发给其他节点;
- Pong 消息,在接受到 Meet 和 Ping 消息以后,也将自己的数据状态发给对方。同时也可以对集群中所有的节点发起广播,告知大家的自身状态;
- Fail 消息,如果一个节点另一节点下线或者挂掉了,会向集群中广播这个消息。
2.1 可能下线与确定下线
因为Redis Cluster是去中心化的,一个节点认为某个节点失联了并不代表所有的节点都认为它失联了,所以集群还多经过一次协商过程,只有当大多数节点认定某个节点失联了,集群才会认为该节点需要进行主从切换来容错。
Redis 集群节点采用 Gossip 协议来广播自己的状态以及对整个集群的认知。比如一个节点发现某个节点失联了(PFail,即 Possibly Fail),它会将这条信息向整个集群广播,其他节点就可以收到这点的失联信息。如果收到了某个节点失联的节点数量(PFail Count)已经达到了集群的大多数,就可以标记该失联节点为确定下线状态(Fail),然后向整个集群广播,强迫其他节点也接受该节点已经下线的事实,并立即对该失联节点进行主从切换。
3. 数据迁移
Redis迁移的单位是槽,Redis一个槽一个槽地进行迁移,当一个槽正在迁移时,这个槽就处于中间过渡状态。如下图所示,这个槽在源节点的状态为migrating
,在目标节点的状态为importing
,表示数据正在从源节点流向目标节点。
这里不涉及Redis数据迁移的具体过程,大致流程如下:从源节点获取内存→存到目标节点→从源节点删除内容。注意这里的迁移过程是同步的,在目标节点执行restor
指令到源节点删除 key 之间,源节点的主线程会处于阻塞状态,知道 key 被删除成功。
如果迁移的过程中突然出现网络故障,整个槽的迁移只进行了一半,这是两个节点依旧处于中间过度状态,待下次迁移工具重新连上时,会提示用户进行迁移。
在迁移过程中,如果每个key的内容都很小,那么migrate指令会执行得很快,它就不会影响客户端的正常访问。如果key的内容很大,因为migrate指令是阻塞指令,会导致源节点卡顿,影响集群的稳定性。所以在集群环境下,业务逻辑要尽可能地避免很大的key。
3.1 MOVED 重定向
MOVED指令是用来纠正槽位的,当客户端像一个错误的节点发出了指令后,该节点会发现指令的key所在的槽位并不归自己管理(比如说操作被迁移了)。这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连接这个节点以获取数据。
GET x
-MOVED 3999 127.0.0.1:6381
MOVED 指令的第一个参数 3999 是key 对应的槽位编号,后面是目标节点地址,MOVED指令前面有一个减号,表示该指令是一个错误消息。
客户端在收到MOVED指令后,要立即纠正本地的槽位映射表,后续所有 Key 将使用新的槽位映射表。
3.2 ASKING 重定向
ASKING指令和MOVED不一样,它是用来临时纠正槽位的。如果当前槽位正处于迁移中,指令会先被发送到槽位所在的旧节点。如果旧节点存在数据,那就直接返回结果,如果不存在数据,那么数据可能真的不存在,也可能正在迁移目标节点上,所以旧节点就会通知客户端去新节点尝试拿数据,看看新节点有没有。这时就会给客户端返回一个 asking error 携带上目标节点的地址。客户端收到这个 asking error 后,就会去目标节点尝试。客户端不会刷新槽位映射关系表,因为它只是临时纠正该指令的槽位信息,不影响后续指令。
4. 节点的扩展与收缩
作为分布式部署的缓存节点总会遇到缓存扩容和缓存故障的问题。因此也有产生了节点上线与节点下线的情况。由于每个节点中保存着槽数据,因此当缓存节点出现变动时,这些槽数据会根据对应的虚拟槽算法被迁移到其他缓存节点上。
如上图所示,集群中本来存在缓存节点1和缓存节点2,此时“缓存节点 3”上线了并且加入到集群中。此时根据虚拟槽的算法,缓存节点 1和缓存节点 2中某些对应槽的数据会被迁移到新节点加入到缓存节点 3上面。
针对节点扩容,新建立的节点需要运行在集群模式下,因此新建节点的配置最好与集群内其他节点配置保持一致。
新节点加入到集群的时候,作为孤儿节点是没有和其他节点进行通讯的。因此,其会采用 cluster meet 命令加入到集群中。在集群中任意节点执行 cluster meet 命令让新节点加入进来。假设新节点是 192.168.1.1 5002,老节点是 192.168.1.1 5003,那么运行以下命令将新节点加入到集群中。
192.168.1.1 5003> cluster meet 192.168.1.1 5002
这个是由老节点发起的。新节点刚刚建立没有建立槽对应的数据,也就是说没有缓存任何数据,需要进行数据迁移。而如果这个新上线的节点是从节点,就需要去同步主节点上的数据。
至于节点的收缩就不展开讲了,主要也是涉及到数据的迁移。
参考资料:
-
《Redis深度历险 核心原理与应用实践》