码哥字节

码哥字节 查看完整档案

深圳编辑安庆大学  |  电子信息科学与技术 编辑公众号  |  码哥字节 编辑 segmentfault.com/u/magebyte 编辑
编辑

公众号-码哥字节

一个有品位的博主,不跟风不扯淡,助力程序员成长。

一线大厂互联网工作经验,左手微服务、右手中间件、前脚高并发、后脚分布式。

个人动态

码哥字节 发布了文章 · 4月2日

Redis 高可用篇:你管这叫 Sentinel 哨兵集群原理

概要

我们知道「主从复制是高可用的基石」,从库宕机依然可以将请求发送给主库或者其他从库,但是 Master 宕机,只能响应读操作,写请求无法再执行。

所以主从复制架构面临一个严峻问题,主库挂了,无法执行「写操作」,无法自动选择一个 Slave 切换为 Master,也就是无法故障自动切换

深夜与女朋友么么哒……(此处省略 10000 字),突然宕机,总不能提起裤子从床上爬起来手工进行主从切换,再通知其他程序员把地址重新改成新主库上线

如此一折腾自己已被女友切换成前男友了,万万使不得。所以我们必须有一个高可用的方案,为此,Redis 官方提供一个高可用方案——哨兵(Sentinel)

Redis 哨兵集群原理

开篇寄语

技术的迭代非常的快,但是从技术中沉淀下来的思维却是受益终生的。所以不要担心什么中年危机,那些担心中年危机的人通常很难成长起来。只要我们成长,只要我们的认知在不断突破,就不用担心中年危机,这个世界始终是需要那些优秀人才的。

什么是哨兵(Sentinel)

65 哥:码哥,虽然我没女朋友,但是,未雨绸缪我要掌握这个哨兵模式,防止当深夜与女朋友么么哒被打扰,你快说说哨兵的实现原理吧。

搭建实例采用三个哨兵形成集群,三个数据节点(一主两从)方式搭建,如下图所示:

Redis哨兵集群

哨兵集群的搭建演示就不在这里赘述了,需要的读者朋友可点击左下角的「阅读原文」查看。

65 哥你听过「武当派」创始人张三疯么?Redis 主从架构就好比一个武当,掌门人就是 Master。掌门人如果挂了,需要从武当七侠里面选举能人担当掌门人。这就需要一个部门能监控掌门人的生死和武当其他弟子的生命状态,并且能够通过投票从武当弟子中选举一个能者担任新掌门,接着再举行新闻发布会向世界宣布新掌门的信息。这个「部门」就是哨兵。

哨兵在选举新掌门会遇到以下几个问题:

  1. 如何判断掌门真的挂了,有可能假死;
  2. 到底选择哪一个武当子弟作为新掌门?
  3. 通过新闻发布会将新掌门的相关信息通知到所有武当弟子(slave 和 master)和整个武林(客户端)

哨兵部门主要负责的任务是:监控整个武当、选择新掌门,通知整个武当和整个武林。

哨兵机制的主要任务

