Redis搭建主从集群还是比较简单的,只需要通过
replicaof 主库IP 主库端口
命令就可以了。如下图,我们分别在101和102机器上执行命令:
1 | replicaof 192.168.0.1 6379 |
我们就构建了一个主从集群:

为了保证主从数据的一致性,主从节点采用读写分离的方式:主库负责写,同步给从库,主从均可负责读。
那么,我们搭建好了主从集群之后,他们是如何实现数据同步的呢?
1、主从集群数据如何同步?
当从库和主库第一次做数据同步的时候,会先进行全量数据复制,大致流程如下图所示:
- 从库执行replicaof命令,与主库建立连接,发送psync命令,请求同步,刚开始从库不认识主库,也还不知道他的故事,所以说了一句:那谁,请开始讲你的故事(psync ? -1);
- 主库收到从库的打招呼,于是告诉了从库它的名字(runId),要给他讲整个人生的经历(FULLRESYNC,全量复制),以及讲故事的进度(offset);
- 主库努力回忆自己的故事(通过BGSAVE命令,生成RDB文件);
- 主库开始跟从库讲故事(发送RDB文件);
- 从库摒弃主观偏见,重新开始从主库的故事中认识了解主库;
- 主库讲故事过程中,又有新的故事发生了(收快递),于是继续讲给从库听(repl buffer)。
1.1、主从复制过程中,新产生的改动如何同步?
从上面的流程图中,我们也可以知道,主库会在复制过程中,把新的修改操作追加到replication buffer中,当发送完RDB文件后,再把replication buffer中的修改操作继续发送给从库:
1.2、从库太多了,主库如何高效同步?
当我们要同步的从库越多,主库就会执行多次BGSAVE进行数据全量同步,为了减小主库的压力,可以做如下的集群配置,通过选择一个硬件配置比较高的从库作为其他从库的同步源,把压力分担给从库:

1.3、首次同步完成之后,如何继续同步增量数据?
主从同步完成之后,主库会继续使用当前创建的网络长连接,以命令传播的方式进行同步增量数据。
1.4、主从断开了,如何继续同步?
当主从断开重连之后,从库会采用增量复制的方式进行同步。
为了实现增量复制,需要借助于repl_backlog_buffer缓冲区。
如下图:

Redis主库会给每一个从库分别创建一个replication buffer
,这个buffer正是用于辅助传播操作命令到各个从库的。
同时,Redis还持有一个环形缓冲 repl_backlog_buffer,主库会把操作命令同时存储到这个环形缓冲区中,每当有从库断开连接的时候,就会向主库重新发送命令:
psync runID offset
主库获取都这个命令,到repl_backlog_buffer中获取已经落后的数据,重新发送给从库。
注意:由于repl_backlog_buffer是一个环形缓冲区,如果主库进度落后太多,导致要同步的offset都被重写覆盖掉了,那么只能重新进行全量同步了。
所以,为了保证repl_backlog_buffer有足够的空间,要设置好该缓冲区的大小。
计算公式:repl_backlog_buffer = second * write_size_per_second
- second:从服务器断开连接并重新连接到主服务器所需的平均时间;
- write_size_per_second:主库每秒平均生成的命令数据量(写命令和数据大小的总和)。
为了应对突然压力,一般要对这个计算值乘于一个适当的倍数。
更多关于集群数据复制的说明和配置,参考:https://redis.io/topics/replication[1]
2、主库挂了,怎么办?
一个Redis主从集群中,如果主库挂了?怎么办?首先,我们怎么知道主库挂了?不可能让运维人员隔一段时间去检查检查机器状况吧?
所以,我们的第一个问题是:怎么知道主库挂了?
2.1、怎么知道主库下线了?
既然不能让运维同事盯着电脑去监控主库状态,那么我们是不是可以安排一个机器人来做这件事情呢?
很庆幸,Redis中提供了一个这样的角色:哨兵(Sentinel),专门来帮忙监控主库状态。除此之外,监控到主库下线了,它还会帮忙重新选主,以及切换主库。
Redis的哨兵是一个运行在特殊模式下的Redis进程。
Redis安装目录中有一个sentinel.conf文件,里面可以进行哨兵的配置:
1
2
3
4
5
6
7 sentinel monitor itzhaimaster 192.168.0.1 6379 2 # 指定监控的master节点
sentinel down-after-milliseconds itzhaimaster 30000 # 主观下线时间,在此时间内实例不回复哨兵的PING,或者回复错误
sentinel parallel-syncs itzhaimaster 1 # 指定在发生failover主备切换时最大的同步Master从库数量,值越小,完成failover所需的时间就越长;值越大,越多的slave因为replication不可用
sentinel failover-timeout mymaster 180000 # 指定内给宕掉的Master预留的恢复时间,如果超过了这个时间,还没恢复,那哨兵认为这是一次真正的宕机。在下一次选取时会排除掉该宕掉的Master作为可用的节点,然后等待一定的设定值的毫秒数后再来检测该节点是否恢复,如果恢复就把它作为一台从库加入哨兵监测节点群,并在下一次切换时为他分配一个”选取号”。默认是3分钟
protected-mode no # 指定当前哨兵是仅限被localhost本地网络的哨兵访问,默认为yes,表示只能被部署在本地的哨兵访问,如果要设置为no,请确保实例已经通过防火墙等措施保证不会受到外网的攻击
bind #
...然后执行命令启动哨兵进程即可:
./redis-sentinel …/sentinel.conf
**注意:**如果哨兵是部署在不同的服务器上,需要确保将protected-mode设置为no,否则哨兵将不能正常工作。
为啥要哨兵集群?
即使是安排运维同事去帮忙盯着屏幕,也可能眼花看错了,走神了,或者上厕所了,或者Redis主库刚好负载比较高,需要处理一下,或者运维同事的网络不好等导致错误的认为主库下线了。同样的,我们使用哨兵也会存在这个问题。
为了避免出现这种问题,我们多安排几个哨兵,来协商确认主库是否下线了。
以下是三个哨兵组成的集群,在监控着主从Redis集群:
如上图,哨兵除了要监控Redis集群,还需要与哨兵之间交换信息。哨兵集群的监控主要有三个关键任务组成:
_sentinel_:hello
:哨兵之间每2秒通过主库上面的_sentinel_:hello
频道交换信息:- 发现其他哨兵,并建立连接;
- 交换对节点的看法,以及自身信息;
INFO
:每个哨兵每10秒向主库发送INFO命令,获取从库列表,发现从节点,确认主从关系,并与从库建立连接;ping
:每个哨兵每1秒对其他哨兵和Redis执行ping命令,判断对方在线状态。
这样,哨兵进去之间就可以开会讨论主库是不是真正的下线了。
如何确认主库是真的下线了?
当一个哨兵发现主库连不上的时候,并且超过了设置的down-after-milliseconds
主观下线时间,就会把主库标记为主观下线,这个时候还不能真正的任务主库是下线了,还需要跟其他哨兵进行沟通确认,因为,也许是自己眼花了呢?
比如哨兵2把主库标记为客观下线了,这个时候还需要跟其他哨兵进行沟通确认,如下图所示:
哨兵2判断到主库下线了,于是请求哨兵集群的其他哨兵,确认是否其他哨兵也认为主库下线了,结果收到了哨兵1的下线确认票,但是哨兵3却不清楚主库的状况。
只有哨兵2拿到的确认票数超过了quorum配置的数量之后,才可以任务是客观下线。这里哨兵2已经拿到两个确认票,quorum=2,此时,哨兵2可以把主库标识为客观下线状态了。
关于quorum:建议三个节点设置为2,超过3个节点设置为 节点数/2+1。
2.2、主库下线了,怎么办?
主库挂了,当然是要执行主从切换了,首先,我们就要选出一位哨兵来帮我们执行主从切换。
如何选举主从切换的哨兵?
一个哨兵要想被哨兵集群选举为Leader进行主从切换,必须符合两个条件:
- 拿到票数要大于等于哨兵配置文件的quorum值;
- 拿到整个集群半数以上的哨兵票数。
注意:与判断客观下线不太一样的,这里多了一个条件。
如果一轮投票下来,发现没有哨兵胜出,那么会等待一段时间之后再尝试进行投票,等待时间为:哨兵故障转移超时时间failover_timeout * 2,这样就能够争取有足够的时间让集群网络好转。
2.3、切换完主库之后,怎么办?
切换完成之后,哨兵会自动重写配置文件,让其监控新的主库。
2.4、客户端怎么知道主从正在切换?
哨兵之间可以通过主库上面的_sentinel_:hello
进行交换信息。同样的,客户端也可以从哨兵上面的各个订阅频道获取各种主从切换的信息,来判断当前集群的状态。
以下是常见的订阅频道:
- 主库下线:
+sdown <instance details>
:哨兵实例进入主观下线(Subjectively Down)状态;-sdown <instance details>
:哨兵实例退出主观下线(Objectively Down)状态;+odown <instance details>
:进入客观下线状态;-odown <instance details>
:退出客观下线状态;
- 主从切换:
failover-end <instance details>
:故障转移操作顺利完成。所有从服务器都开始复制新的主服务器了;+switch-master <master name> <oldip> <oldport> <newip> <newport>
:配置变更,主服务器的 IP 和地址已经改变。 这是绝大多数外部用户都关心的信息。
有了这些信息之后,客户端就可以知道主从切换进度,并且获取到新的主库IP,并使用新的主库IP和端口进行通信,执行写操作了。
更多关于Redis哨兵机制的说明和配置方法,参考文档:Redis Sentinel Documentation[2]
通过主从集群,保证了Redis的高可靠,但是随着Redis存储的数据量越来越多,势必会导致占用内存越来越大,内存太大产生问题:
- 重启,从磁盘恢复数据的时间会变长;
- Redis在做持久化的时候,需要fork子进程,虽然是通过写时复制进行fork,拷贝的只是页表数据,也是会导致拷贝时间变长,导致阻塞主线程,最终影响到服务的可用性。
为此,我们需要保证单个节点的数据量不能太大,于是引入了切片集群。下面就来详解介绍切片集群的实现技术。
2.5、如何避免主库脑裂导致的数据丢失问题?
我们直接看一下这个场景。
假设quorum=2,由于网络问题,两个哨兵都主观判断到master下线了,最终master被判断为客观下线:
于是进行主从切换,但是在切换期间,原master又恢复正常了,可以正常接收客户端请求,现在就有两个master都可以同时接收客户端的请求了。这个时候,集群里面会有两个master。原本只有一个大脑的分布式系统,分裂成了两个,所以称为脑裂现象。
这个时候如果有写请求到达原来的主库,新的主库就没有这部分数据了。等到主从切换完成之后,哨兵会让原来的主库会执行slave of命令,进而触发和新主库的全量同步,最终导致主从切换期间源主库接收的数据丢失了。
如何避免脑裂问题?
为了避免以上问题,最关键的就是避免客户端同时对两个主库进行写。
Redis通过配置,可以支持这样的功能:如果响应主库的消息延迟小于或等于M秒的从库的数量至少有N个,那么主从才会继续接写入。如果响应主库的消息延迟小于或等于 M 秒的从库数量少于 N 个,则主库会停止接受写入。相关配置:
- min-slaves-to-write:N 至少要连接的从库数;
- min-slaves-max-lag:M 主库向从库ping的ACK最大延迟不能超过的秒数
当然,这样不能保证避免脑裂问题。
场景1
以下场景则可以避免脑裂问题,假设从节点为一个,主观下线时间和客观下线时间相差无几,如下图:
虽然主库恢复正常之后,还在进行主从切换,由于只有一个从库,并且延迟超过了min-slaves-max-lag,所以主库被限制停止接受消息了
场景2
以下场景则不可以避免脑裂问题,假设从节点为一个,主观下线时间和客观下线时间相差无几,如下图:
虽然主库恢复正常后,还在进行主从切换,由于只有一个从库,但是延迟还没有超过min-slaves-max-lag,所以原主库可以继续接收消息,最终导致主从切换完之后,数据丢失。
也就是说,min-slaves-to-write 和 min-slaves-max-lag也不一定能够避免脑裂问题,只是降低了脑裂的风险。
进一步探讨问题的本质?
作为一个分布式系统,节点之间的数据如果要保持强一致性,那么就需要通过某种分布式一致性协调算法来实现,而Redis中没有。
而类似Zookeeper则要求大多数节点都写成功之后,才能算成功,避免脑裂导致的集群数据不一致。
注意:如果只有一个从库,设置min-slaves-to-write=1有一定的风险,如果从库因为某些原因需要暂停服务,那么主库也就没法提供服务了。如果是手工运维导致需要暂停从库,那么可以先开启另一台从库。