CrabNote螃蟹笔记

为什么要有 Redis Cluster 集群?

本文《为什么要有 Redis Cluster 集群?》由小宇宙从原站整理搬运,详细讲解相关技术要点。

原文地址:字节二面:你知道 Redis Cluster 集群工作原理吗? (opens new window) 大家好,我是小宇宙。 今天想跟大家聊聊 Redis Cluster 集群这个话题。 说起来这也是面试里的老常客了,有位读者前两天面字节的时候就被问到了这么一道题: Redis Cluster 集群的工作原理是什么?客户端是怎么知道该访问哪个分片的? 这个问题说小不小,说大不大,难点是里面涉及的概念挺多的,像哈希槽、MOVED 重定向、ASK 重定向、Gossip 协议、故障转移,稍微抓不住主线就会越讲越乱。 所以这篇文章,小宇宙准备顺着一条主线把这些概念一个一个串起来讲明白。读完之后,你不仅能把这道面试题轻松答下来,更重要的是,会对 Redis 集群的核心工作原理有一个完整的认识。 我们不急着讲「Cluster 是什么」,先来想一个问题。 1. 为什么我们需要 Redis Cluster? 要理解一样东西「为什么要有」,最好的办法就是先看看「没有它的时候,我们遇到了什么问题」。 1.1 单机 Redis 能扛多久? 一开始,大家用 Redis 都是单机部署,就一台机器,跑起来就能用,简单粗暴。但这种部署方式有两个天生的坑。 第一个坑是可用性 。这台机器一旦宕机,整个服务就 GG 了,所有依赖 Redis 的业务全都得跟着歇菜,体验极差。 第二个坑是容量 。一台机器的内存总是有上限的,你买得起 64G、128G 甚至 512G 的服务器,但内存不可能无限堆。而且你可能还没意识到,Redis 的内存越大,持久化时 fork 子进程的耗时就越长,阻塞主线程的风险也就越高,到后面甚至会出现「内存够用但 Redis 已经带不动」的情况。 为了解决第一个坑,Redis 先后给出了两个方案: 主从复制 和 哨兵 。 主从复制让一个主节点带若干个从节点,数据从主同步到从。主挂了,从还能兜一下读请求。但这个方案有个尴尬的地方:主挂了之后,谁来把从节点切换成新的主?只能靠人肉上去搞,显然不够自动。 于是有了哨兵。哨兵的作用就是一群「看门大爷」,它们 7x24 小时盯着主节点,一旦发现主节点挂了,就自动从从节点里挑一个出来接班,全程不需要人工介入。到这一步,Redis 的 高可用 问题算是基本解决了。 但请注意,我们刚才解决的只是 第一个坑 ,第二个坑还在那儿趴着呢。 1.2 哨兵解决不了的问题 我问大家一个问题:哨兵模式下,主节点和从节点里保存的数据是一样的还是不一样的? 答案是 完全一样的 。因为从节点就是主节点的复制品,主有什么它就有什么。 这就意味着,不管你挂多少个从节点,整个系统能存下的数据量 还是一台机器的内存上限 。比如你主节点是 64G 内存,配了 5 个从节点,总的集群内存是 吗?不是的,还是 64G,其余的几台机器都是拿来做冗余备份的,存的是 同一份数据 。 那如果你的业务数据就是要存 200G 呢?这时候光靠哨兵就完全无能为力了。 怎么办?其实大家稍微动动脑子就能想到一个朴素的办法: 既然一台机器装不下,那就把数据拆开,存到好几台机器上 。比如一台存一部分,另一台存另一部分,每台机器只负责一小块,合起来才是完整的数据集。 这个思路有个专门的名字,叫「 切片集群 」,英文叫 sharding。它的核心思想就一句话: 把一个大数据集切成多份,分别存到多台机器上,让整个集群的容量随机器数量线性扩展。 切片集群只是一个通用概念,具体怎么实现其实有好几种方案,比如客户端自己分片、用代理(像 Codis、Twemproxy)来分片等等。而我们今天要聊的 Redis Cluster ,就是 Redis 官方从 3.0 版本开始提供的切片集群方案 ,属于「亲儿子」级别的。 所以你看,Cluster 要解决的核心问题其实就一个: 怎么把数据分散到多台 Redis 上,同时还要保证这些 Redis 合起来用起来像一台一样顺手 。 顺着这个问题往下想,下一步要回答的自然就是:数据到底该怎么分呢? 2. 数据怎么分?哈希槽到底是个什么东西 要把数据切到多台机器上,最核心的问题就是: 给你一个 key,怎么决定它该存到哪台机器上 ? 这个问题如果让你来设计,你会怎么做?我们一步一步来。 2.1 最朴素的办法:key 直接对机器数取模 最容易想到的办法是这样:假设有 3 台 Redis,对 key 算一个哈希值,然后用这个哈希值除以 3 取余,余数是 0 就放第一台,是 1 就放第二台,是 2 就放第三台。简单,公平,一看就会。 但这个方案有一个致命的问题,小宇宙举个例子你就明白了。 假设现在集群里有 3 台机器,你有一个 key 叫 ,算出来 hash 是 6, ,所以这个 key 存在第一台机器上。 过了一段时间,业务数据越来越多,你又加了一台机器,现在集群里有 4 台了。客户端再来访问 ,这次算出来 ,跑去第三台机器找,结果发现根本没有。为啥?因为数据还在第一台呢。 这时候你就会发现, 每次加机器或者减机器,几乎所有 key 的位置都要重新计算 ,意味着你几乎要把整个集群的数据重新搬一遍。这个成本高得离谱,业务根本受不了。 2.2 升级一下:引入一个「中间层」 有没有办法让扩缩容的成本小一点? 聪明的工程师想出了一个办法: 不让 key 直接对应机器,而是在 key 和机器中间加一层「槽」 。具体是这样的: 先预设好一个固定数量的「槽」,比如说 16384 个,这些槽的数量是 写死的,永远不变 。然后: key 不再直接映射到机器,而是先映射到某个槽; 机器不再直接装 key,而是「认领」一批槽,装在这批槽里的 key 就归它管。 一图胜千言,整个关系大概是这样的: 这样一来有什么好处呢?你想想看,当集群扩容增加一台机器时,我只需要 从已有的机器里匀一部分槽出来给新机器 就行了,槽里面装的 key 根本不用动。因为对 key 来说,它只认自己属于哪个槽,至于这个槽归哪台机器管,那是集群内部的事情,跟 key 没半点关系。 换句话说, 扩缩容时迁移的粒度从「一个个 key」变成了「一批批槽」 ,而槽的数量是固定的、迁移起来也可控多了。 这就是 Redis Cluster 哈希槽的设计思想,说白了就是在 key 和机器之间加了一层「解耦」。 2.3 那 key 是怎么算出槽号的? Redis Cluster 用的是一个叫 CRC16 的哈希算法。每个进入集群的 key,都会先用 CRC16 算出一个哈希值,然后对 16384 取模,得到的结果就是这个 key 所属的槽号,取值范围是 。 用一个公式表达就是: 小宇宙知道你现在肯定在心里嘀咕: 为啥偏偏是 16384 呢?不是 65536,不是 8192,怎么就选了这么一个奇怪的数字 ?这里小宇宙先卖个关子,文章最后会专门用一节来聊这个问题,现在我们先继续往下。 2.4 槽是怎么分给各个节点的 槽算出来了,具体哪个槽归哪台机器呢?这个是在 集群初始化 的时候就安排好的。Redis 会把 16384 个槽尽量平均分给每个主节点。比如说你有 3 个主节点 A、B、C,那一种常见的分配是: 节点 A 负责 0 5460 号槽 节点 B 负责 5461 10922 号槽 节点 C 负责 10923 16383 号槽 合起来正好把 16384 个槽全部覆盖完,一个不多一个不少。集群里每个节点都会在本地维护一份完整的「哪个槽归谁管」的映射表,节点之间也会不停地互相同步这份信息(具体怎么同步的我们后面讲 Gossip 的时候会说)。 好,现在你已经理解了数据怎么分。但是新问题又来了: 客户端发一个 命令的时候,它怎么知道要发给哪一台 Redis 呢 ?总不能先挨个问一遍吧? 3. 客户端是怎么知道该访问哪台机器的? 这一节是整个 Redis Cluster 里最容易让初学者迷糊的地方,小宇宙慢慢给你讲。 3.1 最笨的办法:每次都让服务端告诉我 我们可以先设想一个很笨的办法:客户端随便挑一台节点发请求,如果发对了那最好,如果发错了,就让服务端返回一个「你应该去找哪个节点」的提示,然后客户端再重新发一次。 这个办法能用吗?能用,但性能太差了,每次请求最坏都要走两趟,实际生产里没人会这么干。 3.2 真实方案:客户端自己缓存一份「槽 → 节点」的地图 Redis Cluster 真正的做法是这样的: 客户端在启动的时候,会主动去任意一个节点拉一份「哪个槽归哪台节点管」的映射表,把它缓存在自己本地 。 你可以把这份表想象成一张地图,上面清清楚楚写着「0 5460 号槽在 A 节点」「5461 10922 号槽在 B 节点」等等。 之后客户端每次要访问一个 key,就会先在本地做三件事: 1. 用 CRC16 算出这个 key 属于哪个槽; 2. 查本地地图,看看这个槽归哪个节点; 3. 直接发请求给那个节点。 整个过程 全是本地计算 ,不需要问任何人,所以一次请求就能命中,效率非常高。这种带着本地缓存地图的客户端,我们一般叫它 Smart Client (智能客户端)。Java 的 Jedis、Lettuce,Go 的 go redis,甚至你用 加了 参数后,都是 Smart Client 的行为。 3.3 地图过期了怎么办?MOVED 重定向出场 不过你肯定马上会问: 这张地图是客户端启动时一次性拉下来的,万一集群后面扩容或者缩容了,槽的归属变了,客户端本地地图不就过期了吗 ? 这个问题 Redis Cluster 当然也考虑到了,它用的解决方案叫 MOVED 重定向 。 过程是这样的:假设客户端本地地图里记的是「槽 100 在 A 节点」,但实际上因为扩容,槽 100 已经被迁到了 B 节点。客户端还是按照老地图把请求发给 A,A 一看这个槽自己早就不管了,于是 不会去处理这个请求,而是返回一个 MOVED 错误 ,错误里带上新的地址: 意思是:「兄弟,槽 100 已经搬到 192.168.1.2:6379 那儿去了,你去找它」。 客户端收到 MOVED 之后做两件事: 1. 立刻更新本地地图 ,把槽 100 的归属改成 B 节点; 2. 重新把请求发给 B 节点,完成这次操作。 注意第一步,这非常关键。因为更新了本地地图之后, 下次再访问属于槽 100 的 key,就能一次命中了 ,不会再走一趟错路。所以 MOVED 错误虽然偶尔会出现,但客户端会「越跑越聪明」,不会一直走冤枉路。 流程图长这样: 3.4 正在搬家中的槽怎么办?ASK 重定向 还有一种更微妙的情况: 槽不是已经搬完了,而是正在搬的过程中 。 想象一下这个场景:我们正在把槽 100 从节点 A 迁到节点 B,迁移是一个 key 一个 key 搬的,当前的状态是:槽 100 里有一部分 key 已经被搬到 B 了,另一部分 key 还留在 A。 这时候客户端按照旧地图发请求去 A 访问 ,A 一查: 如果 还在自己这里,那正常处理返回结果,没什么事; 如果 已经被搬到 B 了,这时候该怎么办? A 不能直接返回 MOVED,为啥?你想啊,MOVED 的语义是「这个槽 永久 归 B 了」,可实际情况是槽 100 里还有一部分 key 没搬完,这个槽 整体上 还是归 A 管。如果这时候让客户端更新地图,把整个槽 100 都改到 B,客户端下次访问留在 A 的那些 key 就全部找错地方了。 所以 Redis 设计了一个新的错误叫 ASK 重定向 ,错误消息长这样: 它的含义是:「这个 key 这一次 你去 B 节点问问看,但这只是临时的,不要更新你的地图」。 客户端收到 ASK 之后会做两件事: 1. 不更新本地地图 ,槽 100 的归属还是 A; 2. 先给 B 发一个特殊的命令叫 ,然后再发真正的请求。 为啥要先发个 呢?因为这时候 B 还没正式接管槽 100,如果你直接发请求,B 反而会返回一个 MOVED 让你回去找 A(因为 B 觉得这个槽此刻不归自己)。 的作用就是告诉 B:「我知道这个槽你还没正式接管,但我是从 A 那边被 ASK 过来的,你就破个例给我处理一下这次请求吧」。B 收到 之后就会临时处理一次,仅此一次,下次你还得重新发 ASKING。 所以 MOVED 和 ASK 的核心区别 就在这里,小宇宙用一句话帮你记住: MOVED 是「永久搬家通知」,客户端要更新地图;ASK 是「临时借道通知」,客户端这一次跳过去,地图不动。 这个区别看似是细节,实际上是 Redis Cluster 能够在 不停机 的情况下完成数据迁移的关键。没有 ASK 重定向,迁移过程中的请求就没法正确处理了,整个集群动起来就会磕磕绊绊的。 4. 节点之间怎么同步信息?聊聊 Gossip 协议 到目前为止,我们讨论的都还停留在「客户端 ↔ 单个节点」这个层面。但别忘了 Cluster 是一堆节点组成的,这些节点之间也需要互相通信。 为啥需要通信呢?因为整个集群的状态是动态变化的: 有新节点加入进来 有节点挂掉下线 有槽在节点之间迁移 有主从切换发生 这些变化不能只有当事人知道,整个集群的每一个节点都得知道,不然就乱套了。 4.1 两种思路:中心化 vs 去中心化 要让所有节点都掌握集群的最新状态,大的思路上有两种。 第一种是中心化 :专门搞一个(或一小撮)「管理员」角色,所有节点都向它汇报状态,所有节点也都从它那儿读最新状态。哨兵模式本质上就是这个路子。但中心化有个明显的问题: 管理员自己如果挂了呢 ?虽然可以再给管理员做高可用,但整个架构会变得很复杂,而且维护这批管理员本身也是个成本。 第二种是去中心化 :干脆不要管理员了,让节点之间直接互相聊天,各自把自己知道的东西告诉别人,这样最终大家的信息都会慢慢同步到一致。这种方式天生没有单点,也不需要额外的组件。 Redis Cluster 选的是第二种,它用的协议叫 Gossip(流言)协议 。 4.2 Gossip 协议:一个八卦传播的故事 Gossip 这个词本来的意思就是「八卦、流言」,这个协议的工作方式和八卦传播简直一模一样,小宇宙给你举个生活中的例子。 假设你们公司有 100 号人,某天你知道了一个爆炸性八卦。你会怎么传?你不会一个一个跑去跟全公司每个人讲,太累了。你只会随机找 2、3 个同事聊一下,然后这 2、3 个同事又各自去找 2、3 个同事,一传十十传百。用不了多久,整个公司就都知道了。 Gossip 协议就是这么工作的:每个节点 不需要跟所有节点都通信 ,只要周期性地随机挑几个节点聊聊,把自己知道的集群状态告诉它们。这几个节点收到后又会把信息传给自己挑的几个邻居。经过若干轮之后,整个集群的所有节点都会收敛到一致的状态。 这种方式的好处是 实现简单、没有单点 ,坏处是 信息传播有一定的延迟 ,不像中心化那样「一喊全都知道」。但对于 Redis Cluster 这种对一致性要求没那么极致的场景来说,这点延迟是完全可以接受的。 4.3 Cluster 里的几种消息 在 Redis Cluster 中,节点之间互相发送的 Gossip 消息主要有这么几种,名字起得都挺形象的: ping :节点每秒都会向随机选出的其他节点发 ping 消息,顺便把自己当前知道的集群状态(比如我知道哪些节点是活的、哪些槽归谁)一并塞在消息里带过去; pong :节点收到 ping 之后,回复一个 pong 消息作为确认,pong 里同样会带上自己所知道的集群状态; meet :当你往集群里加一台新节点时,要让集群里某台老节点主动给新节点发一个 meet 消息,相当于「打个招呼,欢迎加入本集群」。新节点收到 meet 之后就会和发消息方建立连接,然后顺着 Gossip 网络把集群里其他的节点也「认识」一遍; fail :当某个节点被判定为彻底挂了,会有节点向整个集群广播一个 fail 消息,告诉大家「这家伙真的不行了,大家把它的状态更新成下线吧」。 你可能会注意到,ping 和 pong 每次都带上「自己所知道的集群状态」,这就是信息不断扩散、不断对齐的过程。每次「闲聊」都在交换情报,慢慢地整个集群的视图就统一了。 4.4 集群总线和 16379 端口 还有一个小细节值得提一嘴:集群里节点之间的通信走的 不是 我们平时连接 Redis 用的那个端口(比如 6379),而是一个专门的 集群总线端口 ,叫做 cluster bus,约定就是「业务端口 + 10000」。所以如果你配的 Redis 端口是 6379,那节点之间的通信端口就是 16379。 这意味着你在部署 Cluster 时,防火墙上 两个端口都要放行 。忘了放 16379 是新手经常踩的坑,会出现节点之间看起来谁也连不上谁的诡异现象,搭集群怎么都搭不起来。 4.5 Gossip 不是没有代价 Gossip 听起来很美,但它也不是没有代价的。最主要的代价是: Gossip 的消息量会随节点数量的增长而急剧增长 ,大概是平方级的关系。 为啥是平方级呢?你想想看,每个节点都要跟其他节点通信,而且每条消息里还要携带一份自己所知道的集群状态。节点越多,每条消息体越大,每秒发出的消息总数也越多,累加起来带宽开销就会很夸张。 这也是为什么 Redis 官方建议 Cluster 主节点的数量不要超过 1000 个 。超过这个规模,光是节点之间互相「打招呼」的流量就足够把网络搞得很紧张,甚至影响正常业务请求。 5. 节点挂了怎么办?聊聊故障转移 到目前为止,我们讲的都是「一切正常」的情况。但实际生产环境里,机器挂掉是家常便饭,磁盘坏、网线松、内核 panic,五花八门。一个号称高可用的集群方案,必须要能在节点挂掉的时候 自动恢复服务 ,这就是我们说的故障转移(failover)。 5.1 先铺垫一下:Cluster 里的主从关系 在讲故障转移之前,我们得先明确一件事: Redis Cluster 里的每个主节点,都可以配一个或多个从节点 。 这些从节点平时在干什么呢?它们其实就是对应主节点的「影子」,通过主从复制不断把主节点的数据同步过来,保证自己这边的数据和主节点尽量保持一致。 但是注意,这些从节点 默认是不对外提供读写服务的 。你如果直接连上一个从节点去 GET 一个 key,它会返回一个 MOVED 让你回去找主节点(后面小节会讲一个叫 READONLY 的命令可以改变这个行为)。所以你可以把从节点理解为「热备」:平时就在那儿待命,主节点出事了立马顶上去。 这就是为什么「3 主 3 从」是 Redis Cluster 最小的推荐部署规模:3 个主节点分摊 16384 个槽负责读写,每个主节点各配一个从节点做热备。任何一个主节点挂了,对应的从节点都能立刻顶上。 5.2 怎么发现一个节点挂了?pfail 和 fail 两阶段 我们前面说过,节点之间会不断地互相发 ping。如果某个节点发出去的 ping 在约定时间(由参数 控制,默认 15 秒)内都没收到对方的回复,它就会心里嘀咕:「这哥们是不是挂了?」 注意这里是「心里嘀咕」,不是「对外宣布」。因为有可能只是我跟他之间的网络闪了一下,别的节点其实还连得上。这种 单个节点单方面的判断 ,在 Cluster 里叫 pfail 状态 (probably failed,可能挂了),也就是我们常说的 主观下线 。 然后这个 pfail 状态会随着 Gossip 消息慢慢传到集群里别的节点。如果集群里 半数以上 持有槽的主节点都认为这个节点处于 pfail 状态(也就是大家都觉得它挂了,不是我一个人闹脾气),那它的状态就会升级为 fail 状态 ,也叫 客观下线 。这时候才会有节点出来广播一条 fail 消息,告诉整个集群:「这家伙真没了,启动故障转移吧。」 为啥要搞主观下线、客观下线两个阶段?就是为了 防止误判 。网络抖动是常有的事,单靠一个节点的判断就把别人拉下马,太草率了。多个节点达成共识之后再下线,稳妥得多。 5.3 选出一个新主:Cluster 的选举机制 挂的如果是从节点,其实没啥大事,顶多就是少了一个备胎,对读写服务没有直接影响。但如果挂的是主节点,那问题就严重了:这个主节点负责的那部分槽,现在没人管了,客户端访问那些槽都会失败。 这时候就得从它的从节点里面 选一个出来当新主 。怎么选呢?Redis Cluster 用的是类似 Raft 的投票机制,流程大致是这样的。 第一步,资格检查。 不是所有从节点都有资格参选。如果一个从节点跟主节点已经失联太久(说明它的数据可能已经很陈旧了),那它就没资格参与这次选举。Redis 会根据 这个参数来判断「失联多久算太久」。 第二步,排队等发起选举。 有资格的从节点不会一股脑全都跳出来喊「选我选我」,而是会按照自己和主节点的 数据同步进度 排个队: 复制得越新的从节点,等待的时间越短 。 这么做很聪明,你想想看:数据最新的那个从节点优先发起选举、也最有可能当选,这样就能把故障切换后 数据丢失的风险降到最低 。 第三步,发起选举,请求投票。 等待时间到了之后,从节点就会向集群里所有持有槽的主节点拉票:「我想竞选槽 X 的新主,请你们投我一票」。 第四步,收集选票。 这里有一个重要的规则: 只有持有槽的主节点才有投票权 ,从节点和不持有槽的节点都没有投票权。这就有点像国家议会里只有议员才能投票一样,要有一定「身份」才行。每个主节点在一轮选举里只能投一票,投给第一个来拉票的合格候选人。 第五步,成为新主。 如果某个候选从节点拿到了 半数以上 主节点的票(比如 5 个主节点里有 3 票),它就当选了。当选之后,它会把自己从「从节点」升级成「主节点」,然后通过 Gossip 广播一条消息告诉整个集群:「这些槽现在归我管了」。其他节点收到之后更新自己的本地信息,客户端下次再访问这些槽时,会通过 MOVED 重定向被纠正到新主节点,故障转移就完成了。 5.4 一个必须要提的「但是」:Cluster 会丢数据吗? 讲到这儿,你可能以为 Cluster 就是万能的。但是小宇宙必须提醒你一件事: Redis Cluster 的主从复制是异步的,所以故障转移过程中是有可能丢数据的 。 异步是什么意思呢?就是主节点处理完一个写命令之后,会 立刻 返回成功给客户端, 然后 才慢悠悠地把这个写命令同步给从节点。这中间有一个极小的时间窗口。 想象一下这个极端情况: 1. 客户端向主节点写入一条数据,主节点写完内存,返回 OK; 2. 主节点还没来得及把这条数据同步给从节点,就突然宕机了; 3. 从节点被选举成新主,但它手里根本没有这条数据; 4. 客户端再来查,发现数据不见了。 这就是典型的故障转移导致的数据丢失。所以在架构选型的时候你心里要有一个清醒的认识: Redis Cluster 在 CAP 理论里是偏向 AP 的 ,它为了可用性和分区容忍性,牺牲了一部分强一致性。 对于绝大多数缓存场景,这点损失是完全可以接受的。但对于一定要强一致的场景(比如涉及钱的扣款、库存扣减),你就得另想办法了,比如配合数据库做最终一致,或者上 ZooKeeper、etcd 这类强一致的协调服务。 6. 初学者最容易踩的两个坑 讲到这里,Redis Cluster 的核心原理其实已经讲完了。但小宇宙知道,原理懂了不代表你动手就能顺利用起来。这一节挑两个 初学者第一次用 Cluster 就会撞到 的坑,提前给你打个预防针。 6.1 多 key 操作报 CROSSSLOT 错误 这是第一次用 Cluster 的人最常见的一个懵逼瞬间。你在单机 Redis 上写得好好的代码: 换到 Cluster 环境下,直接报错: 这是为啥?因为 和 算出来的 CRC16 值完全不一样,很可能落在不同的槽里,而不同的槽又很可能归不同的节点管。一个命令要同时操作两个节点上的数据,Cluster 做不到,只能报错。 这个问题 Redis 官方也知道,所以给了一个解决办法叫 Hash Tag :你可以在 key 里用花括号 括起一段, 只有花括号里的部分会参与 CRC16 计算 。看例子: 这两个 key 在计算槽号时,只会看 这一段,所以它们算出来的槽号 一定是一样的 ,也就一定会落在同一个节点上,MSET 就能正常工作了。 同样的道理也适用于事务(MULTI/EXEC)、Lua 脚本、pipeline 这些涉及多 key 的场景: 如果你希望这些多 key 操作能在 Cluster 下正确工作,记得把相关的 key 加上统一的 Hash Tag ,让它们绑在一个槽里。 6.2 从节点默认不提供读服务 第二个坑是关于读写分离的。 很多小伙伴听说 Cluster 有从节点,自然而然地以为「那我可以把读请求打到从节点上做读写分离啊」,结果发现连上从节点 GET 一个 key,直接被 MOVED 回主节点了,很郁闷。 这是因为 Cluster 里的从节点 默认是不处理读请求的 ,它的唯一任务就是等主节点挂了之后顶上去。如果你确实想让从节点分担一部分读压力,需要在客户端侧先对从节点发一个 命令,告诉它:「我知道从这里读可能读到稍微旧一点的数据,我能接受」。发过 之后,后续的读请求就会在这个从节点上正常处理了。 不过要提醒你一句, 从节点的数据是异步同步过来的,和主节点之间可能有短暂的延迟 ,所以对数据新鲜度要求高的业务,还是老老实实走主节点吧。 7. 加餐:为什么哈希槽的数量是 16384? 前面卖了个关子,这一节我们把这个「灵魂疑问」解决掉。 其实这个问题当年在 GitHub 上就有人去问过 Redis 的作者 antirez,我们来看看他是怎么回答的: antirez 给的理由主要跟 网络传输效率 有关。 我们前面讲 Gossip 的时候说过,节点之间每秒都要通过 ping/pong 交换集群的状态信息。其中一个很重要的内容就是「我这个节点负责哪些槽」。这个信息在节点里面是用一个\ \ 位图(bitmap)\ \ 来存的:每个槽占一个 bit,这个 bit 是 1 表示「这个槽归我管」,是 0 表示「不归我管」。 那如果槽的数量是 65536 ,这个位图需要多少空间呢? 也就是说,每发一次 ping 都要带着一个 8KB 的位图到处跑。 而如果槽的数量是 16384 ,情况就好多了: 一下子就从 8KB 降到了 2KB,省了 6KB。别小看这 6KB,Gossip 是每秒都在跑的,集群里节点越多这个差距就越放大,带宽压力明显会小很多。 那你可能又会问: 既然位图越小越省带宽,为啥不用更小的 8192 呢 ?8192 个槽对应的位图才 1KB,不是更省? 这就涉及到另一个约束了: 槽的数量要大于集群中主节点的数量 ,而且要有足够的 富余度 来保证负载均衡。因为 CRC16 算出来的结果并不是绝对均匀的,槽的数量如果太少,就容易出现有的节点分到的 key 多、有的节点分到的 key 少的情况。 antirez 在回复里也说了,他认为 Redis Cluster 最多支撑到约 1000 个主节点这个规模,16384 个槽平均分给 1000 个节点,每个节点大约有 16 个槽,已经足以保证负载的均衡了;而 8192 就稍微有点紧。 综合考虑「带宽消耗」和「负载均衡」这两方面,antirez 最终选择了 16384 这个数字。它恰好能覆盖常见的集群规模,同时位图只有 2KB,带宽开销也能接受,算是一个工程上的折中。 顺便提一个小细节,Redis 源码里实际计算槽号用的是 位运算 : 这是因为 等价于 ,而 16384 正好是 2 的 14 次方,16383 就是 ,用 比用 快一些。这也是为什么槽的数量要选一个 2 的幂次方。 8. 总结一下 这篇文章讲了挺多内容,小宇宙最后用几句话帮你把主线串起来。 Redis Cluster 要解决的核心问题 ,是单机 Redis 的内存有上限,必须把数据分散到多台机器上。这是它存在的根本理由。 数据怎么分? 通过在 key 和机器之间加一层「哈希槽」: 。好处是扩缩容时只搬槽不搬 key,迁移成本可控。 客户端怎么找到数据? Smart Client 在本地缓存一份「槽 → 节点」的地图,先本地算槽号再直接发请求,一次命中。如果地图过期,服务端通过 MOVED 通知客户端「永久搬家」;如果槽正在迁移中,则通过 ASK 做「临时借道」。 节点之间怎么同步状态? 用 Gossip 协议,每个节点周期性地跟其他节点「八卦」自己知道的集群状态,最终整个集群的信息会收敛到一致。代价是消息量随节点数平方级增长,所以规模不建议超过 1000 主节点。 节点挂了怎么办? 通过 pfail、fail 两阶段判断是否真的下线,然后从故障主节点的从节点里,按数据新鲜度优先级发起选举,半数以上主节点投票赞成即可当选新主。注意主从是异步复制的,故障转移有丢数据的风险,所以 Cluster 整体偏 AP 而不是 CP。 动手前的两个坑 要记住:多 key 操作要用 Hash Tag 把 key 绑到同一个槽,从节点读要先发 。 最后小宇宙留一个问题给你思考: 如果让你从零设计一个分布式 KV 存储,你会借鉴 Redis Cluster 的哪些点,又会改掉哪些点呢 ?欢迎在评论区聊聊你的想法。 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。