哨兵是 Redis 的一种运行模式,它专注于对 Redis 实例(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主及主从切换,实现故障转移,确保整个 Redis 系统的可用性。结合 Redis 的 官方文档,可以知道 Redis 哨兵具备的能力有如下几个:

  • 监控:持续监控 master 、slave 是否处于预期工作状态。
  • 自动切换主库:当 Master 运行故障,哨兵启动自动故障恢复流程:从 slave 中选择一台作为新 master。
  • 通知:让 slave 执行 replicaof ,与新的 master 同步;并且通知客户端与新 master 建立连接。

哨兵也是一个 Redis 进程,只是不对外提供读写服务,通常哨兵要配置成单数,为啥呢?且听「码哥字节」慢慢分析。

65 哥:那到底「哨兵」这个神秘部门是如何实现这三个能力的?

我们先从全局观看哨兵,简要的了解整个运作流程,接着再针对每一个任务详细分析。首先从监控开始…...

监控

Sentinel 只是武当弟子中的特殊部门,在默认情况下,Sentinel 通过飞鸽传书以每秒一次的频率向所有武当弟子、掌门与哨兵(包括 Master、Slave、其他 Sentinel 在内)发送 PING 命令,如果 slave 没有在在规定时间内响应「哨兵」的 PING 命令,「哨兵」就认为这哥们可能嗝屁了,就会将他记录为「下线状态」;

假如 master 掌门没有在规定时间响应 「哨兵」的 PING 命令,哨兵就判定掌门下线,开始执行「自动切换 master 掌门」的流程。

PING 命令的回复有两种情况:

  1. 有效回复:返回 +PONG、-LOADING、-MASTERDOWN 任何一种;
  2. 无效回复:有效回复之外的回复,或者再指定时间内返回任何回复。
65 哥:哨兵如何判断「掌门」嗝屁呢?掌门诈尸咋办?

为了防止掌门「假死」,「哨兵」设计了「主观下线」和「客观下线」两种暗号。

主观下线

哨兵利用 PING 命令来检测掌门、 slave 的生命状态。如果是无效回复,哨兵就把这个哥们标记为「主观下线」。检测到的是武当小弟,也就是 slave 角色。那么就直接标记「主观下线」。

因为 master 掌门还在,slave 的嗝屁对整个武当影响不大。依然可以对外开会,比武论剑、吃香喝辣…...

如果检测到是 master 掌门完蛋,这时候哨兵不能这么简单的标记「主观下线」,开启新掌门选举。

因为有可能出现误判,掌门并没有嗝屁,一旦启动了掌门切换,后续的选主、通知开发布会,slave 花时间与新 master 同步数据都会消耗大量资源。

所以「哨兵」要降低误判的概率,误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下。

既然一个人容易误判,那就多个人一起投票判断。哨兵机制也是类似的,采用多实例组成的集群模式进行部署,这就是哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。

同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。

客观下线

判断 master 是否下线不能只有一个「哨兵」说了算,只有过半的哨兵判断 master 已经「主观下线」,这时候才能将 master 标记为「客观下线」,也就是说这是一个客观事实,掌门真的嗝屁了,华佗再世也治不好了。

只有 master 被判定为「客观下线」,才会进一步触发哨兵开始主从切换流程。

客观下线

主观下线与客观下线的区别

简单来说,主观下线是哨兵自己认为节点宕机,而客观下线是不但哨兵自己认为节点宕机,而且该哨兵与其他哨兵沟通后,达到一定数量的哨兵都认为该哥们嗝屁了。

这里的「一定数量」是一个法定数量(Quorum),是由哨兵监控配置决定的,解释一下该配置:

 # sentinel monitor <master-name> <master-host> <master-port> <quorum>
 # 举例如下:
 sentinel monitor mymaster 127.0.0.1 6379 2

这条配置项用于告知哨兵需要监听的主节点:

  • sentinel monitor:代表监控。
  • mymaster:代表主节点的名称,可以自定义。
  • 192.168.11.128:代表监控的主节点 ip,6379 代表端口。
  • 2:法定数量,代表只有两个或两个以上的哨兵认为主节点不可用的时候,才会把 master 设置为客观下线状态,然后进行 failover 操作。

「客观下线」的标准就是,当有 N 个哨兵实例时,要有 N/2 + 1 个实例判断 master 为「主观下线」,才能最终判定 Master 为「客观下线」,其实就是过半机制。

自动切换主库

65 哥:既然判断 master 客观下线了,那就要从选出一个新掌门人了吧。

「哨兵」的第二个任务,选择新 master 掌门。需要从武当弟子中按照一定规则选择一个牛逼人物作为新掌门,完成选任掌门后,新 master 带领众弟子一起吃香喝辣。

按照一定的「筛选条件」 + 「打分」策略,选出「最强王者」担任掌门,也就是通过一些条件海选过滤一些「无能之辈」,接着将通过海选的靓仔全都打分排名,将最高者选为新 master。

如图所示:

新master选择

网络经常断开的靓仔也不可取,你想,即使变成 master,可是很快网络出了故障,又得重新选择新 master,这不闹着玩么,得排除掉!

筛选条件

65 哥:那都有哪些筛选条件呀?
  • 从库当前在线状态,下线的直接丢弃;
  • 评估之前的网络连接状态 down-after-milliseconds * 10:如果从库总是和主库断连,而且断连次数超出了一定的阈值(10 次),我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了。

打分

过滤掉不合适的 slave 之后,则进入打分环节。打分会按照三个规则进行三轮打分,规则分别为:

  1. slave 优先级,通过 slave-priority 配置项,给不同的从库设置不同优先级(后台有人没办法),优先级高的直接晋级为新 master 掌门。
  2. slave_repl_offsetmaster_repl_offset进度差距(谁的武功与之前掌门的功夫越接近谁就更牛逼),如果都一样,那就继续下一个规则。其实就是比较 slave 与旧 master 复制进度的差距;
  3. slave runID,在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。 (论资排辈,根据 runID 的创建时间来判断,时间早的上位);

通知

65 哥:为哈还要召开新闻发布会呢?

重新选举新 master 掌门这种事情,何等大事,怎能不告知天下。再者其他 slave 弟子也要知道新掌门是谁,一起追随新掌门吃香喝辣大保健。

最后一个任务,「哨兵」将新 「master 掌门」的连接信息发送给其他 slave 武当弟子,并且让 slave 执行 replacaof 命令,和新「master 掌门」建立连接,并进行数据复制学习新掌门的所有武功。

除此之外,「哨兵」还需要将新掌门的连接信息通知整个武林(客户端),使得让所有想拜访、讨教的人能找到新任掌门,这样诸多事宜才能交给新掌门做决定(将读写请求转移到新 master)。

哨兵的主要任务与实现目标

哨兵执行任务与目标

哨兵集群工作原理

「哨兵」部门并不是一个人,多个人共同组成一个「哨兵集群」,即使有一些「哨兵」被老王打死了,其他的「哨兵」依然可以共同协作完成监控、新掌门选举以及通知 slave 、master 以及每一个武林人士(客户端)。

在配置哨兵集群的时候,哨兵配置中只设置了监控的 master IP 和 port,并没有配置其他哨兵的连接信息。

 sentinel monitor <master-name> <ip> <redis-port> <quorum>

哨兵之间是如何知道彼此的?如何知道 slave 并监控他们的?由哪一个「哨兵」执行主从切换呢?

带着这些问题,跟着「码哥字节」一起追本溯源,深入哨兵集群心脏。

pub/sub 实现哨兵间通信和发现 slave

65 哥:哨兵之间是如何知道彼此的?

哨兵之间可以相互通信约会搞事情,主要归功于 Redis 的 pub/sub 发布/订阅机制。

哨兵与 master 建立通信,利用 master 提供发布/订阅机制发布自己的信息,比如身高体重、是否单身、IP、端口……

master 有一个 __sentinel__:hello 的专用通道,用于哨兵之间发布和订阅消息。这就好比是 __sentinel__:hello 微信群,哨兵利用 master 建立的微信群发布自己的消息,同时关注其他哨兵发布的消息

Redis pub/sub 机制

当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口,从而相互发现建立连接。

Redis 通过频道的方式对消息进行分别管理,这里的频道其实就是不同的微信群。比如“码哥字节读者技术群”就是专门分享技术的群。朋友们可以关注公众号,后台回复“加群”,一起成长。

65 哥:哨兵之间虽然建立连接了,但是还需要和 slave 建立连接,不然没法监控他们呀,如何知道 slave 并监控他们的?

的确,哨兵之间建立连接形成集群还不够,还需要跟 slave 建立连接,不然没法监控他们,无法对主从库进行心跳判断。

除此之外,如果发生了主从切换也得通知 slave 重新跟新 master 建立连接执行数据同步。关于主从架构数据同步原理可移步《Redis 高可用篇:你管这叫主从架构数据一致性同步》。

关键还是利用 master 来实现,哨兵向 master 发送 INFO 命令, master 掌门自然是知道自己门下所有的 salve 小弟的。所以 master 接收到命令后,便将 slave 列表告诉哨兵。

哨兵根据 master 响应的 slave 名单信息与每一个 salve 建立连接,并且根据这个连接持续监控哨兵。

如图所示,哨兵 2 向 Master 发送 INFO 命令,Master 就把 slave 列表返回给哨兵 2,哨兵 2 便根据 slave 列表连接信息与每一个 slave 建立连接,并基于此连接实现持续监控。

剩下的哨兵也同理基于此实现监控。

INFO命令获取slave信息

选择哨兵执行主从切换

65 哥:master 嗝屁了以后,哨兵这么多,那到底让哪一个哨兵来执行新 master 切换呢?

这个跟哨兵判断 master “客观下线”类似,也是通过投票的方式选出来的。

任何一个哨兵判断 master “主观下线”后,就会给其他哨兵基友发送 is-master-down-by-addr 命令,好基友则根据自己跟 master 之间的连接状况分别响应 Y 或者 NY 表示赞成票, N 就是反对。

如果某个哨兵获得了大多数哨兵的“赞成票”之后,就可以标记 master 为 “客观下线”,赞成票数是通过哨兵配置文件中的 quorum 配置项设定。

 sentinel monitor <master-name> <ip> <redis-port> <quorum>

比如一共 3 个哨兵组成集群,那么 quorum 就可以配置成 2,当一个哨兵获得了 2 张赞成票,就可以标记 master “客观下线”,当然这个票包含自己的那一票。

获得多数赞成票的哨兵可以向其他哨兵发送命令,申明自己想要执行主从切换。并让其他哨兵进行投票,投票过程就叫做 “Leader 选举”。

想要成为 “Leader”没那么简单,得有两把刷子。需要满足以下条件:

  1. 获得其他哨兵基友过半的赞成票;
  2. 赞成票的数量还要大于等于配置文件的 quorum 的值。

如果哨兵集群有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。因此,通常我们至少会配置 3 个哨兵实例。

这也是为啥哨兵集群部署成单数的原因,双数的话多余浪费。

选举流程如下图所示:

Redis哨兵执行主从切换

通过 pub/sub 实现客户端事件通知

65 哥:新 master 选出来了,要怎么公示天下呢?

当然是召开新闻发布会呀,邀请消息相关类型的媒体报道传播,感兴趣的人自然就去关注订阅相关事件,并根据事件做出行动。

在 Redis 也是类似,通过 pub/sub 机制发布不同事件,让客户端在这里订阅消息。客户端可以订阅哨兵的消息,哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。

也就是在不同的“微信群”发布不同的事件,让对该事件感兴趣的人进群即可。

master 下线事件

  • +sdown:进入“主观下线”状态;
  • -sdown:退出“主观下线”状态;
  • +odown:进入“客观下线”状态;
  • -odown:退出“客观下线”状态;

slave 重新配置事件

  • +slave-reconf-sent:哨兵发送 replicaof 命令重新配置从库;
  • +slave-reconf-inprog:slave 配置了新 master,但是尚未进行同步;
  • +slave-reconf-done:slave 配置了新 master,并与新 master 完成了数据同步;

新主库切换

+switch-master:master 地址发生了变化。

知道了这些频道之后,就可以让客户端从哨兵这里订阅消息了。客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。

然后,我们可以在客户端执行订阅命令,来获取不同的事件消息。

举个栗子:如下指令订阅“所有实例进入客观下线状态的事件”

 SUBSCRIBE +odown

注意事项与配置说明

发现了没,Redis 的 pub/sub 发布订阅机制尤其重要,有了 pub/sub 机制,哨兵和哨兵之间、哨兵和从库之间、哨兵和客户端之间就都能建立起连接了,各种事件的发布也是通过这个机制实现。

down-after-milliseconds

Sentinel 配置文件中的 down-after-milliseconds 选项指定了 Sentinel 判断实例进入主观下线所需的时间长度:如果一个实例在 down-after-milliseconds 毫秒内,连续向 Sentinel 返回无效回复,那么 Sentinel 会修改这个实例所对应数据,以此来表示这个实例已经进入主观下线状态。

要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds。因为这个值在不同的哨兵实例上配置不一致,导致哨兵集群一直没有对有故障的主库形成共识,也就没有及时切换主库,最终的结果就是集群服务不稳定

down-after-milliseconds * 10

down-after-milliseconds 是我们认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。

总结

哨兵主要任务

Redis 哨兵机制是实现 Redis 不间断服务的高可用手段之一。主从架构集群的数据同步,是数据可靠的基础保障;主库宕机,自动执行主从切换是服务不间断的关键支撑。

Redis 哨兵机制实现了主从库的自动切换,再也不怕跟女盆友么么哒的时候 master 宕机了:

  • 监控 master 与 slave 运行状态,判断是否客观下线;
  • master 客观下线后,选择一个 slave 切换成 master;
  • 通知 slave 和客户端新 master 信息。

哨兵集群原理

为了避免单个哨兵故障后无法进行主从切换,以及为了减少误判率,又引入了哨兵集群;哨兵集群又需要有一些机制来支撑它的正常运行:

  • 基于 pub/sub 机制实现哨兵集群之间的通信;
  • 基于 INFO 命令获取 slave 列表,帮助 哨兵与 slave 建立连接;
  • 通过哨兵的 pub/sub,实现了与客户端和哨兵之间的事件通知。

主从切换,并不是随意选择一个哨兵就可以执行,而是通过投票仲裁,选择一个 Leader,由这个 Leader 负责主从切换。

一起交流

下一篇「码哥字节」将带来 《Redis 高可用篇:Cluster 集群,是兄弟就跟我一起扛》,关注我,获取真正的硬核知识点。

另外技术读者群也开通了,后台回复「加群」获取「码哥字节」作者微信交流或者提出建议,一起成长交流。群里有 N 多大厂的大佬,也可内推哦。

以上就是 Redis 哨兵集群原理详解,觉得不错请点赞、分享,「码哥字节」感激不尽。

码哥字节

参考资料

查看原文

赞 10 收藏 4 评论 2

码哥字节 发布了文章 · 3月24日

Kafka 性能篇:为何 Kafka 这么快?

『码哥』的 Redis 系列文章有一篇讲透了 Redis 的性能优化 ——《Redis 核心篇:唯快不破的秘密》。深入地从 IO、线程、数据结构、编码等方面剖析了 Redis “快”的内部秘密。65 哥深受启发,在学习 Kafka 的过程中,发现 Kafka 也是一个性能十分优秀的中间件,遂要求『码哥』讲一讲 Kafka 性能优化方面的知识,所以『码哥』决定将这篇性能方面的博文作为 Kafka 系列的开篇之作。

先预告一下 Kafka 系列文章,大家敬请期待哦:

以讲解性能作为 Kafka 之旅的开篇之作,让我们一起来深入了解 Kafka “快”的内部秘密。你不仅可以学习到 Kafka 性能优化的各种手段,也可以提炼出各种性能优化的方法论,这些方法论也可以应用到我们自己的项目之中,助力我们写出高性能的项目。

关公战秦琼

65: Redis 和 Kafka 完全是不同作用的中间件,有比较性吗?

是的,所以此文讲的不是《分布式缓存的选型》,也不是《分布式中间件对比》。我们聚焦于这两个不同领域的项目对性能的优化,看一看优秀项目对性能优化的通用手段,以及在针对不同场景下的特色的优化方式。

很多人学习了很多东西,了解了很多框架,但在遇到实际问题时,却常常会感觉到知识不足。这就是没有将学习到的知识体系化,没有从具体的实现中抽象出可以行之有效的方法论

学习开源项目很重要的一点就是归纳,将不同项目的优秀实现总结出方法论,然后演绎到自我的实践中去。

码哥寄语

理性、客观、谨慎是程序员的特点,也是优点,但是很多时候我们也需要带一点感性,带一点冲动,这个时候可以帮助我们更快的做决策。「悲观者正确、乐观者成功。」希望大家都是一个乐观地解决问题的人。

Kafka 性能全景

从高度抽象的角度来看,性能问题逃不出下面三个方面:

  • 网络
  • 磁盘
  • 复杂度

对于 Kafka 这种网络分布式队列来说,网络和磁盘更是优化的重中之重。针对于上面提出的抽象问题,解决方案高度抽象出来也很简单:

  • 并发
  • 压缩
  • 批量
  • 缓存
  • 算法

知道了问题和思路,我们再来看看,在 Kafka 中,有哪些角色,而这些角色就是可以优化的点:

  • Producer
  • Broker
  • Consumer

是的,所有的问题,思路,优化点都已经列出来了,我们可以尽可能的细化,三个方向都可以细化,如此,所有的实现便一目了然,即使不看 Kafka 的实现,我们自己也可以想到一二点可以优化的地方。

这就是思考方式。提出问题 > 列出问题点 > 列出优化方法 > 列出具体可切入的点 > tradeoff和细化实现

现在,你也可以尝试自己想一想优化的点和方法,不用尽善尽美,不用管好不好实现,想一点是一点。

65 哥:不行啊,我很笨,也很懒,你还是直接和我说吧,我白嫖比较行。

顺序写

65 哥:人家 Redis 是基于纯内存的系统,你 kafka 还要读写磁盘,能比?

为什么说写磁盘慢?

我们不能只知道结论,而不知其所以然。要回答这个问题,就得回到在校时我们学的操作系统课程了。65 哥还留着课本吗?来,翻到讲磁盘的章节,让我们回顾一下磁盘的运行原理。

65 哥:鬼还留着哦,课程还没上到一半书就没了。要不是考试俺眼神好,估计现在还没毕业。

看经典大图:

完成一次磁盘 IO,需要经过寻道旋转数据传输三个步骤。

影响磁盘 IO 性能的因素也就发生在上面三个步骤上,因此主要花费的时间就是:

  1. 寻道时间:Tseek 是指将读写磁头移动至正确的磁道上所需要的时间。寻道时间越短,I/O 操作越快,目前磁盘的平均寻道时间一般在 3-15ms。
  2. 旋转延迟:Trotation 是指盘片旋转将请求数据所在的扇区移动到读写磁盘下方所需要的时间。旋转延迟取决于磁盘转速,通常用磁盘旋转一周所需时间的 1/2 表示。比如:7200rpm 的磁盘平均旋转延迟大约为 60*1000/7200/2 = 4.17ms,而转速为 15000rpm 的磁盘其平均旋转延迟为 2ms。
  3. 数据传输时间:Ttransfer 是指完成传输所请求的数据所需要的时间,它取决于数据传输率,其值等于数据大小除以数据传输率。目前 IDE/ATA 能达到 133MB/s,SATA II 可达到 300MB/s 的接口数据传输率,数据传输时间通常远小于前两部分消耗时间。简单计算时可忽略。

因此,如果在写磁盘的时候省去寻道旋转可以极大地提高磁盘读写的性能。

Kafka 采用顺序写文件的方式来提高磁盘写入性能。顺序写文件,基本减少了磁盘寻道旋转的次数。磁头再也不用在磁道上乱舞了,而是一路向前飞速前行。

Kafka 中每个分区是一个有序的,不可变的消息序列,新的消息不断追加到 Partition 的末尾,在 Kafka 中 Partition 只是一个逻辑概念,Kafka 将 Partition 划分为多个 Segment,每个 Segment 对应一个物理文件,Kafka 对 segment 文件追加写,这就是顺序写文件。

65 哥:为什么 Kafka 可以使用追加写的方式呢?

这和 Kafka 的性质有关,我们来看看 Kafka 和 Redis,说白了,Kafka 就是一个Queue,而 Redis 就是一个HashMapQueueMap的区别是什么?

Queue 是 FIFO 的,数据是有序的;HashMap数据是无序的,是随机读写的。Kafka 的不可变性,有序性使得 Kafka 可以使用追加写的方式写文件。

其实很多符合以上特性的数据系统,都可以采用追加写的方式来优化磁盘性能。典型的有Redis的 AOF 文件,各种数据库的WAL(Write ahead log)机制等等。

所以清楚明白自身业务的特点,就可以针对性地做出优化。

零拷贝

65 哥:哈哈,这个我面试被问到过。可惜答得一般般,唉。

什么是零拷贝?

我们从 Kafka 的场景来看,Kafka Consumer 消费存储在 Broker 磁盘的数据,从读取 Broker 磁盘到网络传输给 Consumer,期间涉及哪些系统交互。Kafka Consumer 从 Broker 消费数据,Broker 读取 Log,就使用了 sendfile。如果使用传统的 IO 模型,伪代码逻辑就如下所示:

 readFile(buffer)
 send(buffer)

如图,如果采用传统的 IO 流程,先读取网络 IO,再写入磁盘 IO,实际需要将数据 Copy 四次。

  1. 第一次:读取磁盘文件到操作系统内核缓冲区;
  2. 第二次:将内核缓冲区的数据,copy 到应用程序的 buffer;
  3. 第三步:将应用程序 buffer 中的数据,copy 到 socket 网络发送缓冲区;
  4. 第四次:将 socket buffer 的数据,copy 到网卡,由网卡进行网络传输。
65 哥:啊,操作系统这么傻吗?copy 来 copy 去的。

并不是操作系统傻,操作系统的设计就是每个应用程序都有自己的用户内存,用户内存和内核内存隔离,这是为了程序和系统安全考虑,否则的话每个应用程序内存满天飞,随意读写那还得了。

不过,还有零拷贝技术,英文——Zero-Copy零拷贝就是尽量去减少上面数据的拷贝次数,从而减少拷贝的 CPU 开销,减少用户态内核态的上下文切换次数,从而优化数据传输的性能。

常见的零拷贝思路主要有三种:

  • 直接 I/O:数据直接跨过内核,在用户地址空间与 I/O 设备之间传递,内核只是进行必要的虚拟存储配置等辅助工作;
  • 避免内核和用户空间之间的数据拷贝:当应用程序不需要对数据进行访问时,则可以避免将数据从内核空间拷贝到用户空间;
  • 写时复制:数据不需要提前拷贝,而是当需要修改的时候再进行部分拷贝。

Kafka 使用到了 mmapsendfile 的方式来实现零拷贝。分别对应 Java 的 MappedByteBufferFileChannel.transferTo

使用 Java NIO 实现零拷贝,如下:

 FileChannel.transferTo()

在此模型下,上下文切换的数量减少到一个。具体而言,transferTo()方法指示块设备通过 DMA 引擎将数据读取到读取缓冲区中。然后,将该缓冲区复制到另一个内核缓冲区以暂存到套接字。最后,套接字缓冲区通过 DMA 复制到 NIC 缓冲区。

我们将副本数从四减少到三,并且这些副本中只有一个涉及 CPU。 我们还将上下文切换的数量从四个减少到了两个。这是一个很大的改进,但是还没有查询零副本。当运行 Linux 内核 2.4 及更高版本以及支持收集操作的网络接口卡时,后者可以作为进一步的优化来实现。如下所示。

根据前面的示例,调用transferTo()方法会使设备通过 DMA 引擎将数据读取到内核读取缓冲区中。但是,使用gather操作时,读取缓冲区和套接字缓冲区之间没有复制。取而代之的是,给 NIC 一个指向读取缓冲区的指针以及偏移量和长度,该偏移量和长度由 DMA 清除。CPU 绝对不参与复制缓冲区。

关于零拷贝详情,可以详读这篇文章零拷贝 (Zero-copy) 浅析及其应用

PageCache

producer 生产消息到 Broker 时,Broker 会使用 pwrite() 系统调用【对应到 Java NIO 的 FileChannel.write() API】按偏移量写入数据,此时数据都会先写入page cache。consumer 消费消息时,Broker 使用 sendfile() 系统调用【对应 FileChannel.transferTo() API】,零拷贝地将数据从 page cache 传输到 broker 的 Socket buffer,再通过网络传输。

leader 与 follower 之间的同步,与上面 consumer 消费数据的过程是同理的。

page cache中的数据会随着内核中 flusher 线程的调度以及对 sync()/fsync() 的调用写回到磁盘,就算进程崩溃,也不用担心数据丢失。另外,如果 consumer 要消费的消息不在page cache里,才会去磁盘读取,并且会顺便预读出一些相邻的块放入 page cache,以方便下一次读取。

因此如果 Kafka producer 的生产速率与 consumer 的消费速率相差不大,那么就能几乎只靠对 broker page cache 的读写完成整个生产 - 消费过程,磁盘访问非常少。

网络模型

65 哥:网络嘛,作为 Java 程序员,自然是 Netty

是的,Netty 是 JVM 领域一个优秀的网络框架,提供了高性能的网络服务。大多数 Java 程序员提到网络框架,首先想到的就是 Netty。Dubbo、Avro-RPC 等等优秀的框架都使用 Netty 作为底层的网络通信框架。

Kafka 自己实现了网络模型做 RPC。底层基于 Java NIO,采用和 Netty 一样的 Reactor 线程模型。

Reacotr 模型主要分为三个角色

  • Reactor:把 IO 事件分配给对应的 handler 处理
  • Acceptor:处理客户端连接事件
  • Handler:处理非阻塞的任务

在传统阻塞 IO 模型中,每个连接都需要独立线程处理,当并发数大时,创建线程数多,占用资源;采用阻塞 IO 模型,连接建立后,若当前线程没有数据可读,线程会阻塞在读操作上,造成资源浪费

针对传统阻塞 IO 模型的两个问题,Reactor 模型基于池化思想,避免为每个连接创建线程,连接完成后将业务处理交给线程池处理;基于 IO 复用模型,多个连接共用同一个阻塞对象,不用等待所有的连接。遍历到有新数据可以处理时,操作系统会通知程序,线程跳出阻塞状态,进行业务逻辑处理

Kafka 即基于 Reactor 模型实现了多路复用和处理线程池。其设计如下:

其中包含了一个Acceptor线程,用于处理新的连接,Acceptor 有 N 个 Processor 线程 select 和 read socket 请求,N 个 Handler 线程处理请求并相应,即处理业务逻辑。

I/O 多路复用可以通过把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销。

总结: Kafka Broker 的 KafkaServer 设计是一个优秀的网络架构,有想了解 Java 网络编程,或需要使用到这方面技术的同学不妨去读一读源码。后续『码哥』的 Kafka 系列文章也将涉及这块源码的解读。

批量与压缩

Kafka Producer 向 Broker 发送消息不是一条消息一条消息的发送。使用过 Kafka 的同学应该知道,Producer 有两个重要的参数:batch.sizelinger.ms。这两个参数就和 Producer 的批量发送有关。

Kafka Producer 的执行流程如下图所示:

发送消息依次经过以下处理器:

  • Serialize:键和值都根据传递的序列化器进行序列化。优秀的序列化方式可以提高网络传输的效率。
  • Partition:决定将消息写入主题的哪个分区,默认情况下遵循 murmur2 算法。自定义分区程序也可以传递给生产者,以控制应将消息写入哪个分区。
  • Compress:默认情况下,在 Kafka 生产者中不启用压缩.Compression 不仅可以更快地从生产者传输到代理,还可以在复制过程中进行更快的传输。压缩有助于提高吞吐量,降低延迟并提高磁盘利用率。
  • Accumulate:Accumulate顾名思义,就是一个消息累计器。其内部为每个 Partition 维护一个Deque双端队列,队列保存将要发送的批次数据,Accumulate将数据累计到一定数量,或者在一定过期时间内,便将数据以批次的方式发送出去。记录被累积在主题每个分区的缓冲区中。根据生产者批次大小属性将记录分组。主题中的每个分区都有一个单独的累加器 / 缓冲区。
  • Group Send:记录累积器中分区的批次按将它们发送到的代理分组。 批处理中的记录基于 batch.size 和 linger.ms 属性发送到代理。 记录由生产者根据两个条件发送。 当达到定义的批次大小或达到定义的延迟时间时。

Kafka 支持多种压缩算法:lz4、snappy、gzip。Kafka 2.1.0 正式支持 ZStandard —— ZStandard 是 Facebook 开源的压缩算法,旨在提供超高的压缩比 (compression ratio),具体细节参见 zstd

Producer、Broker 和 Consumer 使用相同的压缩算法,在 producer 向 Broker 写入数据,Consumer 向 Broker 读取数据时甚至可以不用解压缩,最终在 Consumer Poll 到消息时才解压,这样节省了大量的网络和磁盘开销。

分区并发

Kafka 的 Topic 可以分成多个 Partition,每个 Paritition 类似于一个队列,保证数据有序。同一个 Group 下的不同 Consumer 并发消费 Paritition,分区实际上是调优 Kafka 并行度的最小单元,因此,可以说,每增加一个 Paritition 就增加了一个消费并发。

Kafka 具有优秀的分区分配算法——StickyAssignor,可以保证分区的分配尽量地均衡,且每一次重分配的结果尽量与上一次分配结果保持一致。这样,整个集群的分区尽量地均衡,各个 Broker 和 Consumer 的处理不至于出现太大的倾斜。

65 哥:那是不是分区数越多越好呢?

当然不是。

越多的分区需要打开更多的文件句柄

在 kafka 的 broker 中,每个分区都会对照着文件系统的一个目录。在 kafka 的数据日志文件目录中,每个日志数据段都会分配两个文件,一个索引文件和一个数据文件。因此,随着 partition 的增多,需要的文件句柄数急剧增加,必要时需要调整操作系统允许打开的文件句柄数。

客户端 / 服务器端需要使用的内存就越多

客户端 producer 有个参数 batch.size,默认是 16KB。它会为每个分区缓存消息,一旦满了就打包将消息批量发出。看上去这是个能够提升性能的设计。不过很显然,因为这个参数是分区级别的,如果分区数越多,这部分缓存所需的内存占用也会更多。

降低高可用性

分区越多,每个 Broker 上分配的分区也就越多,当一个发生 Broker 宕机,那么恢复时间将很长。

文件结构

Kafka 消息是以 Topic 为单位进行归类,各个 Topic 之间是彼此独立的,互不影响。每个 Topic 又可以分为一个或多个分区。每个分区各自存在一个记录消息数据的日志文件。

Kafka 每个分区日志在物理上实际按大小被分成多个 Segment。

  • segment file 组成:由 2 大部分组成,分别为 index file 和 data file,此 2 个文件一一对应,成对出现,后缀”.index”和“.log”分别表示为 segment 索引文件、数据文件。
  • segment 文件命名规则:partion 全局的第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用 0 填充。

index 采用稀疏索引,这样每个 index 文件大小有限,Kafka 采用mmap的方式,直接将 index 文件映射到内存,这样对 index 的操作就不需要操作磁盘 IO。mmap的 Java 实现对应 MappedByteBuffer

65 哥笔记:mmap 是一种内存映射文件的方法。即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read,write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

Kafka 充分利用二分法来查找对应 offset 的消息位置:

  1. 按照二分法找到小于 offset 的 segment 的.log 和.index
  2. 用目标 offset 减去文件名中的 offset 得到消息在这个 segment 中的偏移量。
  3. 再次用二分法在 index 文件中找到对应的索引。
  4. 到 log 文件中,顺序查找,直到找到 offset 对应的消息。

总结

Kafka 是一个优秀的开源项目。其在性能上面的优化做的淋漓尽致,是很值得我们深入学习的一个项目。无论是思想还是实现,我们都应该认真的去看一看,想一想。

Kafka 性能优化:

  1. 零拷贝网络和磁盘
  2. 优秀的网络模型,基于 Java NIO
  3. 高效的文件数据结构设计
  4. Parition 并行和可扩展
  5. 数据批量传输
  6. 数据压缩
  7. 顺序读写磁盘
  8. 无锁轻量级 offset

往期回顾

  1. 图解 | 搞定分布式,程序员进阶之路
  2. 从面试角度一文学完 Kafka
  3. 不可不知的软件架构模式
  4. Redis 日志篇:无畏宕机快速恢复的杀手锏

文章如有错误,感谢指正,关注我,获取真正的硬核知识点。另外技术读者群也开通了,后台回复「加群」获取「码哥字节」作者微信,一起成长交流。

码哥字节

以上就是 Kafka“快”的秘密,觉得不错请点赞、分享,「码哥字节」感激不尽。

查看原文

赞 0 收藏 0 评论 0

码哥字节 收藏了文章 · 3月19日

Redis 单机、哨兵、集群搭建

1. Redis 单机搭建(以 6.0.6 版本为例)


  1. 安装 gcc 套装。

    yum install cpp
    yum install binutils
    yum install glibc
    yum install glibc-kernheaders
    yum install glibc-common
    yum install glibc-devel
    yum install gcc
    yum install make
  2. 升级 gcc。

    yum -y install centos-release-scl
    yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
    scl enable devtoolset-9 bash
  3. 使 scl 长期有效。

    echo "source /opt/rh/devtoolset-9/enable" >> /etc/profile
  4. 下载 redis

    wget http://download.redis.io/releases/redis-6.0.6.tar.gz
  5. 解压。

    tar vxf redis-6.0.6.tar.gz
  6. 编译,安装。

    cd redis-6.0.6/
    make
    make PREFIX=/usr/local/redis install
  7. 创建配置文件、数据目录,并且将配置文件复制到目录中。

    mkdir /usr/local/redis/conf
    mkdir /usr/local/redis/data
    cp ${REDIS_SRC_HOME}/redis.conf /usr/local/redis/conf/
  8. 修改配置文件 conf/redis.conf

    • 修改其中对应的内容。
    # 后台启动的意思
    daemonize yes 
    # IP绑定,redis不建议对公网开放,直接绑定 0.0.0.0 没毛病
    bind 0.0.0.0
    # redis数据文件存放的目录
    dir /usr/local/redis/data
    # 开启AOF
    appendonly yes
  9. 启动 redis。

    cd /usr/local/redis
    ./bin/redis-server ./conf/redis.conf
  10. 测试。

    ./bin/redis-cli
    • 也可以通过客户端连接。

2. Redis 哨兵高可用搭建


目前为了方便演示,在一台机器上以不同的端口启动 3 个服务。

进行之前,先进行单节点上的前 8 个步骤。

  1. 复制出 3 份配置文件。

    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6380.conf
    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6381.conf
    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6382.conf
  2. 分别修改 3 个文件 redis-6380.confredis-6381.confredis-6382.conf,修改其中对应的端口及 pid 对应的保存文件(注意:三个文件都需要修改)。

    # 端口号(如果同一台服务器上启动,注意要修改为不同的端口)
    port 6380
    # 这个文件会自动生成(如果同一台服务器上启动,注意要修改为不同的端口)
    pidfile /var/run/redis_6380.pid
  3. 启动 3 个 redis。

    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6380.conf
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6381.conf
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6382.conf
  4. 配置为 1 主 2 从

    /usr/local/redis/bin/redis-cli -p 6381 slaveof 127.0.0.1 6380
    /usr/local/redis/bin/redis-cli -p 6382 slaveof 127.0.0.1 6380
  5. 检查集群。

    /usr/local/redis/bin/redis-cli -p 6380 info Replication
  6. 准备哨兵配置文件。

    cp ${REDIS_SRC_HOME}/sentinel.conf /usr/local/redis/conf/
    
    cp /usr/local/redis/conf/sentinel.conf /usr/local/redis/conf/sentinel-26380.conf
    cp /usr/local/redis/conf/sentinel.conf /usr/local/redis/conf/sentinel-26381.conf
    cp /usr/local/redis/conf/sentinel.conf /usr/local/redis/conf/sentinel-26382.conf
  7. 分别修改 3 个文件 sentinel-26380.confsentinel-26381.confsentinel-26382.conf (注意:三个文件都需要修改)。

    # 绑定IP
    bind 0.0.0.0
    # 后台运行
    daemonize yes
    # 默认yes,没指定密码或者指定IP的情况下,外网无法访问
    protected-mode no
    # 哨兵的端口,客户端通过这个端口来发现redis
    port 26380
    # 这个文件会自动生成(如果同一台服务器上启动,注意要修改为不同的端口)
    pidfile /var/run/redis-sentinel-26380.pid
    # sentinel监控的master的名字叫做mymaster,初始地址为 127.0.0.1 6380,2代表两个及以上哨兵认定为死亡,才认为是真的死亡
    sentinel monitor mymaster 127.0.0.1 6380 2
  8. 启动哨兵集群。

    /usr/local/redis/bin/redis-server /usr/local/redis/conf/sentinel-26380.conf --sentinel
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/sentinel-26381.conf --sentinel
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/sentinel-26382.conf --sentinel
  9. 检测。

    • 停掉 master 进程,可以看到,会从其他两台 slave 中选择一台,变成 master。

3. Redis 集群搭建


目前为了方便演示,在一台机器上以不同的端口启动 6 个服务。

进行之前,先进行单节点上的前 8 个步骤。

1. 集群搭建

  1. 准备 6 份配置文件。

    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6381.conf
    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6382.conf
    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6383.conf
    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6384.conf
    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6385.conf
    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis-6386.conf
  2. 分别修改 redis-6381.confredis-6382.confredis-6383.confredis-6384.confredis-6385.confredis-6386.conf 文件中的以下内容。

    # 端口号(如果同一台服务器上启动,注意要修改为不同的端口)
    port 6381
    # 开启集群
    cluster-enabled yes
    # 会自动生成在上面配置的dir目录下(如果同一台服务器上启动,注意要修改为不同的端口)
    cluster-config-file nodes-6381.conf
    cluster-node-timeout 5000
    # 这个文件会自动生成(如果同一台服务器上启动,注意要修改为不同的端口)
    pidfile /var/run/redis_6381.pid 
  3. 启动 6 个 redis 实例。

    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6381.conf
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6382.conf
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6383.conf
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6384.conf
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6385.conf
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis-6386.conf
  4. 创建 cluster。

    /usr/local/redis/bin/redis-cli --cluster create 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 127.0.0.1:6386 --cluster-replicas 1
  5. 集群检验和测试。

    # 检查集群,查看所有节点信息
    /usr/local/redis/bin/redis-cli -c -h 127.0.0.1 -p 6381 cluster nodes
    
    # 执行后的信息(节点id ip+端口 角色 masterid 处理的ping数量 最后一个pong时间 节点配置版本 节点连接状态 slot槽分配情况)
    386bfa4ad82ecf67800fb957899e6c0621cb73a6 127.0.0.1:6386@16386 slave 5352c77a1245f67f89bfaeb47caa04eb534e6724 0 1597911585458 1 connected
    21a8ab725f458901e2d23781a9757b8ada023af1 127.0.0.1:6385@16385 slave 103e8326a6d583064a4d4448d285343536490659 0 1597911586000 3 connected
    5352c77a1245f67f89bfaeb47caa04eb534e6724 127.0.0.1:6381@16381 myself,master - 0 1597911583000 1 connected 0-5460
    103e8326a6d583064a4d4448d285343536490659 127.0.0.1:6383@16383 master - 0 1597911585052 3 connected 10923-16383
    2d40bdd3bc9b01d67362110217123debb6f780cf 127.0.0.1:6384@16384 slave 2cd4da5b1a6d361216f620a21ec4e50da21a2e8a 0 1597911586582 2 connected
    2cd4da5b1a6d361216f620a21ec4e50da21a2e8a 127.0.0.1:6382@16382 master - 0 1597911586479 2 connected 5461-10922
    
    # 测试 Redis Cluster 的一种简单方法是使用 redis-cli 命令行实用程序。
    # -c 是支持cluster重定向。
    /usr/local/redis/bin/redis-cli -c -h 127.0.0.1 -p 6381
    # 然后执行一些命令,例如:
    > set a 1
    
    # 查看一个 key a 属于哪一个槽位
    > cluster keyslot a

2. 集群 slot 数量整理 reshard。

  • /usr/local/redis/bin/redis-cli --cluster help 可以查看所有这个命令和子命令的帮助信息。
  • 默认是 master 平均分了 0-16383 的所有虚拟 slot。可以进行调整,部分节点放多一点 slot (槽或者位置)。

    /usr/local/redis/bin/redis-cli --cluster reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes
  • 重新检查集群。

    /usr/local/redis/bin/redis-cli --cluster check 127.0.0.1:6381

3. 测试自动故障转移。

  • cluster 集群不保证数据一致,数据也可能丢失。
  • 首先是运行客户端不断的写入或读取数据,以便能够发现问题。
  • 然后是模拟节点故障:找一个主节点关闭,主从故障切换的过程中,这个时间段的操作,客户端而言,只能是失败。
  • 官方描述 https://redis.io/topics/clust...,There is always a window of time when it is possible to lose writes during partitions.(分区的时间窗口内总是有可能丢失写操作) 。

4. 手动故障转移。

  • 可能某个节点需要维护(机器下线、硬件升级、系统版本调整等等场景),需要手动的实现转移。
  • 在 slave 节点上执行命令。

    CLUSTER FAILOVER
  • 注:CLUSTER help 可以看到帮助文档和简介。 相对安全的做法。

5. 扩容。

# 1、 启动新节点
/usr/local/redis/bin/redis-server /usr/local/redis/conf/6387.conf

# 2、 加入到已经存在的集群作为master
/usr/local/redis/bin/redis-cli --cluster add-node 127.0.0.1:6387 127.0.0.1:6382
# 本质就是发送一个新节点通过 CLUSTER MEET 命令加入集群
# 新节点没有分配 hash 槽

# 3、 加入到已经存在的集群作为 slave
/usr/local/redis/bin/redis-cli --cluster add-node 127.0.0.1:6387 127.0.0.1:6382 --cluster-slave
# 可以手工指定 master,否则就是选择一个 slave 数量较少的master 
/usr/local/redis/bin/redis-cli --cluster add-node 127.0.0.1:6387 127.0.0.1:6382 --cluster-slave --cluster-master-id <node-id>
# 还可以将空 master,转换为 slave(终端操作)。
cluster replicate <master-node-id>

# 4、 检查集群
/usr/local/redis/bin/redis-cli --cluster check 127.0.0.1:6382

6. 缩容(删除节点)。

# 注意:删除 master 的时候要把数据清空或者分配给其他主节点
/usr/local/redis/bin/redis-cli --cluster del-node 127.0.0.1:6381 <node-id>
欢迎关注订阅号:
image
查看原文

码哥字节 收藏了文章 · 3月19日

Redis 6.0 集群搭建实践

图片

本文是Redis集群学习的实践总结(基于Redis 6.0+),详细介绍逐步搭建Redis集群环境的过程,并完成集群伸缩的实践。

Redis集群简介

Redis集群(Redis Cluster) 是Redis提供的分布式数据库方案,通过 分片(sharding) 来进行数据共享,并提供复制和故障转移功能。相比于主从复制、哨兵模式,Redis集群实现了较为完善的高可用方案,解决了存储能力受到单机限制,写操作无法负载均衡的问题。

本文是Redis集群学习的实践总结,详细介绍逐步搭建Redis集群环境的过程,并完成集群伸缩的实践。

Redis集群环境搭建

方便起见,这里集群环境的所有节点全部位于同一个服务器上,共6个节点以端口号区分,3个主节点+3个从节点。集群的简单架构如图:图片本文基于最新的Redis 6.0+,直接从github下载最新的源码编译获得常用工具 redis-server ,  redis-cli 。值得注意的是,从Redis 5.0以后的版本,集群管理软件 redis-trib.rb 被集成到 redis-cli 客户端工具中(详细可参考cluster-tutorial)。

本节介绍集群环境搭建时,并未借助 redis-trib.rb 快速管理,而是按照标准步骤一步步搭建,这也是为了熟悉集群管理的基本步骤。在集群伸缩实践一节将借助 redis-trib.rb 完成集群重新分片工作。

集群的搭建可以分为四步:

  • 启动节点:将节点以集群方式启动,此时节点是独立的。
  • 节点握手:将独立的节点连成网络。
  • 槽指派:将16384个槽位分配给主节点,以达到分片保存数据库键值对的效果。
  • 主从复制:为从节点指定主节点。

启动节点

每个节点初始状态仍为 Master服务器,唯一不同的是:使用 Cluster 模式启动。需要对配置文件进行修改,以端口号为6379的节点为例,主要修改如下几项:

# redis_6379_cluster.conf
port 6379
cluster-enabled yes
cluster-config-file "node-6379.conf"
logfile "redis-server-6379.log"
dbfilename "dump-6379.rdb"
daemonize yes

其中 cluster-config-file 参数指定了集群配置文件的位置,每个节点在运行过程中,会维护一份集群配置文件;每当集群信息发生变化时(如增减节点),集群内所有节点会将最新信息更新到该配置文件;当节点重启后,会重新读取该配置文件,获取集群信息,可以方便的重新加入到集群中。也就是说,当Redis节点以集群模式启动时,会首先寻找是否有集群配置文件,如果有则使用文件中的配置启动,如果没有,则初始化配置并将配置保存到文件中。集群配置文件由Redis节点维护,不需要人工修改。

为6个节点修改好相应的配置文件后,即可利用 redis-server redis_xxxx_cluster.conf 工具启动6个服务器(xxxx表示端口号,对应相应的配置文件)。利用ps命令查看进程:

$ ps -aux | grep redis
... 800  0.1  0.0  49584  2444 ?        Ssl  20:42   0:00 redis-server 127.0.0.1:6379 [cluster]
... 805  0.1  0.0  49584  2440 ?        Ssl  20:42   0:00 redis-server 127.0.0.1:6380 [cluster]
... 812  0.3  0.0  49584  2436 ?        Ssl  20:42   0:00 redis-server 127.0.0.1:6381 [cluster]
... 817  0.1  0.0  49584  2432 ?        Ssl  20:43   0:00 redis-server 127.0.0.1:6479 [cluster]
... 822  0.0  0.0  49584  2380 ?        Ssl  20:43   0:00 redis-server 127.0.0.1:6480 [cluster]
... 827  0.5  0.0  49584  2380 ?        Ssl  20:43   0:00 redis-server 127.0.0.1:6481 [cluster]

节点握手

将上面的每个节点启动后,节点间是相互独立的,他们都处于一个只包含自己的集群当中,以端口号6379的服务器为例,利用 CLUSTER NODES 查看当前集群包含的节点。

127.0.0.1:6379> CLUSTER NODES
37784b3605ad216fa93e976979c43def42bf763d :6379@16379 myself,master - 0 0 0 connected 449 4576 5798 7568 8455 12706

我们需要将各个独立的节点连接起来,构成一个包含多个节点的集群,使用 CLUSTER MEET 命令。

$ redis-cli -p 6379 -c          # -c 选项指定以Cluster模式运行redis-cli 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6380 OK 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6381 OK 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6480 OK 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6381 OK 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6382 OK

再次查看此时集群中包含的节点情况:

127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603632309283 4 connected
87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,master - 0 1603632308000 1 connected
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603632310292 2 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 master - 0 1603632309000 5 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603632308000 3 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 master - 0 1603632311302 0 connected

可以发现此时6个节点均作为主节点加入到集群中, CLUSTER NODES 返回的结果各项含义如下:

<id> <ip:port@cport> <flags> <master> <ping-sent> <pong-recv> <config-epoch> <link-state> <slot> <slot> ... <slot>
  • 节点id: 由40个16进制字符串组成,节点id只在集群初始化时创建一次,然后保存到集群配置文件(即前文提到的cluster-config-file)中,以后节点重新启动时会直接在集群配置文件中读取。
  • port@cport: 前者为普通端口,用于为客户端提供服务;后者为集群端口,分配方法为:普通端口+10000,只用于节点间的通讯。

其余各项的详细解释可以参考官方文档cluster nodes。

槽指派

Redis集群通过分片(sharding)的方式保存数据库的键值对,整个数据库被分为16384个槽(slot),数据库每个键都属于这16384个槽的一个,集群中的每个节点都可以处理0个或者最多16384个slot。

槽是数据管理和迁移的基本单位。当数据库中的16384个槽都分配了节点时,集群处于上线状态(ok);如果有任意一个槽没有分配节点,则集群处于下线状态(fail)。

注意,只有主节点有处理槽的能力,如果将槽指派步骤放在主从复制之后,并且将槽位分配给从节点,那么集群将无法正常工作(处于下线状态)。

利用 CLUSTER ADDSLOTS

redis-cli  -p 6379 cluster addslots {0..5000}
redis-cli  -p 6380 cluster addslots {5001..10000}
redis-cli  -p 6381 cluster addslots {10001..16383}

槽指派后集群中节点情况如下:

127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603632880310 4 connected 5001-10000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,master - 0 1603632879000 1 connected 0-5000 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603632879000 2 connected 10001-16383 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 master - 0 1603632878000 5 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603632880000 3 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 master - 0 1603632881317 0 connected
127.0.0.1:6379> CLUSTER INFO
cluster_state:ok                        # 集群处于上线状态
cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6 cluster_size:3 cluster_current_epoch:5 cluster_my_epoch:1 cluster_stats_messages_ping_sent:4763 cluster_stats_messages_pong_sent:4939 cluster_stats_messages_meet_sent:5 cluster_stats_messages_sent:9707 cluster_stats_messages_ping_received:4939 cluster_stats_messages_pong_received:4768 cluster_stats_messages_received:9707

主从复制

上述步骤后,集群节点均作为主节点存在,仍不能实现Redis的高可用,配置主从复制之后,才算真正实现了集群的高可用功能。

CLUSTER REPLICATE <node_id> 用来让集群中接收命令的节点成为 node_id 所指定节点的从节点,并开始对主节点进行复制。

redis-cli  -p 6479 cluster replicate 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52
redis-cli  -p 6480 cluster replicate c47598b25205cc88abe2e5094d5bfd9ea202335f
redis-cli  -p 6481 cluster replicate 51081a64ddb3ccf5432c435a8cf20d45ab795dd8
127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603633105211 4 connected 5001-10000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,master - 0 1603633105000 1 connected 0-5000 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603633105000 2 connected 10001-16383 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603633107229 5 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 slave 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 0 1603633106221 3 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603633104000 4 connected

顺带补充,上述步骤1.2,1.3,1.4可以利用 redis-trib.rb 工具整体实现,在Redis 5.0之后直接利用 redis-cli 完成,参考命令如下:

redis-cli --cluster create  127.0.0.1:6379 127.0.0.1:6479  127.0.0.1:6380 127.0.0.1:6480  127.0.0.1:6381 127.0.0.1:6481  --cluster-replicas 1
--cluster-replicas 1 指示给定的创建节点列表是以主节点+从节点对组成的。

在集群中执行命令

集群此时处于上线状态,可以通过客户端向集群中的节点发送命令。接收命令的节点会计算出命令要处理的键属于哪个槽,并检查这个槽是否指派给自己。

  • 如果键所在的slot刚好指派给了当前节点,会直接执行这个命令。
  • 否则,节点向客户端返回 MOVED 错误,指引客户端转向 redirect 至正确的节点,并再次发送此前的命令。

此处,我们利用 CLUSTER KEYSLOT 查看到键 name 所在槽号为5798(被分配在6380节点),当对此键操作时,会被重定向到相应的节点。对键 fruits 的操作与此类似。

127.0.0.1:6379> CLUSTER KEYSLOT name
(integer) 5798
127.0.0.1:6379> set name huey -> Redirected to slot [5798] located at 127.0.0.1:6380 OK 127.0.0.1:6380>
127.0.0.1:6379> get fruits -> Redirected to slot [14943] located at 127.0.0.1:6381
"apple"
127.0.0.1:6381>

值得注意的是,当我们将命令通过客户端发送给一个从节点时,命令会被重定向至对应的主节点。

127.0.0.1:6480> KEYS *
1) "name"
127.0.0.1:6480> get name -> Redirected to slot [5798] located at 127.0.0.1:6380
"huey"

