Redis

洞悉Redis技术内幕:缓存,数据结构,并发,集群与算法
帅旋
关注
充电
IT宅站长,技术博主,共享单车手,全网id:arthinking。

Redis主从集群是如何实现的?

发布于 2021-06-16 | 更新于 2024-03-03

Redis搭建主从集群还是比较简单的,只需要通过

replicaof 主库IP 主库端口

命令就可以了。如下图,我们分别在101和102机器上执行命令:

1
replicaof 192.168.0.1 6379

我们就构建了一个主从集群:

image-20211010133718773

为了保证主从数据的一致性,主从节点采用读写分离的方式:主库负责写,同步给从库,主从均可负责读。

那么,我们搭建好了主从集群之后,他们是如何实现数据同步的呢?

1、主从集群数据如何同步?

当从库和主库第一次做数据同步的时候,会先进行全量数据复制,大致流程如下图所示:

image-20211010133812778

  • 从库执行replicaof命令,与主库建立连接,发送psync命令,请求同步,刚开始从库不认识主库,也还不知道他的故事,所以说了一句:那谁,请开始讲你的故事(psync ? -1);
  • 主库收到从库的打招呼,于是告诉了从库它的名字(runId),要给他讲整个人生的经历(FULLRESYNC,全量复制),以及讲故事的进度(offset);
  • 主库努力回忆自己的故事(通过BGSAVE命令,生成RDB文件);
  • 主库开始跟从库讲故事(发送RDB文件);
  • 从库摒弃主观偏见,重新开始从主库的故事中认识了解主库;
  • 主库讲故事过程中,又有新的故事发生了(收快递),于是继续讲给从库听(repl buffer)。

1.1、主从复制过程中,新产生的改动如何同步?

从上面的流程图中,我们也可以知道,主库会在复制过程中,把新的修改操作追加到replication buffer中,当发送完RDB文件后,再把replication buffer中的修改操作继续发送给从库:

1.2、从库太多了,主库如何高效同步?

当我们要同步的从库越多,主库就会执行多次BGSAVE进行数据全量同步,为了减小主库的压力,可以做如下的集群配置,通过选择一个硬件配置比较高的从库作为其他从库的同步源,把压力分担给从库:

image-20211010133844788

1.3、首次同步完成之后,如何继续同步增量数据?

主从同步完成之后,主库会继续使用当前创建的网络长连接,以命令传播的方式进行同步增量数据。

1.4、主从断开了,如何继续同步?

当主从断开重连之后,从库会采用增量复制的方式进行同步。

为了实现增量复制,需要借助于repl_backlog_buffer缓冲区。

如下图:

image-20211010133920683

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集群:

image-20211010134052596

如上图,哨兵除了要监控Redis集群,还需要与哨兵之间交换信息。哨兵集群的监控主要有三个关键任务组成:

  • _sentinel_:hello:哨兵之间每2秒通过主库上面的_sentinel_:hello频道交换信息:
    • 发现其他哨兵,并建立连接;
    • 交换对节点的看法,以及自身信息;
  • INFO:每个哨兵每10秒向主库发送INFO命令,获取从库列表,发现从节点,确认主从关系,并与从库建立连接;
  • ping:每个哨兵每1秒对其他哨兵和Redis执行ping命令,判断对方在线状态。

这样,哨兵进去之间就可以开会讨论主库是不是真正的下线了。

如何确认主库是真的下线了?

当一个哨兵发现主库连不上的时候,并且超过了设置的down-after-milliseconds主观下线时间,就会把主库标记为主观下线,这个时候还不能真正的任务主库是下线了,还需要跟其他哨兵进行沟通确认,因为,也许是自己眼花了呢?

比如哨兵2把主库标记为客观下线了,这个时候还需要跟其他哨兵进行沟通确认,如下图所示:

image-20211010134122455

哨兵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被判断为客观下线:

image-20211010134220501

于是进行主从切换,但是在切换期间,原master又恢复正常了,可以正常接收客户端请求,现在就有两个master都可以同时接收客户端的请求了。这个时候,集群里面会有两个master。原本只有一个大脑的分布式系统,分裂成了两个,所以称为脑裂现象。

这个时候如果有写请求到达原来的主库,新的主库就没有这部分数据了。等到主从切换完成之后,哨兵会让原来的主库会执行slave of命令,进而触发和新主库的全量同步,最终导致主从切换期间源主库接收的数据丢失了。

如何避免脑裂问题?

为了避免以上问题,最关键的就是避免客户端同时对两个主库进行写。

Redis通过配置,可以支持这样的功能:如果响应主库的消息延迟小于或等于M秒的从库的数量至少有N个,那么主从才会继续接写入。如果响应主库的消息延迟小于或等于 M 秒的从库数量少于 N 个,则主库会停止接受写入。相关配置:

  • min-slaves-to-write:N 至少要连接的从库数;
  • min-slaves-max-lag:M 主库向从库ping的ACK最大延迟不能超过的秒数

当然,这样不能保证避免脑裂问题。

场景1

以下场景则可以避免脑裂问题,假设从节点为一个,主观下线时间和客观下线时间相差无几,如下图:

image-20211010134256953

虽然主库恢复正常之后,还在进行主从切换,由于只有一个从库,并且延迟超过了min-slaves-max-lag,所以主库被限制停止接受消息了

场景2

以下场景则不可以避免脑裂问题,假设从节点为一个,主观下线时间和客观下线时间相差无几,如下图:

image-20211010134328393

虽然主库恢复正常后,还在进行主从切换,由于只有一个从库,但是延迟还没有超过min-slaves-max-lag,所以原主库可以继续接收消息,最终导致主从切换完之后,数据丢失。

也就是说,min-slaves-to-write 和 min-slaves-max-lag也不一定能够避免脑裂问题,只是降低了脑裂的风险。

进一步探讨问题的本质?

作为一个分布式系统,节点之间的数据如果要保持强一致性,那么就需要通过某种分布式一致性协调算法来实现,而Redis中没有。

而类似Zookeeper则要求大多数节点都写成功之后,才能算成功,避免脑裂导致的集群数据不一致。

注意:如果只有一个从库,设置min-slaves-to-write=1有一定的风险,如果从库因为某些原因需要暂停服务,那么主库也就没法提供服务了。如果是手工运维导致需要暂停从库,那么可以先开启另一台从库。

References


  1. Replication. Retrieved from https://redis.io/topics/replication ↩︎

  2. Redis Sentinel Documentation. Retrieved from https://redis.io/topics/sentinel ↩︎

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/redis/master-slave-cluster.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。