集群故障转移

集群中主节点下线时,复制此主节点的所有的从节点将会选出一个节点作为新的主节点,并完成故障转移。和主从复制的配置相似,当原先的从节点再次上线,它会被作为新主节点的的从节点存在于集群中。

下面模拟6379节点宕机的情况(将其SHUTDOWN),可以观察到其从节点6479将作为新的主节点继续工作。

462:S 26 Oct 14:08:12.750 * FAIL message received from c47598b25205cc88abe2e5094d5bfd9ea202335f about 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 462:S 26 Oct 14:08:12.751 # Cluster state changed: fail 462:S 26 Oct 14:08:12.829 # Start of election delayed for 595 milliseconds (rank #0, offset 9160). 462:S 26 Oct 14:08:13.434 # Starting a failover election for epoch 6. 462:S 26 Oct 14:08:13.446 # Failover election won: I'm the new master.
462:S 26 Oct 14:08:13.447 # configEpoch set to 6 after successful failover 462:M 26 Oct 14:08:13.447 # Setting secondary replication ID to d357886e00341b57bf17e46b6d9f8cf53b7fad21, valid up to offset: 9161. New replication ID is adbf41b16075ea22b17f145186c53c4499864d5b 462:M 26 Oct 14:08:13.447 * Discarding previously cached master state. 462:M 26 Oct 14:08:13.448 # Cluster state changed: ok

6379节点从宕机状态恢复后,将作为6380节点的从节点存在。

127.0.0.1:6379> CLUSTER NODES
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603692968000 2 connected 10001-16383 c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603692968504 0 connected 5001-10000 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603692967495 6 connected 0-5000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603692964000 1 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603692967000 4 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603692967000 5 connected

前文提到 cluster-config-file 会记录下集群节点的状态,打开节点6379的配置文件 nodes-6379.conf ,可以看到 CLUSTER NODES 所示信息均被保存在配置文件中:

51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603694920206 2 connected 10001-16383 c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603694916000 0 connected 5001-10000 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603694920000 6 connected 0-5000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603694918000 1 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603694919000 4 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603694919200 5 connected
vars currentEpoch 6 lastVoteEpoch 0

集群伸缩实践

集群伸缩的关键在于对集群的进行重新分片,实现槽位在节点间的迁移。本节将以在集群中添加节点和删除节点为例,对槽迁移进行实践。

借助于 redis-cli 中集成的 redis-trib.rb 工具进行槽位的管理,工具的帮助菜单如下:

$ redis-cli --cluster help
Cluster Manager Commands:
  create         host1:port1 ... hostN:portN --cluster-replicas <arg> check          host:port --cluster-search-multiple-owners
  info           host:port
  fix            host:port --cluster-search-multiple-owners --cluster-fix-with-unreachable-masters
  reshard        host:port --cluster-from <arg>
                 --cluster-to <arg>
                 --cluster-slots <arg>
                 --cluster-yes --cluster-timeout <arg>
                 --cluster-pipeline <arg>
                 --cluster-replace
  rebalance      host:port --cluster-weight <node1=w1...nodeN=wN>
                 --cluster-use-empty-masters --cluster-timeout <arg>
                 --cluster-simulate --cluster-pipeline <arg>
                 --cluster-threshold <arg>
                 --cluster-replace
  add-node       new_host:new_port existing_host:existing_port --cluster-slave --cluster-master-id <arg> del-node       host:port node_id
  call           host:port command arg arg .. arg set-timeout    host:port milliseconds
  import         host:port --cluster-from <arg>
                 --cluster-copy --cluster-replace
  backup         host:port backup_directory
  help
  
For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.

集群伸缩-添加节点

考虑在集群中添加两个节点,端口号为6382和6482,其中节点6482对6382进行复制。

  • (1) 启动节点:按照1.1中介绍的步骤,启动6382和6482节点。
  • (2) 节点握手:借助 redis-cli --cluster add-node 命令分别添加节点6382和6482。
redis-cli --cluster add-node 127.0.0.1:6382 127.0.0.1:6379 redis-cli --cluster add-node 127.0.0.1:6482 127.0.0.1:6379
$ redis-cli --cluster add-node 127.0.0.1:6382 127.0.0.1:6379
>>> Adding node 127.0.0.1:6382 to cluster 127.0.0.1:6379
>>> Performing Cluster Check (using node 127.0.0.1:6379)
S: 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379 slots: (0 slots) slave
    replicates 4c23b25bd4bcef7f4b77d8287e330ae72e738883
M: 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381 slots:[10001-16383] (6383 slots) master 1 additional replica(s)
M: c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380 slots:[5001-10000] (5000 slots) master 1 additional replica(s)
M: 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479 slots:[0-5000] (5001 slots) master 1 additional replica(s)
S: 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481 slots: (0 slots) slave
    replicates 51081a64ddb3ccf5432c435a8cf20d45ab795dd8
S: 32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480 slots: (0 slots) slave
    replicates c47598b25205cc88abe2e5094d5bfd9ea202335f
[OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage...
[OK] All 16384 slots covered. >>> Send CLUSTER MEET to node 127.0.0.1:6382 to make it join the cluster.
[OK] New node added correctly.
  • 移动的槽位数:最终平均每个主节点有4096个slot,因此总共移动4096 slots
  • 接收槽位的目标节点ID:节点6382的ID
  • 移出槽位的源节点ID:节点6379/6380/6381的ID
  1. 重新分片:借助 redis-cli --cluster reshard 命令对集群重新分片,使得各节点槽位均衡(分别从节点6379/6380/6381中迁移一些slot到节点6382中)。需要指定:
$ redis-cli --cluster reshard 127.0.0.1 6479
>>> Performing Cluster Check (using node 127.0.0.1:6479)
M: 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479 slots:[0-5000] (5001 slots) master 1 additional replica(s)
S: 32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480 slots: (0 slots) slave
  replicates c47598b25205cc88abe2e5094d5bfd9ea202335f
M: 706f399b248ed3a080cf1d4e43047a79331b714f 127.0.0.1:6482 slots: (0 slots) master
M: af81109fc29f69f9184ce9512c46df476fe693a3 127.0.0.1:6382 slots: (0 slots) master
M: 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381 slots:[10001-16383] (6383 slots) master 1 additional replica(s)
S: 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481 slots: (0 slots) slave
  replicates 51081a64ddb3ccf5432c435a8cf20d45ab795dd8
S: 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379 slots: (0 slots) slave
  replicates 4c23b25bd4bcef7f4b77d8287e330ae72e738883
M: c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380 slots:[5001-10000] (5000 slots) master 1 additional replica(s)
[OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)? 4096 What is the receiving node ID?
  • (4) 设置主从关系:
redis-cli -p 6482 cluster replicate af81109fc29f69f9184ce9512c46df476fe693a3 
127.0.0.1:6482> CLUSTER NODES
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603694930000 0 connected
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603694931000 2 connected 11597-16383 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603694932000 2 connected
706f399b248ed3a080cf1d4e43047a79331b714f 127.0.0.1:6482@16482 myself,slave af81109fc29f69f9184ce9512c46df476fe693a3 0 1603694932000 8 connected
87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603694932000 6 connected
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603694933678 0 connected 6251-10000 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603694932669 6 connected 1250-5000 af81109fc29f69f9184ce9512c46df476fe693a3 127.0.0.1:6382@16382 master - 0 1603694933000 9 connected 0-1249 5001-6250 10001-11596

集群伸缩-删除节点

这里考虑将新添加的两个节点6382和6482删除,需要将节点6382上分配的槽位迁移到其他节点。

  • (1) 重新分片:同样借助 redis-cli --cluster reshard 命令,将6382节点上的槽位全部转移到节点6479上。
$ redis-cli --cluster reshard 127.0.0.1 6382
>>> Performing Cluster Check (using node 127.0.0.1:6382)
M: af81109fc29f69f9184ce9512c46df476fe693a3 127.0.0.1:6382 slots:[0-1249],[5001-6250],[10001-11596] (4096 slots) master 1 additional replica(s)
M: 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381 slots:[11597-16383] (4787 slots) master 1 additional replica(s)
S: 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379 slots: (0 slots) slave
    replicates 4c23b25bd4bcef7f4b77d8287e330ae72e738883
S: 32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480 slots: (0 slots) slave
    replicates c47598b25205cc88abe2e5094d5bfd9ea202335f
M: 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479 slots:[1250-5000] (3751 slots) master 1 additional replica(s)
M: c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380 slots:[6251-10000] (3750 slots) master 1 additional replica(s)
S: 706f399b248ed3a080cf1d4e43047a79331b714f 127.0.0.1:6482 slots: (0 slots) slave
    replicates af81109fc29f69f9184ce9512c46df476fe693a3
S: 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481 slots: (0 slots) slave
    replicates 51081a64ddb3ccf5432c435a8cf20d45ab795dd8
[OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)? 4096 What is the receiving node ID? 4c23b25bd4bcef7f4b77d8287e330ae72e738883
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1: af81109fc29f69f9184ce9512c46df476fe693a3
Source node #2: done
127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603773540922 0 connected 6251-10000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603773539000 1 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603773541000 10 connected 0-6250 10001-11596 706f399b248ed3a080cf1d4e43047a79331b714f 127.0.0.1:6482@16482 slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603773541000 10 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603773539000 5 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603773541931 4 connected
af81109fc29f69f9184ce9512c46df476fe693a3 127.0.0.1:6382@16382 master - 0 1603773539000 9 connected
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603773540000 2 connected 11597-16383
  • (2) 删除节点:利用 redis-cli --cluster del-node 命令依次删除从节点6482和主节点6382。
$ redis-cli --cluster del-node 127.0.0.1:6482 706f399b248ed3a080cf1d4e43047a79331b714f >>> Removing node 706f399b248ed3a080cf1d4e43047a79331b714f from cluster 127.0.0.1:6482
>>> Sending CLUSTER FORGET messages to the cluster... >>> Sending CLUSTER RESET SOFT to the deleted node.
$ redis-cli --cluster del-node 127.0.0.1:6382 af81109fc29f69f9184ce9512c46df476fe693a3 >>> Removing node af81109fc29f69f9184ce9512c46df476fe693a3 from cluster 127.0.0.1:6382
>>> Sending CLUSTER FORGET messages to the cluster... >>> Sending CLUSTER RESET SOFT to the deleted node.
127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603773679121 0 connected 6251-10000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603773677000 1 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603773678000 10 connected 0-6250 10001-11596 32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603773680130 5 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603773677099 4 connected
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603773678112 2 connected 11597-16383

总结

Redis集群环境的搭建主要包括启动节点、节点握手、槽指派和主从复制等四个步骤,集群伸缩同样涉及这几个方面。借助 redis-cli --cluster 命令来管理集群环境,不仅能增加简便性,还能降低操作失误的风险。

原文:https://www.cnblogs.com/hueyx...

image

查看原文

码哥字节 赞了文章 · 3月19日

Redis 6.0 集群搭建实践

图片

本文是Redis集群学习的实践总结(基于Redis 6.0+),详细介绍逐步搭建Redis集群环境的过程,并完成集群伸缩的实践。

Redis集群简介

Redis集群(Redis Cluster) 是Redis提供的分布式数据库方案,通过 分片(sharding) 来进行数据共享,并提供复制和故障转移功能。相比于主从复制、哨兵模式,Redis集群实现了较为完善的高可用方案,解决了存储能力受到单机限制,写操作无法负载均衡的问题。

本文是Redis集群学习的实践总结,详细介绍逐步搭建Redis集群环境的过程,并完成集群伸缩的实践。

Redis集群环境搭建

方便起见,这里集群环境的所有节点全部位于同一个服务器上,共6个节点以端口号区分,3个主节点+3个从节点。集群的简单架构如图:图片本文基于最新的Redis 6.0+,直接从github下载最新的源码编译获得常用工具 redis-server ,  redis-cli 。值得注意的是,从Redis 5.0以后的版本,集群管理软件 redis-trib.rb 被集成到 redis-cli 客户端工具中(详细可参考cluster-tutorial)。

本节介绍集群环境搭建时,并未借助 redis-trib.rb 快速管理,而是按照标准步骤一步步搭建,这也是为了熟悉集群管理的基本步骤。在集群伸缩实践一节将借助 redis-trib.rb 完成集群重新分片工作。

集群的搭建可以分为四步:

  • 启动节点:将节点以集群方式启动,此时节点是独立的。
  • 节点握手:将独立的节点连成网络。
  • 槽指派:将16384个槽位分配给主节点,以达到分片保存数据库键值对的效果。
  • 主从复制:为从节点指定主节点。

启动节点

每个节点初始状态仍为 Master服务器,唯一不同的是:使用 Cluster 模式启动。需要对配置文件进行修改,以端口号为6379的节点为例,主要修改如下几项:

# redis_6379_cluster.conf
port 6379
cluster-enabled yes
cluster-config-file "node-6379.conf"
logfile "redis-server-6379.log"
dbfilename "dump-6379.rdb"
daemonize yes

其中 cluster-config-file 参数指定了集群配置文件的位置,每个节点在运行过程中,会维护一份集群配置文件;每当集群信息发生变化时(如增减节点),集群内所有节点会将最新信息更新到该配置文件;当节点重启后,会重新读取该配置文件,获取集群信息,可以方便的重新加入到集群中。也就是说,当Redis节点以集群模式启动时,会首先寻找是否有集群配置文件,如果有则使用文件中的配置启动,如果没有,则初始化配置并将配置保存到文件中。集群配置文件由Redis节点维护,不需要人工修改。

为6个节点修改好相应的配置文件后,即可利用 redis-server redis_xxxx_cluster.conf 工具启动6个服务器(xxxx表示端口号,对应相应的配置文件)。利用ps命令查看进程:

$ ps -aux | grep redis
... 800  0.1  0.0  49584  2444 ?        Ssl  20:42   0:00 redis-server 127.0.0.1:6379 [cluster]
... 805  0.1  0.0  49584  2440 ?        Ssl  20:42   0:00 redis-server 127.0.0.1:6380 [cluster]
... 812  0.3  0.0  49584  2436 ?        Ssl  20:42   0:00 redis-server 127.0.0.1:6381 [cluster]
... 817  0.1  0.0  49584  2432 ?        Ssl  20:43   0:00 redis-server 127.0.0.1:6479 [cluster]
... 822  0.0  0.0  49584  2380 ?        Ssl  20:43   0:00 redis-server 127.0.0.1:6480 [cluster]
... 827  0.5  0.0  49584  2380 ?        Ssl  20:43   0:00 redis-server 127.0.0.1:6481 [cluster]

节点握手

将上面的每个节点启动后,节点间是相互独立的,他们都处于一个只包含自己的集群当中,以端口号6379的服务器为例,利用 CLUSTER NODES 查看当前集群包含的节点。

127.0.0.1:6379> CLUSTER NODES
37784b3605ad216fa93e976979c43def42bf763d :6379@16379 myself,master - 0 0 0 connected 449 4576 5798 7568 8455 12706

我们需要将各个独立的节点连接起来,构成一个包含多个节点的集群,使用 CLUSTER MEET 命令。

$ redis-cli -p 6379 -c          # -c 选项指定以Cluster模式运行redis-cli 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6380 OK 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6381 OK 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6480 OK 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6381 OK 127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6382 OK

再次查看此时集群中包含的节点情况:

127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603632309283 4 connected
87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,master - 0 1603632308000 1 connected
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603632310292 2 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 master - 0 1603632309000 5 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603632308000 3 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 master - 0 1603632311302 0 connected

可以发现此时6个节点均作为主节点加入到集群中, CLUSTER NODES 返回的结果各项含义如下:

<id> <ip:port@cport> <flags> <master> <ping-sent> <pong-recv> <config-epoch> <link-state> <slot> <slot> ... <slot>
  • 节点id: 由40个16进制字符串组成,节点id只在集群初始化时创建一次,然后保存到集群配置文件(即前文提到的cluster-config-file)中,以后节点重新启动时会直接在集群配置文件中读取。
  • port@cport: 前者为普通端口,用于为客户端提供服务;后者为集群端口,分配方法为:普通端口+10000,只用于节点间的通讯。

其余各项的详细解释可以参考官方文档cluster nodes。

槽指派

Redis集群通过分片(sharding)的方式保存数据库的键值对,整个数据库被分为16384个槽(slot),数据库每个键都属于这16384个槽的一个,集群中的每个节点都可以处理0个或者最多16384个slot。

槽是数据管理和迁移的基本单位。当数据库中的16384个槽都分配了节点时,集群处于上线状态(ok);如果有任意一个槽没有分配节点,则集群处于下线状态(fail)。

注意,只有主节点有处理槽的能力,如果将槽指派步骤放在主从复制之后,并且将槽位分配给从节点,那么集群将无法正常工作(处于下线状态)。

利用 CLUSTER ADDSLOTS

redis-cli  -p 6379 cluster addslots {0..5000}
redis-cli  -p 6380 cluster addslots {5001..10000}
redis-cli  -p 6381 cluster addslots {10001..16383}

槽指派后集群中节点情况如下:

127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603632880310 4 connected 5001-10000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,master - 0 1603632879000 1 connected 0-5000 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603632879000 2 connected 10001-16383 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 master - 0 1603632878000 5 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603632880000 3 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 master - 0 1603632881317 0 connected
127.0.0.1:6379> CLUSTER INFO
cluster_state:ok                        # 集群处于上线状态
cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6 cluster_size:3 cluster_current_epoch:5 cluster_my_epoch:1 cluster_stats_messages_ping_sent:4763 cluster_stats_messages_pong_sent:4939 cluster_stats_messages_meet_sent:5 cluster_stats_messages_sent:9707 cluster_stats_messages_ping_received:4939 cluster_stats_messages_pong_received:4768 cluster_stats_messages_received:9707

主从复制

上述步骤后,集群节点均作为主节点存在,仍不能实现Redis的高可用,配置主从复制之后,才算真正实现了集群的高可用功能。

CLUSTER REPLICATE <node_id> 用来让集群中接收命令的节点成为 node_id 所指定节点的从节点,并开始对主节点进行复制。

redis-cli  -p 6479 cluster replicate 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52
redis-cli  -p 6480 cluster replicate c47598b25205cc88abe2e5094d5bfd9ea202335f
redis-cli  -p 6481 cluster replicate 51081a64ddb3ccf5432c435a8cf20d45ab795dd8
127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603633105211 4 connected 5001-10000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,master - 0 1603633105000 1 connected 0-5000 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603633105000 2 connected 10001-16383 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603633107229 5 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 slave 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 0 1603633106221 3 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603633104000 4 connected

顺带补充,上述步骤1.2,1.3,1.4可以利用 redis-trib.rb 工具整体实现,在Redis 5.0之后直接利用 redis-cli 完成,参考命令如下:

redis-cli --cluster create  127.0.0.1:6379 127.0.0.1:6479  127.0.0.1:6380 127.0.0.1:6480  127.0.0.1:6381 127.0.0.1:6481  --cluster-replicas 1
--cluster-replicas 1 指示给定的创建节点列表是以主节点+从节点对组成的。

在集群中执行命令

集群此时处于上线状态,可以通过客户端向集群中的节点发送命令。接收命令的节点会计算出命令要处理的键属于哪个槽,并检查这个槽是否指派给自己。

  • 如果键所在的slot刚好指派给了当前节点,会直接执行这个命令。
  • 否则,节点向客户端返回 MOVED 错误,指引客户端转向 redirect 至正确的节点,并再次发送此前的命令。

此处,我们利用 CLUSTER KEYSLOT 查看到键 name 所在槽号为5798(被分配在6380节点),当对此键操作时,会被重定向到相应的节点。对键 fruits 的操作与此类似。

127.0.0.1:6379> CLUSTER KEYSLOT name
(integer) 5798
127.0.0.1:6379> set name huey -> Redirected to slot [5798] located at 127.0.0.1:6380 OK 127.0.0.1:6380>
127.0.0.1:6379> get fruits -> Redirected to slot [14943] located at 127.0.0.1:6381
"apple"
127.0.0.1:6381>

值得注意的是,当我们将命令通过客户端发送给一个从节点时,命令会被重定向至对应的主节点。

127.0.0.1:6480> KEYS *
1) "name"
127.0.0.1:6480> get name -> Redirected to slot [5798] located at 127.0.0.1:6380
"huey"

集群故障转移

集群中主节点下线时,复制此主节点的所有的从节点将会选出一个节点作为新的主节点,并完成故障转移。和主从复制的配置相似,当原先的从节点再次上线,它会被作为新主节点的的从节点存在于集群中。

下面模拟6379节点宕机的情况(将其SHUTDOWN),可以观察到其从节点6479将作为新的主节点继续工作。

462:S 26 Oct 14:08:12.750 * FAIL message received from c47598b25205cc88abe2e5094d5bfd9ea202335f about 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 462:S 26 Oct 14:08:12.751 # Cluster state changed: fail 462:S 26 Oct 14:08:12.829 # Start of election delayed for 595 milliseconds (rank #0, offset 9160). 462:S 26 Oct 14:08:13.434 # Starting a failover election for epoch 6. 462:S 26 Oct 14:08:13.446 # Failover election won: I'm the new master.
462:S 26 Oct 14:08:13.447 # configEpoch set to 6 after successful failover 462:M 26 Oct 14:08:13.447 # Setting secondary replication ID to d357886e00341b57bf17e46b6d9f8cf53b7fad21, valid up to offset: 9161. New replication ID is adbf41b16075ea22b17f145186c53c4499864d5b 462:M 26 Oct 14:08:13.447 * Discarding previously cached master state. 462:M 26 Oct 14:08:13.448 # Cluster state changed: ok

6379节点从宕机状态恢复后,将作为6380节点的从节点存在。

127.0.0.1:6379> CLUSTER NODES
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603692968000 2 connected 10001-16383 c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603692968504 0 connected 5001-10000 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603692967495 6 connected 0-5000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603692964000 1 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603692967000 4 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603692967000 5 connected

前文提到 cluster-config-file 会记录下集群节点的状态,打开节点6379的配置文件 nodes-6379.conf ,可以看到 CLUSTER NODES 所示信息均被保存在配置文件中:

51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603694920206 2 connected 10001-16383 c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603694916000 0 connected 5001-10000 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603694920000 6 connected 0-5000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603694918000 1 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603694919000 4 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603694919200 5 connected
vars currentEpoch 6 lastVoteEpoch 0

集群伸缩实践

集群伸缩的关键在于对集群的进行重新分片,实现槽位在节点间的迁移。本节将以在集群中添加节点和删除节点为例,对槽迁移进行实践。

借助于 redis-cli 中集成的 redis-trib.rb 工具进行槽位的管理,工具的帮助菜单如下:

$ redis-cli --cluster help
Cluster Manager Commands:
  create         host1:port1 ... hostN:portN --cluster-replicas <arg> check          host:port --cluster-search-multiple-owners
  info           host:port
  fix            host:port --cluster-search-multiple-owners --cluster-fix-with-unreachable-masters
  reshard        host:port --cluster-from <arg>
                 --cluster-to <arg>
                 --cluster-slots <arg>
                 --cluster-yes --cluster-timeout <arg>
                 --cluster-pipeline <arg>
                 --cluster-replace
  rebalance      host:port --cluster-weight <node1=w1...nodeN=wN>
                 --cluster-use-empty-masters --cluster-timeout <arg>
                 --cluster-simulate --cluster-pipeline <arg>
                 --cluster-threshold <arg>
                 --cluster-replace
  add-node       new_host:new_port existing_host:existing_port --cluster-slave --cluster-master-id <arg> del-node       host:port node_id
  call           host:port command arg arg .. arg set-timeout    host:port milliseconds
  import         host:port --cluster-from <arg>
                 --cluster-copy --cluster-replace
  backup         host:port backup_directory
  help
  
For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.

集群伸缩-添加节点

考虑在集群中添加两个节点,端口号为6382和6482,其中节点6482对6382进行复制。

  • (1) 启动节点:按照1.1中介绍的步骤,启动6382和6482节点。
  • (2) 节点握手:借助 redis-cli --cluster add-node 命令分别添加节点6382和6482。
redis-cli --cluster add-node 127.0.0.1:6382 127.0.0.1:6379 redis-cli --cluster add-node 127.0.0.1:6482 127.0.0.1:6379
$ redis-cli --cluster add-node 127.0.0.1:6382 127.0.0.1:6379
>>> Adding node 127.0.0.1:6382 to cluster 127.0.0.1:6379
>>> Performing Cluster Check (using node 127.0.0.1:6379)
S: 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379 slots: (0 slots) slave
    replicates 4c23b25bd4bcef7f4b77d8287e330ae72e738883
M: 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381 slots:[10001-16383] (6383 slots) master 1 additional replica(s)
M: c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380 slots:[5001-10000] (5000 slots) master 1 additional replica(s)
M: 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479 slots:[0-5000] (5001 slots) master 1 additional replica(s)
S: 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481 slots: (0 slots) slave
    replicates 51081a64ddb3ccf5432c435a8cf20d45ab795dd8
S: 32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480 slots: (0 slots) slave
    replicates c47598b25205cc88abe2e5094d5bfd9ea202335f
[OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage...
[OK] All 16384 slots covered. >>> Send CLUSTER MEET to node 127.0.0.1:6382 to make it join the cluster.
[OK] New node added correctly.
  • 移动的槽位数:最终平均每个主节点有4096个slot,因此总共移动4096 slots
  • 接收槽位的目标节点ID:节点6382的ID
  • 移出槽位的源节点ID:节点6379/6380/6381的ID
  1. 重新分片:借助 redis-cli --cluster reshard 命令对集群重新分片,使得各节点槽位均衡(分别从节点6379/6380/6381中迁移一些slot到节点6382中)。需要指定:
$ redis-cli --cluster reshard 127.0.0.1 6479
>>> Performing Cluster Check (using node 127.0.0.1:6479)
M: 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479 slots:[0-5000] (5001 slots) master 1 additional replica(s)
S: 32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480 slots: (0 slots) slave
  replicates c47598b25205cc88abe2e5094d5bfd9ea202335f
M: 706f399b248ed3a080cf1d4e43047a79331b714f 127.0.0.1:6482 slots: (0 slots) master
M: af81109fc29f69f9184ce9512c46df476fe693a3 127.0.0.1:6382 slots: (0 slots) master
M: 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381 slots:[10001-16383] (6383 slots) master 1 additional replica(s)
S: 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481 slots: (0 slots) slave
  replicates 51081a64ddb3ccf5432c435a8cf20d45ab795dd8
S: 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379 slots: (0 slots) slave
  replicates 4c23b25bd4bcef7f4b77d8287e330ae72e738883
M: c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380 slots:[5001-10000] (5000 slots) master 1 additional replica(s)
[OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)? 4096 What is the receiving node ID?
  • (4) 设置主从关系:
redis-cli -p 6482 cluster replicate af81109fc29f69f9184ce9512c46df476fe693a3 
127.0.0.1:6482> CLUSTER NODES
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603694930000 0 connected
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603694931000 2 connected 11597-16383 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603694932000 2 connected
706f399b248ed3a080cf1d4e43047a79331b714f 127.0.0.1:6482@16482 myself,slave af81109fc29f69f9184ce9512c46df476fe693a3 0 1603694932000 8 connected
87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603694932000 6 connected
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603694933678 0 connected 6251-10000 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603694932669 6 connected 1250-5000 af81109fc29f69f9184ce9512c46df476fe693a3 127.0.0.1:6382@16382 master - 0 1603694933000 9 connected 0-1249 5001-6250 10001-11596

集群伸缩-删除节点

这里考虑将新添加的两个节点6382和6482删除,需要将节点6382上分配的槽位迁移到其他节点。

  • (1) 重新分片:同样借助 redis-cli --cluster reshard 命令,将6382节点上的槽位全部转移到节点6479上。
$ redis-cli --cluster reshard 127.0.0.1 6382
>>> Performing Cluster Check (using node 127.0.0.1:6382)
M: af81109fc29f69f9184ce9512c46df476fe693a3 127.0.0.1:6382 slots:[0-1249],[5001-6250],[10001-11596] (4096 slots) master 1 additional replica(s)
M: 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381 slots:[11597-16383] (4787 slots) master 1 additional replica(s)
S: 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379 slots: (0 slots) slave
    replicates 4c23b25bd4bcef7f4b77d8287e330ae72e738883
S: 32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480 slots: (0 slots) slave
    replicates c47598b25205cc88abe2e5094d5bfd9ea202335f
M: 4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479 slots:[1250-5000] (3751 slots) master 1 additional replica(s)
M: c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380 slots:[6251-10000] (3750 slots) master 1 additional replica(s)
S: 706f399b248ed3a080cf1d4e43047a79331b714f 127.0.0.1:6482 slots: (0 slots) slave
    replicates af81109fc29f69f9184ce9512c46df476fe693a3
S: 9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481 slots: (0 slots) slave
    replicates 51081a64ddb3ccf5432c435a8cf20d45ab795dd8
[OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)? 4096 What is the receiving node ID? 4c23b25bd4bcef7f4b77d8287e330ae72e738883
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1: af81109fc29f69f9184ce9512c46df476fe693a3
Source node #2: done
127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603773540922 0 connected 6251-10000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603773539000 1 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603773541000 10 connected 0-6250 10001-11596 706f399b248ed3a080cf1d4e43047a79331b714f 127.0.0.1:6482@16482 slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603773541000 10 connected
32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603773539000 5 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603773541931 4 connected
af81109fc29f69f9184ce9512c46df476fe693a3 127.0.0.1:6382@16382 master - 0 1603773539000 9 connected
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603773540000 2 connected 11597-16383
  • (2) 删除节点:利用 redis-cli --cluster del-node 命令依次删除从节点6482和主节点6382。
$ redis-cli --cluster del-node 127.0.0.1:6482 706f399b248ed3a080cf1d4e43047a79331b714f >>> Removing node 706f399b248ed3a080cf1d4e43047a79331b714f from cluster 127.0.0.1:6482
>>> Sending CLUSTER FORGET messages to the cluster... >>> Sending CLUSTER RESET SOFT to the deleted node.
$ redis-cli --cluster del-node 127.0.0.1:6382 af81109fc29f69f9184ce9512c46df476fe693a3 >>> Removing node af81109fc29f69f9184ce9512c46df476fe693a3 from cluster 127.0.0.1:6382
>>> Sending CLUSTER FORGET messages to the cluster... >>> Sending CLUSTER RESET SOFT to the deleted node.
127.0.0.1:6379> CLUSTER NODES
c47598b25205cc88abe2e5094d5bfd9ea202335f 127.0.0.1:6380@16380 master - 0 1603773679121 0 connected 6251-10000 87b7dfacde34b3cf57d5f46ab44fd6fffb2e4f52 127.0.0.1:6379@16379 myself,slave 4c23b25bd4bcef7f4b77d8287e330ae72e738883 0 1603773677000 1 connected
4c23b25bd4bcef7f4b77d8287e330ae72e738883 127.0.0.1:6479@16479 master - 0 1603773678000 10 connected 0-6250 10001-11596 32ed645a9c9d13ca68dba5a147937fb1d05922ee 127.0.0.1:6480@16480 slave c47598b25205cc88abe2e5094d5bfd9ea202335f 0 1603773680130 5 connected
9d587b75bdaed26ca582036ed706df8b2282b0aa 127.0.0.1:6481@16481 slave 51081a64ddb3ccf5432c435a8cf20d45ab795dd8 0 1603773677099 4 connected
51081a64ddb3ccf5432c435a8cf20d45ab795dd8 127.0.0.1:6381@16381 master - 0 1603773678112 2 connected 11597-16383

总结

Redis集群环境的搭建主要包括启动节点、节点握手、槽指派和主从复制等四个步骤,集群伸缩同样涉及这几个方面。借助 redis-cli --cluster 命令来管理集群环境,不仅能增加简便性,还能降低操作失误的风险。

原文:https://www.cnblogs.com/hueyx...

image

查看原文

赞 7 收藏 4 评论 0

码哥字节 关注了用户 · 3月19日

民工哥 @jishuroad

民工哥,10多年职场老司机的经验分享,坚持自学一路从技术小白成长为互联网企业信息技术部门的负责人。

我的新书:《Linux系统运维指南》

微信公众号:民工哥技术之路

民工哥:知乎专栏

欢迎关注,我们一同交流,相互学习,共同成长!!

关注 3581

码哥字节 关注了用户 · 3月19日

高阳Sunny @sunny

SegmentFault 思否 CEO
C14Z.Group Founder
Forbes China 30U30

独立思考 敢于否定

曾经是个话痨... 要做一个有趣的人!

任何问题可以给我发私信或者发邮件 sunny@sifou.com

关注 2173

码哥字节 发布了文章 · 3月19日

Redis 高可用篇:你管这叫主从架构数据同步原理?

《Redis 核心篇:唯快不破的秘密》中,「码哥」揭秘了 Redis 五大数据类型底层的数据结构、IO 模型、线程模型、渐进式 rehash 掌握了 Redis 快的本质原因。

接着,在《Redis 日志篇:无畏宕机与快速恢复的杀手锏》中揭晓了当 Redis 发生宕机可以通过重新读取 RDB 快照和执行 AOF 日志实现快速恢复的高可用手段。

高可用有两个含义:一是数据尽量不丢失,二是服务尽可能提供服务。 AOF 和 RDB 保证了数据持久化尽量不丢失,而主从复制就是增加副本,一份数据保存到多个实例上。即使有一个实例宕机,其他实例依然可以提供服务。

本篇主要带大家全方位吃透 Redis 高可用技术解决方案之一主从复制架构

本篇硬核,建议收藏慢慢品味,我相信读者朋友会有一个质的提升。如有错误还望纠正,谢谢。关注「码哥字节」设置「星标」第一时间接收优质文章,谢谢读者的支持。

核心知识点

开篇寄语

问题 = 机会。遇到问题的时候,内心其实是开心的,越大的问题意味着越大的机会。

任何事情都是有代价的,有得必有失,有失必有得,所以不必计较很多东西,我们只要想清楚自己要做什么,并且想清楚自己愿意为之付出什么代价,然后就放手去做吧!

1. 主从复制概述

65 哥:有了 RDB 和 AOF 再也不怕宕机丢失数据了,但是 Redis 实例宕机了怎么实现高可用呢?

既然一台宕机了无法提供服务,那多台呢?是不是就可以解决了。Redis 提供了主从模式,通过主从复制,将数据冗余一份复制到其他 Redis 服务器。

前者称为主节点 (master),后者称为从节点 (slave);数据的复制是单向的,只能由主节点到从节点。

默认情况下,每台 Redis 服务器都是主节点;且一个主节点可以有多个从节点 (或没有从节点),但一个从节点只能有一个主节点。

65 哥:主从之间的数据如何保证一致性呢?

为了保证副本数据的一致性,主从架构采用了读写分离的方式。

  • 读操作:主、从库都可以执行;
  • 写操作:主库先执行,之后将写操作同步到从库;

Redis 读写分离

65 哥:为何要采用读写分离的方式?

我们可以假设主从库都可以执行写指令,假如对同一份数据分别修改了多次,每次修改发送到不同的主从实例上,就导致是实例的副本数据不一致了。

如果为了保证数据一致,Redis 需要加锁,协调多个实例的修改,Redis 自然不会这么干!

65 哥:主从复制还有其他作用么?
  1. 故障恢复:当主节点宕机,其他节点依然可以提供服务;
  2. 负载均衡:Master 节点提供写服务,Slave 节点提供读服务,分担压力;
  3. 高可用基石:是哨兵和 cluster 实施的基础,是高可用的基石。

2. 搭建主从复制

主从复制的开启,完全是在从节点发起的,不需要我们在主节点做任何事情。

65 哥:怎么搭建主从复制架构呀?

可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系。

在从节点开启主从复制,有 3 种方式:

  1. 配置文件
    在从服务器的配置文件中加入 replicaof <masterip> <masterport>
  2. 启动命令
    redis-server 启动命令后面加入 --replicaof <masterip> <masterport>
  3. 客户端命令
    启动多个 Redis 实例后,直接通过客户端执行命令:replicaof <masterip> <masterport>,则该 Redis 实例成为从节点。

比如假设现在有实例 1(172.16.88.1)、实例 2(172.16.88.2)和实例 3 (172.16.88.3),在实例 2 和实例 3 上分别执行以下命令,实例 2 和 实例 3 就成为了实例 1 的从库,实例 1 成为 Master。

replicaof 172.16.88.1 6379

3. 主从复制原理

主从库模式一旦采用了读写分离,所有数据的写操作只会在主库上进行,不用协调三个实例。

主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。

65 哥:主从库同步是如何完成的呢?主库数据是一次性传给从库,还是分批同步?正常运行中又怎么同步呢?要是主从库间的网络断连了,重新连接后数据还能保持一致吗?

65 哥你问题咋这么多,同步分为三种情况:

  1. 第一次主从库全量复制;
  2. 主从正常运行期间的同步;
  3. 主从库间网络断开重连同步。

主从库第一次全量复制

65 哥:我好晕啊,先从主从库间第一次同步说起吧。

主从库第一次复制过程大体可以分为 3 个阶段:连接建立阶段(即准备阶段)、主库同步数据到从库阶段、发送同步期间新写命令到从库阶段

直接上图,从整体上有一个全局观的感知,后面具体介绍。

Redis全量同步

建立连接

该阶段的主要作用是在主从节点之间建立连接,为数据全量同步做好准备。从库会和主库建立连接,从库执行 replicaof 并发送 psync 命令并告诉主库即将进行同步,主库确认回复后,主从库间就开始同步了

65 哥:从库怎么知道主库信息并建立连接的呢?

在从节点的配置文件中的 replicaof 配置项中配置了主节点的 IP 和 port 后,从节点就知道自己要和那个主节点进行连接了。

从节点内部维护了两个字段,masterhost 和 masterport,用于存储主节点的 IP 和 port 信息。

从库执行 replicaof 并发送 psync 命令,表示要执行数据同步,主库收到命令后根据参数启动复制。命令包含了主库的 runID复制进度 offset 两个参数。

  • runID:每个 Redis 实例启动都会自动生成一个 唯一标识 ID,第一次主从复制,还不知道主库 runID,参数设置为 「?」。
  • offset:第一次复制设置为 -1,表示第一次复制,记录复制进度偏移量。

主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。

FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

主库同步数据给从库

第二阶段

master 执行 bgsave命令生成 RDB 文件,并将文件发送给从库,同时主库为每一个 slave 开辟一块 replication buffer 缓冲区记录从生成 RDB 文件开始收到的所有写命令。

从库收到 RDB 文件后保存到磁盘,并清空当前数据库的数据,再加载 RDB 文件数据到内存中。

发送新写命令到从库

第三阶段

从节点加载 RDB 完成后,主节点将 replication buffer 缓冲区的数据发送到从节点,Slave 接收并执行,从节点同步至主节点相同的状态。

65 哥:主库将数据同步到从库过程中,可以正常接受请求么?

主库不会被阻塞,Redis 作为唯快不破的男人,怎么会动不动就阻塞呢。

在生成 RDB 文件之后的写操作并没有记录到刚刚的 RDB 文件中,为了保证主从库数据的一致性,所以主库会在内存中使用一个叫 replication buffer 记录 RDB 文件生成后的所有写操作。

65 哥:为啥从库收到 RDB 文件后要清空当前数据库?

因为从库在通过 replcaof命令开始和主库同步前可能保存了其他数据,防止主从数据之间的影响。

replication buffer 到底是什么玩意?

一个在 master 端上创建的缓冲区,存放的数据是下面三个时间内所有的 master 数据写操作。

1)master 执行 bgsave 产生 RDB 的期间的写操作;

2)master 发送 rdb 到 slave 网络传输期间的写操作;

3)slave load rdb 文件把数据恢复到内存的期间的写操作。

Redis 和客户端通信也好,和从库通信也好,Redis 都分配一个内存 buffer 进行数据交互,客户端就是一个 client,从库也是一个 client,我们每个 client 连上 Redis 后,Redis 都会分配一个专有 client buffer,所有数据交互都是通过这个 buffer 进行的。

Master 先把数据写到这个 buffer 中,然后再通过网络发送出去,这样就完成了数据交互。

不管是主从在增量同步还是全量同步时,master 会为其分配一个 buffer ,只不过这个 buffer 专门用来传播写命令到从库,保证主从数据一致,我们通常把它叫做 replication buffer。

replication buffer 太小会引发的问题

replication buffer 由 client-output-buffer-limit slave 设置,当这个值太小会导致主从复制连接断开

1)当 master-slave 复制连接断开,master 会释放连接相关的数据。replication buffer 中的数据也就丢失了,此时主从之间重新开始复制过程。

2)还有个更严重的问题,主从复制连接断开,导致主从上出现重新执行 bgsave 和 rdb 重传操作无限循环。

当主节点数据量较大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与从节点之间的连接;

这种情况可能引起全量复制 -> replication buffer 溢出导致连接中断 -> 重连 -> 全量复制 -> replication buffer 缓冲区溢出导致连接中断……的循环。

具体详情:[top redis headaches for devops – replication buffer]
因而推荐把 replication buffer 的 hard/soft limit 设置成 512M。

config set client-output-buffer-limit "slave 536870912 536870912 0"
65 哥:主从库复制为何不使用 AOF 呢?相比 RDB 来说,丢失的数据更少。

这个问题问的好,原因如下:

  1. RDB 文件是二进制文件,网络传输 RDB 和写入磁盘的 IO 效率都要比 AOF 高。
  2. 从库进行数据恢复的时候,RDB 的恢复效率也要高于 AOF。

增量复制

65 哥:主从库间的网络断了咋办?断开后要重新全量复制么?

在 Redis 2.8 之前,如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。

从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。

增量复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效

repl\_backlog\_buffer

断开重连增量复制的实现奥秘就是 repl_backlog_buffer 缓冲区,不管在什么时候 master 都会将写指令操作记录在 repl_backlog_buffer 中,因为内存有限, repl_backlog_buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容

master 使用 master_repl_offset记录自己写到的位置偏移量,slave 则使用 slave_repl_offset记录已经读取到的偏移量。

master 收到写操作,偏移量则会增加。从库持续执行同步的写指令后,在 repl_backlog_buffer 的已复制的偏移量 slave\_repl\_offset 也在不断增加。

正常情况下,这两个偏移量基本相等。在网络断连阶段,主库可能会收到新的写操作命令,所以 master_repl_offset会大于 slave_repl_offset

repl_backlog_buffer

当主从断开重连后,slave 会先发送 psync 命令给 master,同时将自己的 runIDslave_repl_offset发送给 master。

master 只需要把 master_repl_offsetslave_repl_offset之间的命令同步给从库即可。

增量复制执行流程如下图:

Redis增量复制

65 哥:repl\_backlog\_buffer 太小的话从库还没读取到就被 Master 的新写操作覆盖了咋办?

我们要想办法避免这个情况,一旦被覆盖就会执行全量复制。我们可以调整 repl\_backlog\_size 这个参数用于控制缓冲区大小。计算公式:

repl_backlog_buffer = second * write_size_per_second
  1. second:从服务器断开重连主服务器所需的平均时间;
  2. write\_size\_per\_second:master 平均每秒产生的命令数据量大小(写命令和数据大小总和);

例如,如果主服务器平均每秒产生 1 MB 的写数据,而从服务器断线之后平均要 5 秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于 5 MB。

为了安全起见,可以将复制积压缓冲区的大小设为2 * second * write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理。

基于长连接的命令传播

65 哥:完成全量同步后,正常运行过程如何同步呢?

当主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,使用长连接的目的就是避免频繁建立连接导致的开销。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING 和 REPLCONF ACK。

主->从:PING

每隔指定的时间,主节点会向从节点发送 PING 命令,这个 PING 命令的作用,主要是为了让从节点进行超时判断。

从->主:REPLCONF ACK

在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:

REPLCONF ACK <replication_offset>

其中 replication\_offset 是从服务器当前的复制偏移量。发送 REPLCONF ACK 命令对于主从服务器有三个作用:

  1. 检测主从服务器的网络连接状态。
  2. 辅助实现 min-slaves 选项。
  3. 检测命令丢失, 从节点发送了自身的 slave\_replication\_offset,主节点会用自己的 master\_replication\_offset 对比,如果从节点数据缺失,主节点会从 repl_backlog_buffer缓冲区中找到并推送缺失的数据。注意,offset 和 repl\_backlog\_buffer 缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。

如何确定执行全量同步还是部分同步?

在 Redis 2.8 及以后,从节点可以发送 psync 命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制部分复制。本文以 Redis 2.8 及之后的版本为例。

关键就是 psync的执行:

增量与全量复制判断

  1. 从节点根据当前状态,发送 psync命令给 master:

    • 如果从节点从未执行过 replicaof ,则从节点发送 psync ? -1,向主节点发送全量复制请求;
    • 如果从节点之前执行过 replicaof 则发送 psync <runID> <offset>, runID 是上次复制保存的主节点 runID,offset 是上次复制截至时从节点保存的复制偏移量。
  2. 主节点根据接受到的psync命令和当前服务器状态,决定执行全量复制还是部分复制:

    • runID 与从节点发送的 runID 相同,且从节点发送的 slave_repl_offset 之后的数据在 repl_backlog_buffer 缓冲区中都存在,则回复 CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
    • runID 与从节点发送的 runID 不同,或者从节点发送的 slave\_repl\_offset 之后的数据已不在主节点的 repl_backlog_buffer 缓冲区中 (在队列中被挤出了),则回复从节点 FULLRESYNC <runid> <offset>,表示要进行全量复制,其中 runID 表示主节点当前的 runID,offset 表示主节点当前的 offset,从节点保存这两个值,以备使用。

一个从库如果和主库断连时间过长,造成它在主库 repl_backlog_buffer 的 slave\_repl\_offset 位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。

总结下

每个从库会记录自己的 slave_repl_offset,每个从库的复制进度也不一定相同。

在和主库重连进行恢复时,从库会通过 psync 命令把自己记录的 slave_repl_offset 发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。

replication buffer 和 repl\_backlog

  1. replication buffer 对应于每个 slave,通过 config set client-output-buffer-limit slave 设置。
  2. repl_backlog_buffer 是一个环形缓冲区,整个 master 进程中只会存在一个,所有的 slave 公用。repl\_backlog 的大小通过 repl-backlog-size 参数设置,默认大小是 1M,其大小可以根据每秒产生的命令、(master 执行 rdb bgsave) +( master 发送 rdb 到 slave) + (slave load rdb 文件)时间之和来估算积压缓冲区的大小,repl-backlog-size 值不小于这两者的乘积。

总的来说,replication buffer 是主从库在进行全量复制时,主库上用于和从库连接的客户端的 buffer,而 repl_backlog_buffer 是为了支持从库增量复制,主库上用于持续保存写操作的一块专用 buffer。

repl_backlog_buffer 是一块专用 buffer,在 Redis 服务器启动后,开始一直接收写操作命令,这是所有从库共享的。主库和从库会各自记录自己的复制进度,所以,不同的从库在进行恢复时,会把自己的复制进度(slave_repl_offset)发给主库,主库就可以和它独立同步。

如图所示:

repl_backlog与repl_buffer区别

4. 主从应用问题

4.1 读写分离的问题

数据过期问题

65 哥:主从复制的场景下,从节点会删除过期数据么?

这个问题问得好,为了主从节点的数据一致性,从节点不会主动删除数据。我们知道 Redis 有两种删除策略:

  1. 惰性删除:当客户端查询对应的数据时,Redis 判断该数据是否过期,过期则删除。
  2. 定期删除:Redis 通过定时任务删除过期数据。
65 哥:那客户端通过从节点读取数据会不会读取到过期数据?

Redis 3.2 开始,通过从节点读取数据时,先判断数据是否已过期。如果过期则不返回客户端,并且删除数据。

4.2 单机内存大小限制

如果 Redis 单机内存达到 10GB,一个从节点的同步时间在几分钟的级别;如果从节点较多,恢复的速度会更慢。如果系统的读负载很高,而这段时间从节点无法提供服务,会对系统造成很大的压力。

如果数据量过大,全量复制阶段主节点 fork + 保存 RDB 文件耗时过大,从节点长时间接收不到数据触发超时,主从节点的数据同步同样可能陷入全量复制->超时导致复制中断->重连->全量复制->超时导致复制中断……的循环。

此外,主节点单机内存除了绝对量不能太大,其占用主机内存的比例也不应过大:最好只使用 50% - 65% 的内存,留下 30%-45% 的内存用于执行 bgsave 命令和创建复制缓冲区等。

总结

  1. 主从复制的作用:AOF 和 RDB 二进制文件保证了宕机快速恢复数据,尽可能的防止丢失数据。但是宕机后依然无法提供服务,所以便演化出主从架构、读写分离。
  2. 主从复制原理:连接建立阶段、数据同步阶段、命令传播阶段;数据同步阶段又分为 全量复制和部分复制;命令传播阶段主从节点之间有 PING 和 REPLCONF ACK 命令互相进行心跳检测。
  3. 主从复制虽然解决或缓解了数据冗余、故障恢复、读负载均衡等问题,但其缺陷仍很明显:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制;这些问题的解决,需要哨兵和集群的帮助,我将在后面的文章中介绍,欢迎关注。
65 哥:码哥你的图画的真好看,内容好,跟着你的文章我收获了很多,我要收藏、点赞、在看和分享。让更多的优秀开发者看到共同进步!

谢谢读者支持,另外读者技术群也开通了,关注公众号回复「加群」,群里 N 多大厂的大佬,跟我一块交流,共同进步!

码哥字节

参考资料:

[1] redis 设计与实现(黄健宏)

[2] redis replication (http://redis.io/topics/replic...

[3] designing redis replication partial resync (http://antirez.com/news/31)

(4) Redis 核心技术与实战(https://time.geekbang.org/col...

查看原文

赞 32 收藏 25 评论 2

码哥字节 发布了文章 · 3月5日

图解 | 你管这玩意叫分布式架构?

编程是一门艺术,它的魅力在于创造。

65 哥已经工作两年了,一直做着简单重复的编程工作,活活熬成了一个只会 CRUD 的打工 boy。

65 哥:总是听大佬讲分布式分布式,什么才是分布式系统呢?

分布式系统是一个硬件或软件系统分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。在一个分布式系统中,一组独立的计算机展现给用户的是一个统一的整体,就好像是一个系统似的。系统拥有多种通用的物理和逻辑资源,可以动态的分配任务,分散的物理和逻辑资源通过计算机网络实现信息交换。

65 哥:巴拉巴拉,能不能讲点人话。俺听不懂。

那好,下面我们从平常最熟悉的事物开始理解分布式系统如何出现,发展的,并经过实践总结通用的理论,这些理论成为指导我们如何设计更完善的分布式系统的基础。

从此篇文章,你将学习到以下知识:

Web 应用的扩展

为什么会出现分布式应用?

65 哥:这个我也不清楚啊,以前我写的 web 应用都是直接扔进 Tomcat 中,启动 Tomcat 就可以访问了,这肯定不是分布式应用。

嗯,我们就从大家最熟悉 web 后台应用讲起,以前我们的系统访问量小,业务也不复杂,一台服务器一个应用就可以处理所有的业务请求了,后来我们公司发达了,访问量上去了,业务也拓展了,虽然老板依旧没有给我们加工资,确总是埋怨我们系统不稳定,扛不住大并发,是可忍孰不可也,加钱,我们要升级。

如果我们的服务器可以无限添加配置,那么一切性能问题都不是问题。

为提高系统处理能力,我们首先想到的扩展方式就是升级系统配置,8 核 cpu 升级为 32 核,64 核,内存 64G 升级为 128G,256G,带宽上万兆,十万兆,这就叫做垂直扩展。但这样的扩展终将无法持续下去,原因如下。

  1. 单机系统的处理能力最终会达到瓶颈
  2. 单机升级的边际成本将越来越大

worth

没有什么可以拦住我们编程打工人的步伐。

俗话说,系统撑不住了,就加服务器,一台不行就加两台。

垂直扩展到达技术瓶颈或投入产出比超过预期,我们可以考虑通过增加服务器数量来提高并发能力,这种方式就是水平扩展

系统拆分

65 哥:哦,这就是分布式系统了?这么简单的么。

我勒个呵呵,哪有那么简单,在水平扩展中,我们增加了服务器数量,但是如何让这些服务器像一个整体一样对外提供稳定有效的服务才是关键。既然已经有了多台服务器,我们就要考虑如何将系统部署到到不同的节点上去。

65 哥:这还不简单,我将我的 SpringBoot 项目部署到多台服务器上,前面加个 nginx 就可以了,现在我们的系统都是这样的,稳定高效 perfect。给你画个架构图(小声,这个我在学校时就会了。)

哪有什么岁月静好,只不过是有人在为你负重前行。上面你所认为的简单,其实有两个原因:

  1. 系统分离不彻底,很重要的一点,就是依然在共享一个数据库
  2. 在这个成熟的体系中,有太多成熟的中间件在为我们服务,比如上面提到的 nginx

系统拆分也有两种方式,垂直拆分水平拆分,注意,这里和上面提到的垂直扩展水平扩展不是处理同一个问题的。(65 哥:哈哈,我知道,世间万物不外乎纵横二字)。

系统的垂直拆分,就是将相同的系统部署多套,所有的节点并没有任何不同,角色和功能都一样,它们各自分担一部分功能请求,这样整个系统的处理能力的上升了。

从处理 web 请求上来看,垂直拆分的每个节点都处理一个完整的请求,每个节点都承担一部分请求量;

从数据存储的角度看,每个数据节点都存储相同的业务数据,每个节点存储一部分数据。

系统的水平拆分,就是将系统按不同模块或角色拆分,不同的模块处理不同的事情。

从 web 请求上来看,需要多个相互依赖的系统配合完成一个请求,每个节点处理的需求不一致;

从数据存储角度上来看,每个数据节点都存储着各自业务模块相关的数据,它们的数据都不一样。

上面垂直拆分之后各个节点组成的就是一个集群,而水平拆分各个节点就是分布式。这就是集群分布式的区别。集群除了上面提到的可以提高并发处理能力外,还可以保证系统的高可用,当一部分节点失效后,整个系统依旧可以提供完整的服务。分布式也一样,除了提高并发能力,解耦系统,使系统边界更清晰,系统功能更内聚也是其一大好处,所以在实际的系统中我们往往这两种方式同时都在使用,而且我们常常提及的分布式系统其实是包含着集群的概念在里面的。

split

分布式目标

归纳和演绎是人类理性的基石,学习和思考就是不断的归纳过去的经验,从而得到普遍的规律,然后将得之的规律演绎于其他事物,用于指导更好的实践过程。

上面我们讲解了分布式系统的由来,现在我们回顾和总结一下这个过程。我们引入分布式系统必然是基于现实的需求和目标而来的。

65 哥:那分布式的目标什么呢?

分布式就是为了满足以下目标而设计的:

  • Transparency: 透明性,即用户是不关心系统背后的分布式的,无论系统是分布式的还是单机的,对用户来说都应该是透明的,用户只需要关心系统可用的能力。这里的透明性就包括以下方面:

    • 访问透明性:固定统一的访问接口和方式,不因为分布式系统内部的变动而改变系统的访问方式。
    • 位置透明性:外部访问者不需要知道分布式系统具体的地址,系统节点的变动也不会影响其功能。
    • 并发透明性:几个进程能并发的使用共享资源而不互相干扰。
    • 复制透明性:使用资源的多个实例提升可靠性和性能,而用户和程序员无需知道副本的相关信息。
    • 故障透明性:分布式系统内部部分节点的故障不影响系统的整体功能。
    • 移动透明性:资源和客户能够在系统内移动而不受影响。
    • 性能透明性:负载变化时,系统能够被重新配置以提高性能。
    • 伸缩透明性:系统和应用能够进行扩展而不改变系统结构和应用算法。
  • Openness: 开放性,通用的协议和使用方式。
  • Scalability: 可伸缩性,随着资源数量的增加和用户访问的增加,系统仍然能保持其有效性,该系统就被称为可伸缩的。分布式系统应该在系统大小,系统管理方面都可扩展。
  • Performance: 性能,相对于单体应用,分布式系统应该用更加突出的性能。
  • Reliability: 可靠性,与单体系统相比,分布式系统应具有更好安全性,一致性和掩盖错误的能力。

分布式挑战

分布式的挑战来源于不确定性。想一想,分布式系统相对于单体应用,多了那些东西?

65 哥:有了更多的服务节点,还有就是服务之间的网络通信。

是的,看来 65 哥同学已经懂得思考和分析系统了。分布式系统的所有挑战就来源于这两者的不确定性。

  1. 节点故障:

节点数量越多,出故障的概率就变高了。分布式系统需要保证故障发生的时候,系统仍然是可用的,这就需要系统能够感知所有节点的服务状态,在节点发生故障的情况下将该节点负责的计算、存储任务转移到其他节点。

  1. 不可靠的网络:

节点间通过网络通信,我们都知道网络是不可靠的。可能的网络问题包括:网络分割、延时、丢包、乱序。相比单机过程调用,网络通信最让人头疼的是超时已经双向通行的不确定性。出现超时状态时,网络通信发起方是无法确定当前请求是否被成功处理的。

在不可靠的网络和节点中,分布式系统依然要保证其可用,稳定,高效,这是一个系统最基本的要求。因此分布式系统的设计和架构充满了挑战。

分而治之

分布式系统就是充分利用更多的资源进行并行运算和存储来提升系统的性能,这就是分而治之的原理。

65 哥:哦,懂了懂了,那 MapReduce 的 map,Elasticsearch 的 sharding,Kafka 的 partition 是不是都是分布式的分而治之原理。

可以啊,65 哥同学不仅能够归纳,还能够举一反三了。不错,无论是 map,sharding 还是 partition,甚至 请求路由负载均衡 都是在将计算或数据拆分,再分布到不同的节点计算和存储,从而提高系统的并发性。

不同集群类型的分

sharding

同样是,在不同领域的,甚至不同实现的系统中通常会有不同的说法。sharding 通常是在数据存储系统中将不同数据分布到不同节点的方式,中文通常翻译为数据分片

比如在 MongoDB 中,当 MongoDB 存储海量的数据时,一台机器可能不足以存储数据,也可能不足以提供可接受的读写吞吐量。这时,我们就可以通过在多台机器上分割数据,使得数据库系统能存储和处理更多的数据。

比如在 Elasticsearch 中,每个索引有一个或多个分片,索引的数据被分配到各个分片上,相当于一桶水用了 N 个杯子装。分片有助于横向扩展,N 个分片会被尽可能平均地(rebalance)分配在不同的节点上。

partition

partition的概念经常在 Kafka 中可以看到,在 kafka 中 topic 是一个逻辑概念,从分布式队列的角度看,topic 对使用者来说就是一个队列,topic 在 kafka 的具体实现中,由分布在不同节点上的 partition 组成,每个 partition 就是根据分区算法拆分的多个分区,在 kafka 中,同一个分区不能被同一个 group 下的多个 consumer 消费,所以一个 topic 有多少 partition 在一定意义上就表示这个 topic 具有多少并发处理能力。

在 Amazing 的分布式数据库DynamoDB中,一张表在底层实现中也被分区为不同的 partition。

load balance

负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。

比如 nginx 的负载均衡,通过不同的负载均衡分配策略,将 http 请求分发到 web 应用的不同节点之上,从而提高应用的并发处理能力。

比如 dubbo 的客户端负载能力,可以将 dubbo 请求路由到具体的 producer 提供节点上,负载均衡是一个完善的 RPC 所应该具有的能力。

在 Spring Cloud 的体系中 Robbin 组件可以通过 Spring Cloud 的各微服务之间通信的负载均衡分配问题,依旧是将请求分发到集群中的不同节点上去。

分的策略

无论是分区还是分片,还是分区路由,其实都有一些通用的分区算法,以下的概念可能很多同学都在不同的领域看到过,如上面看到的反向代理服务器 nginx 中,如分布式消息队列 kafka 中,如 RPC 框架 Dubbo 中,这有时候会让很多同学感到懵。

其实无论在什么领域中,你只要抓住它在完成的核心功能上就可以理解,它们就是在考虑如何的问题,把处理请求(即计算)如何均匀地分到不同的机器上,把数据如何分配到不同的节点上。

从大的方向看有两种策略,一种可复刻,一种不可复刻

可复刻,这种策略根据一定算法分配计算和数据,在相同的条件下,无论什么时间点得出的结果相同,因此对于相同条件的请求和数据来说是可复刻的,在不同时间点相同的请求和数据始终都在统一节点上。这种策略一般用于有数据状态在情况。

不可复刻,这种策略使用全随机方式,即使在相同的条件下,不同时间点得出的结果也不一致,因此也是不可还原的,如果只是为了可还原,如果通过元数据记录已经分配好的数据,之后需要还原时通过元数据就可以准确的得知数据所在位置了。

65 哥:这么神奇么?我想看看不同系统都有什么策略。

Dubbo 的负载均衡

Dubbo 是阿里开源的分布式服务框架。其实现了多种负载均衡策略。

Random LoadBalance

随机,可以按权重设置随机概率。在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

RoundRobin LoadBalance

轮询,按公约后的权重设置轮询比率。存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

LeastActive LoadBalance

最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

ConsistentHash LoadBalance

一致性 Hash,相同参数的请求总是发到同一提供者。 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

Kafka 的分区分配策略

Kafka 中提供了多重分区分配算法(PartitionAssignor)的实现:

RangeAssignor

RangeAssignor 策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个 Topic,RangeAssignor 策略会将消费组内所有订阅这个 Topic 的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。

RoundRobinAssignor

RoundRobinAssignor 的分配策略是将消费组内订阅的所有 Topic 的分区及所有消费者进行排序后尽量均衡的分配(RangeAssignor 是针对单个 Topic 的分区进行排序分配的)。

StickyAssignor

从字面意义上看,Sticky 是“粘性的”,可以理解为分配结果是带“粘性的”——每一次分配变更相对上一次分配做最少的变动(上一次的结果是有粘性的),其主要是为了实现以下两个目标:

  1. 分区的分配尽量的均衡
  2. 每一次重分配的结果尽量与上一次分配结果保持一致
65 哥:哇,看来优秀的系统都是相通的。

副本

副本是解决分布式集群高可用问题的。在集群系统中,每个服务器节点都是不可靠的,每个系统都有宕机的风险,如何在系统中少量节点失效的情况下保证整个系统的可用性是分布式系统的挑战之一。副本就是解决这类问题的方案。副本同样也可以提高并发处理能力,比如数据在不同的节点上可以读写分离,可以并行读等。

在这里其实也有很多说法,如 Master-Salve、Leader-Follower、Primary-Shard、Leader-Replica 等等。

Mysql 的主从架构

目前,大部分的主流关系型数据库都提供了主从热备功能,通过配置两台(或多台)数据库的主从关系,可以将一台数据库服务器的数据更新同步到另一台服务器上。这既可以实现数据库的读写分离,从而改善数据库的负载压力,也可以提高数据高可用,多份数据备份降低了数据丢失的风险。

Elasticsearch 的副本机制

在 ES 中有主分片和副本分片的概念。副本分片的主要目的就是为了故障转移,如果持有主分片的节点挂掉了,一个副本分片就会晋升为主分片的角色从而对外提供查询服务。

CAP 理论

在理论计算机科学中,CAP 定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足分布式系统一致性、可用性和分区容错(即 CAP 中的"C","A"和"P"):

65 哥:什么是一致性、可用性和分区容错性能?
  • 一致性 (Consistency)

一致性意味着所有客户端同时看到相同的数据,无论它们连接到哪个节点。要发生这种情况,每当将数据写入一个节点时,必须立即将数据转发或复制到系统中的所有其他节点,然后才能将写入视为"成功"。

  • 可用性 (Availability)

任何客户端的请求都能得到响应数据,不会出现响应错误。换句话说,可用性是站在分布式系统的角度,对访问本系统的客户的另一种承诺:我一定会给您返回数据,不会给你返回错误,但不保证数据最新,强调的是不出错。

  • 分区容错

分区即分布式系统中的通信中断,两个节点之间的丢失或暂时延迟的连接。分区容错意味着群集必须继续工作,尽管系统中的节点之间存在的通信故障。

这三种性质进行俩俩组合,可以得到下面三种情况:

  • CA:完全严格的仲裁协议,例如 2PC(两阶段提交协议,第一阶段投票,第二阶段事物提交)
  • CP:不完全(多数)仲裁协议,例如 Paxos、Raft
  • AP:使用冲突解决的协议,例如 Dynamo、Gossip

CA 和 CP 系统设计遵循的都是强一致性理论。不同的是 CA 系统不能容忍节点发生故障。CP 系统能够容忍 2f+1 个节点中有 f 个节点发生失败。

Base 理论

CAP 理论表明,对于一个分布式系统而言,它是无法同时满足 Consistency(强一致性)、Availability(可用性) 和 Partition tolerance(分区容忍性) 这三个条件的,最多只能满足其中两个。

在分布式环境中,我们会发现必须选择 P(分区容忍)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。也就是说分区容错性是分布式系统的一个最基本要求。

CAP 定理限制了我们三者无法同时满足,但我们可以尽量让 C、A、P 都满足,这就是 BASE 定理。

BASE 理论是 Basically Available(基本可用),Soft State(软状态)和 Eventually Consistent(最终一致性)三个短语的缩写。既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

基本可用 (Basically Available)

基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务,这就是损失部分可用性的体现。

软状态 ( Soft State)

什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。

软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

最终一致性 ( Eventual Consistency)

最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。

弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

BASE 理论面向的是大型高可用、可扩展的分布式系统。与传统 ACID 特性相反,不同于 ACID 的强一致性模型,BASE 提出通过牺牲强一致性来获得可用性,并允许数据段时间内的不一致,但是最终达到一致状态。

分布式是系统扩展的必然方向,分布式系统所遇到的问题是普遍,随着大量优秀的项目在分布式的道路上披荆斩棘,前人已经总结了大量丰富的理论。并且不同领域的分布式系统也层出不穷,我们既应该学习好这些好的理论知识,也应该去多看看不同分布式系统的实现,总结它们的共性,发现它们在不同领域独特的亮点权衡,更重要的,我们应该将所学用于日常项目的实践当中,也应该在实践中总结出更多的规律理论。

感谢读者看完本文,码哥将为读者持续输出高质量的文章,下期我们继续深入讲讲分布式一致性的问题和解决方案,敬请关注。
码哥字节

查看原文

赞 20 收藏 15 评论 2

码哥字节 发布了文章 · 2月23日

Redis 日志篇:RDB与AOF为宕机恢复保驾护航

特立独行是对的,融入圈子也是对的,重点是要想清楚自己向往怎样的生活,为此愿意付出怎样的代价。

我们通常将 Redis 作为缓存使用,提高读取响应性能,一旦 Redis 宕机,内存中的数据全部丢失,假如现在直接访问数据库大量流量打到 MySQL 可能会带来更加严重的问题。

另外慢慢的从数据库读取放到 Redis 性能必然比不过从 Redis 获取快,也会导致响应变慢。

Redis 为了实现无畏宕机快速恢复,设计了两大杀手锏,分别是 AOF(Append Only FIle)日志和 RDB 快照。

学习一个技术,通常只接触了零散的技术点,没有在脑海里建立一个完整的知识框架和架构体系,没有系统观。这样会很吃力,而且会出现一看好像自己会,过后就忘记,一脸懵逼。

跟着「码哥字节」一起吃透 Redis,深层次的掌握 Redis 核心原理以及实战技巧。搭建一套完整的知识框架,学会全局观去整理整个知识体系。

本文硬核,建议收藏点赞,静下心来阅读,我相信都会有很多收获。

上一篇《Redis 核心篇:唯快不破的秘密》分析了 Redis 的核心数据结构、IO 模型、线程模型、根据不同数据使用合适的数据编码。深层次掌握真正快的原因!

本篇将围绕如下几点展开:

  • 宕机后,如何快速恢复?
  • 宕机了,Redis 如何避免数据丢失?
  • 什么是 RDB 内存快照?
  • AOF 日志实现机制
  • 什么是 写时复制技术?
  • ….

涉及的知识点如图所示:

Redis 日志篇:无畏宕机与快速恢复的杀手锏

Redis 全景图

全景图可以围绕两个维度展开,分别是:

应用维度:缓存使用、集群运用、数据结构的巧妙使用

系统维度:可以归类为三高

  1. 高性能:线程模型、网络 IO 模型、数据结构、持久化机制;
  2. 高可用:主从复制、哨兵集群、Cluster 分片集群;
  3. 高拓展:负载均衡

Redis 系列篇章围绕如下思维导图展开,这次一起探索 Redis 的高性能、持久化机制的秘密。

吃透Redis

拥有全景图,掌握系统观。

系统观其实是至关重要的,从某种程度上说,在解决问题时,拥有了系统观,就意味着你能有依据、有章法地定位和解决问题。

RDB 内存快照,让宕机快速恢复

65 哥:Redis 因为某些原因宕机了,会导致所有的流量会打到后端 MySQL,我立马重启 Redis,可是它的数据存在内存里面,重启后如何还是没有任何数据,如何防止重启数据丢失呢?

65 哥别急,「码哥字节」带你一步步深入理解到底 Redis 宕机后如何快速恢复的。

Redis 数据存储在内存中,是否可以考虑将内存中的数据写到磁盘上呢?当 Redis 重启的时候就把保存在磁盘上的数据快速恢复到内存中,这样就能实现重启后正常提供服务了。

65 哥:我想到一个方案,每次执行「写」操作操作内存的同时写入到磁盘

这个方案有一个致命问题:每次写指令不仅写内存还是写入磁盘,磁盘的性能相对内存太慢,会导致 Redis 性能大大降低。

内存快照

65 哥:那如何规避这个同时写入的问题呢?

我们通常将 Redis 当作缓存使用,所以即使 Redis 没有保存全部数据,还可以通过数据库获取,所以 Redis 不会保存所有的数据, Redis 的数据持久化使用了「RDB 数据快照」的方式来实现宕机快速恢复。

65 哥:那什么是 RDB 内存快照呢?

在 Redis 执行「写」指令过程中,内存数据会一直变化。所谓的内存快照,指的就是 Redis 内存中的数据在某一刻的状态数据。

好比时间定格在某一刻,当我们拍照的,通过照片就能把某一刻的瞬间画面完全记录下来。

Redis 跟这个类似,就是把某一刻的数据以文件的形式拍下来,写到磁盘上。这个快照文件叫做 RDB 文件,RDB 就是 Redis DataBase 的缩写。

Redis 通过定时执行 RDB 内存快照,这样就不必每次执行「写」指令都写磁盘,只需要在执行内存快照的时候写磁盘。既保证了唯快不破,还实现了持久化,宕机快速恢复。

RDB内存快照

在做数据恢复时,直接将 RDB 文件读入内存完成恢复。

65 哥:对哪些数据做快照呢?或者多久做一次快照呢?这个会影响快照的执行效率。

65 哥不错呀,开始考虑数据效率问题了。在《Redis 核心篇:唯快不破的秘密》中我们知道他的单线程模型决定了我们要尽可能的避免会阻塞主线程的操作,避免 RDB 文件生成阻塞主线程。

生成 RDB 策略

Redis 提供了两个指令用于生成 RDB 文件:

  • save: 主线程执行,会阻塞;
  • bgsave:调用 glibc 的函数fork产生一个子进程用于写入 RDB 文件,快照持久化完全交给子进程来处理,父进程继续处理客户端请求,生成 RDB 文件的默认配置。
65 哥:那在对内存数据做「快照」的时候,内存数据还能修改么?也就是写指令能否正常处理?

首先我们要明确一点,避免阻塞和 RDB 文件生成期间能处理写操作不是一回事。虽然主线程没有阻塞,到那时为了保证快照的数据的一致性,只能处理读操作,不能修改正在执行快照的数据。

很明显,为了生成 RDB 而暂停写操作,Redis 是不答应的。

65 哥:那 Redis 如何实现一边处理写请求,同时生成 RDB 文件呢?

Redis 使用操作系统的多进程写时复制技术 COW(Copy On Write) 来实现快照持久化,这个机制很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标。

Redis 在持久化时会调用 glibc 的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。

子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这时你可以将父子进程想像成一个连体婴儿,共享身体。

这是 Linux 操作系统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。

bgsave 子进程可以共享主线程的所有内存数据,读取主线程的数据并写入到 RDB 文件。

在执行 SAVE 命令或者BGSAVE命令创建一个新的 RDB 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中。

当主线程执行写指令修改数据的时候,这个数据就会复制一份副本, bgsave 子进程读取这个副本数据写到 RDB 文件,所以主线程就可以直接修改原来的数据。

写时复制技术保证快照期间数据客修改

这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

Redis 会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主线程同时可以修改数据。

65 哥:那可以每秒都执行 RDB 文件么,这样即使发生宕机最多丢失 1 秒的数据。

过于频繁的执行全量数据快照,有两个严重性能开销:

  1. 频繁生成 RDB 文件写入磁盘,磁盘压力过大。会出现上一个 RDB 还未执行完,下一个又开始生成,陷入死循环。
  2. fork 出 bgsave 子进程会阻塞主线程,主线程的内存越大,阻塞时间越长。

优缺点

快照的恢复速度快,但是生成 RDB 文件频率不好把握,频率过低宕机丢失的数据就会比较多;太快,又会消耗额外开销。

RDB 采用二进制 + 数据压缩的方式写磁盘,文件体积小,数据恢复速度快。

Redis 除了 RDB 全量快照以外,还设计了 AOF 写后日志,接下来我们一起来聊下什么是 AOF 日志。

AOF 写后日志,避免宕机数据丢失

AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。

假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内存数据结构的状态。

写前与写后日志对比

写前日志(Write Ahead Log, WAL): 在实际写数据之前,将修改的数据写到日志文件中,故障恢复得以保证。

比如 MySQL Innodb 存储引擎 中的 redo log(重做日志)便是记录修改的数据日志,在实际修改数据前先记录修改日志在执行修改数据。

写后日志: 先执行「写」指令请求,将数据写入内存,再记录日志。

AOF写后日志

日志格式

当 Redis 接受到 「set key MageByte」命令将数据写到内存后,Redis 会按照如下格式写入 AOF 文件。

  • 「*3」:表示当前指令分为三个部分,每个部分都是 「$ + 数字」开头,紧跟后面是该部分具体的「指令、键、值」。
  • 「数字」:表示这部分的命令、键、值多占用的字节大小。比如 「$3」表示这部分包含 3 个字节,也就是 「set」指令。

AOF 日志格式

65 哥:为什么 Redis 使用写后日志这种方式呢?

写后日志避免了额外的检查开销,不需要对执行的命令进行语法检查。如果使用写前日志的话,就需要先检查语法是否有误,否则日志记录了错误的命令,在使用日志恢复的时候就会出错。

另外,写后才记录日志,不会阻塞当前的「写」指令执行。

65 哥:那有了 AOF 就万无一失了么?

傻孩子,可没这么简单。假如 Redis 刚执行完指令,还没记录日志宕机了,就有可能丢失这个命令相关的数据。

还有,AOF 避免了当前命令的阻塞,但是可能会给下一个命令带来阻塞的风险。AOF 日志是主线程执行,将日志写入磁盘过程中,如果磁盘压力大就会导致写磁盘很慢,导致后续的「写」指令阻塞。

发现了没,这两个问题与磁盘写回有关,如果能合理的控制「写」指令执行完后 AOF 日志写回磁盘的时机,问题就迎刃而解。

写回策略

为了提高文件的写入效率,当用户调用 write 函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。

这种做法虽然提高了效率,但也为写入数据带来了安全问题,因为如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失。

为此,系统提供了fsyncfdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。

Redis 提供的 AOF 配置项appendfsync写回策略直接决定 AOF 持久化功能的效率和安全性。

  • always:同步写回,写指令执行完毕立马将 aof_buf缓冲区中的内容刷写到 AOF 文件。
  • everysec:每秒写回,写指令执行完,日志只会写到 AOF 文件缓冲区,每隔一秒就把缓冲区内容同步到磁盘。
  • no: 操作系统控制,写执行执行完毕,把日志写到 AOF 文件内存缓冲区,由操作系统决定何时刷写到磁盘。

没有两全其美的策略,我们需要在性能和可靠性上做一个取舍。

always同步写回可以做到数据不丢失,但是每个「写」指令都需要写入磁盘,性能最差。

everysec每秒写回,避免了同步写回的性能开销,发生宕机可能有一秒位写入磁盘的数据丢失,在性能和可靠性之间做了折中。

no操作系统控制,执行写指令后就写入 AOF 文件缓冲就可以执行后续的「写」指令,性能最好,但是有可能丢失很多的数据。

65 哥:那我该如何选择策略呢?

我们可以根据系统对高性能和高可靠性的要求,来选择写回策略。总结一下:想要获得高性能,就选择 No 策略;如果想要得到高可靠性保证,就选择 Always 策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择 Everysec 策略。

优缺点

优点:执行成功才记录日志,避免了指令语法检查开销。同时,不会阻塞当前「写」指令。

缺点:由于 AOF 记录的是一个个指令内容,具体格式请看上面的日志格式。故障恢复的时候需要执行每一个指令,如果日志文件太大,整个恢复过程就会非常缓慢。

另外文件系统对文件大小也有限制,不能保存过大文件,文件变大,追加效率也会变低。

日志过大:AOF 重写机制

65 哥:AOF 日志文件过大着怎么办?

AOF 写前日志,记录的是每个「写」指令操作。不会像 RDB 全量快照导致性能损耗,但是执行速度没有 RDB 快,同时日志文件过大也会造成性能问题,对于唯快不破的 Redis 这个真男人来说,绝对不能忍受日志过大导致的问题。

所以,Redis 设计了一个杀手锏「AOF 重写机制」,Redis 提供了 bgrewriteaof指令用于对 AOF 日志进行瘦身。

其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

65 哥:为啥 AOF 重写机制能缩小日志文件呢?

重写机制有「多变一」功能,将旧日志中的多条指令,在重写后就变成了一条指令。

如下所示:

三条 LPUSH 指令,经过 AOF 重写后生成一条,对于多次修改的场景,缩减效果更加明显。

AOF重写机制(纠错:3条变一条)

65 哥:重写后 AOF 日志变小,最后把整个数据库最新数据的操作日志刷写到磁盘了。重写会不会阻塞主线程呢?

「码哥」上文说了,AOF 日志是主线程写回的,AOF 重写的过程实际上后台子进程 bgrewriteaof 完成,防止阻塞主线程。

重写过程

和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。

总的来说,一共出现 两个日志,一次拷内存数据拷贝,分别是旧的 AOF 日志和新的 AOF 重写日志和 Redis 数据拷贝

Redis 会将重写过程中的接收到的「写」指令操作同时记录到旧的 AOF 缓冲区和 AOF 重写缓冲区,这样重写日志也保存最新的操作。等到拷贝数据的所有操作记录重写完成后,重写缓冲区记录的最新操作也会写到新的 AOF 文件中。

每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于遍历数据生成重写记录;使用两个日志保证在重写过程中,新写入的数据不会丢失,并且保持数据一致性。

AOF 重写过程

65 哥:AOF 重写也有一个重写日志,为什么它不共享使用 AOF 本身的日志呢?

这个问题问得好,有以下两个原因:

  1. 一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。
  2. 如果 AOF 重写过程中失败了,那么原本的 AOF 文件相当于被污染了,无法做恢复使用。所以 Redis AOF 重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的 AOF 文件产生影响。等重写完成之后,直接替换旧文件即可。

Redis 4.0 混合日志模型

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。

Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。

于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

所以 RDB 内存快照以稍微慢一点的频率执行,在两次 RDB 快照期间使用 AOF 日志记录期间发生的所有「写」操作。

这样快照就不用频繁的执行,同时由于 AOF 只需要记录两次快照之间发生的「写」指令,不需要记录所有的操作,避免出现文件过大的情况。

总结

Redis 设计了 bgsave 和写时复制,尽可能避免执行快照期间对读写指令的影响,频繁快照会给磁盘带来压力以及 fork 阻塞主线程。

Redis 设计了两大杀手锏实现了宕机快速恢复,数据不丢失。

避免日志过大,提供了 AOF 重写机制,根据数据库的数据最新状态,生成数据的写操作作为新日志,并且通过后台完成不阻塞主线程。

综合 AOF 和 RDB 在 Redis 4.0 提供了新的持久化策略,混合日志模型。在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

最后,关于 AOF 和 RDB 的选择问题,「码 哥 字 节」有三点建议:

  • 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
  • 如果允许分钟级别的数据丢失,可以只使用 RDB;
  • 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。

经过两篇 Redis 系列文章,读者朋友们对 Redis 应该有一个全局认识。

下一篇「码哥」将带来一个实战,《Redis 高可用篇:主从架构的奥秘》 实战 + 原理呈现给大家!

敬请期待......

码哥字节

硬核好文

Redis 核心篇:唯快不破的秘密

Tomcat 架构原理解析到设计借鉴

从面试角度一文学完 kafka

从 JMM 透析 volatile 与 synchronized 原理

关注「码哥字节」,每次都是炸裂硬核。阅读后如有收获请「点赞、分享、收藏」,谢谢支持.

鸣谢

redis 核心技术与实战: https://time.geekbang.org/col...
redis 深度历险:核心原理与应用实践: https://juejin.cn/book/684473...
redis 设计与实践: https://weread.qq.com/web/rea...

查看原文

赞 6 收藏 5 评论 0

认证与成就

  • 认证信息 后端工程师
  • 获得 311 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2019-06-15
个人主页被 10.7k 人浏览