一次线上脑裂故障让我彻底搞懂了Redis集群原理
点击关注公众号,“技术干货”及时达!?? 背景这又是一个线上 Redis 大规模故障引发的案例,在上一次 Redis 缩容故障引发业务大面积不可用(见这篇文章:血的教训!Redis缩容导致线上大规模故障的惨痛经历 – 烈香的博客 (deadlock.cloud))之后,在我的操作之下居然又发生了一次故障,这次还是「缩容」引起的!
哈哈??缩容可能是我绕不过去的一个坎了,但再怎么说我也不会栽在同一个坑里两次,所以这次故障的原因和之前的不同,相信你一定猜不到??
?? 故障现场本次故障是进行 Redis 集群缩容过程中,迁移 slot 数据时,业务突然有大量 Redis 访问异常,报错信息如下,还是和上一次相似的 MOVED 异常:
当我看到这个报错时,我并没有慌,因为我知道这是由于集群内部节点之间对于 slot 的归属不一致导致的。
?「小提示?」:redis 集群内部每个节点都维护了每个 slot 的 owner ,当客户端请求到任意一个 Redis 集群节点时,Redis 根据 key 算出 slot,再判断 slot 是否属于自己,如果不属于就会发送 MOVED slot ip 异常返回给客户端,这样客户端就知道了 slot 的真正归属是谁,会再发送请求给正确的 Redis 节点。
?当时报错信息具体是:MOVED 10.20.22.92 7940。我到 10.20.22.92 这台机器上使用 cluster nodes 命令查看集群信息,发现 slot 确实不在这台机器上:
root@sh2-arch-redis-product-prod-29~$redis-cliclusternodes|grepself
7285477753679199e9238fbe94a00f1569661aea10.20.22.92:6379@16379myself,master-0172114040700035464connected7866-7929...
可奇怪的是去其他 Redis 节点上执行使用 cluster nodes 命令查看发现 7940 这个 slot 确实在 10.20.22.92 上!
root@sh1-arch-redis-product-1~$redis-cliclusternodes|grep10.20.22.92
7285477753679199e9238fbe94a00f1569661aea10.20.2292:6379@16379master-0172114040200035464connected7866-7942...
为了确认 7940 这个 slot 到底发生了什么我专门查看了 Redis 日志,发现 7940 这个 slot 确实在 22:23 分已经迁移到了 10.20.22.92 上面,而业务异常大量报错的时间点在 22:24 分!
...
1803:M16Jul202422:23:17.617#configEpochupdatedafterimportingslot7940
1803:M16Jul202422:23:21.088#NewconfigEpochsetto35279
...
小结让我们梳理一下这次故障诡异的时间线:
「22:23:17」:根据日志发现 slot 7940 在 迁移到 10.20.22.92。
「22:24:03」:客户端开始报错:MOVED 10.20.22.92 7940。
「22:29:12」:开始排查,发现 10.20.22.92 不认为自己有 7940 这个 slot,反而是其他节点认为 7940 是 10.20.22.92 所有。
也就是说「整个集群在 7940 这个 slot 的归属问题上脑裂了!」 为什么已经迁移的 slot 在目标节点上会莫名消失?难道 Redis 有 bug ?(你别说还真是??,往下看就知道)
?? 深入了解 Redis 集群内部通信原理要知道这个问题的原因,就不得不深入了解Redis集群内部到底是如何让slot分布达到一致的。下面就让我们一起深入Redis 内部探究吧。
Redis 集群如何传播 slot 分布?Redis 集群内部每个节点通过 PING-PONG 的 Gossip 协议消息交互,消息内容主要包括两部分:
自己负责的 slot 和自己的 config epoch自己知道的一部分 Redis 节点的 ip,port 等发送自己负责的 slot 就是为了保证 slot 分布信息在集群内部一致,另外还有一个 config epoch ,它可以理解为一个版本号,用于解决不同节点之间 slot 冲突,这里先简单介绍下,后面还有详细讲述。
具体是发送给谁怎么发送的呢?答案是如下:
每秒选择1个最久没有 ping 过的节点发送 PING 消息同时也会确保在 cluster-node-timeout / 2 = 7500ms 时间范围内,发送给还没有发送过 PING 消息的节点。如果集群规模较大有上百个节点,第二步会完成大部分节点的发送,ping/pong 消息头中携带本节点负责的 slot 和 config epoch,因此可以用这个信息完成 slot 分布的传播。
??? 知识点1:集群规模比较大,slot变化的传播是需要时间的。
?节点内部是如何处理 Gossip 协议消息的?首先我们要知道 Redis 节点内部维护了几个重要的信息:
集群内其他节点的状态,包括节点的 configEpoch每个 slot 的 owner ,是一个长度为16384的数组currentEpoch 和 configEpoch:currentEpoch 是这个集群所有节点中最大的 configEpoch(当然这个值可能不是最新的),每个节点都有一个 configEpoch(且大多数情况下每个节点的 configEpoch 是不同的,但也有出现相等的情况,具体冲突解决逻辑见下方)configEpoch 代表 slot 分布的版本号,如果 slot 分布不一致,要靠 configEpoch 解决不一致。dict*nodes;
/*Hashtableofname-clusterNodestructures*/
clusterNode*slots[CLUSTER_SLOTS];
uint64_tcurrentEpoch;
uint64_tconfigEpoch;
?那么回到主题:节点内部是如何处理gossip消息的呢?
?假设有两个节点 A 和 B , A 发送给B一个 Gossip 消息,B 的处理逻辑为:
「节点B 更新自己维护的 节点A 信息」: 如果消息头里的 configEpoch 更大的话,会更新自己维护的 节点A 的 configEpoch。如果节点A的 configEpoch 和 节点B 的相等且自己的 NodeID 更小,「节点B的 currentEpoch 增加一」,并且作为自己的 configEpoch ,(意味着节点B使用了一个新的 configEpoch ,并且是集群中最大的——它自己认为的,因为 currentEpoch 有可能不是最新的)如果A节点声明的slot和自己维护的A拥有的slot有区别的话,根据ping消息声明的每个slot的config epoch的不同有如下操作:
a. 如果消息头的 config epoch 比这个 slot 的 owner 的 config epoch 更大,那么「更新这个 slot 的 owner」。
b. 如果消息头的 config epoch 比这个 slot 的 owner 的 config epoch 更小,说明发送方的 slot 分布是过时的,有些 slot 已经不属于发送方了,所以「返回一个 UPDATE 消息」,这个 UPDATE 消息里包含了这个slot owner 的 slot 分布和 config epoch。如果节点A 收到了 UPDATE 包(注意这其实也是一个 Gossip 消息),节点A 会按照这个逻辑更新自己的 slot 分布。?让我来精简下这段逻辑:其实就是为了「解决两个节点分别声明一个slot的归属权」,而如何解决这种争议呢?看的是这两个节点「config epoch的大小」,谁比较大谁就“胜诉”。而且还要注意,如果一个节点“败诉”,它还会接收到一份“通知”,要求其更新slot的归属为胜诉方。
?注意这里其实是一个覆盖操作,当我分析到这里的时候,我就知道离真相不远了。
??? 知识点二:当争夺归属权失败(由于自己的config epoch比较低),那么自己的slot会被覆盖
?如果我们再靠近点分析,又可以发现一个盲点: 「其实当一个节点的 slot 迁出去(减少)的时候,它发送的 Gossip 消息接收方是不会老实更新的」,这其实和接收方的处理逻辑有关系:
举个栗子:
节点B 上看 节点A 是如下 slot 的拥有者:slot:[1,2,3]
之后 节点A 的 slot 迁出,变成了: [1,2]
再之后 节点A 发送 ping 到 节点B,「节点B 不会更新 节点A 的 slot」,节点B 上 节点A 的 slot 依然是:[1,2,3], 并不是 [1,2] , 只有等到 slot=3 的新 owner 发送 ping 消息且它的 epoch 更大,才会在节点B 更新 slot=3 的 owner。
??? 知识点三:一个节点slot迁出后,其他节点看这个节点的slot只有等到“正主”发送gossip协议消息才会更新。
?那么在迁移 slot 的时候到底发生了什么?config epoch 是如何变化的?
迁移slot时到底发生了什么?在迁移 slot 时,首先要从源节点上迁移数据,数据全部迁移好后,需要在对应节点上调用 cluster setslot slot nodeId 命令,彻底变更 slot 的归属,setslot 命令的具体逻辑如下:
设置自己维护的 slots 的 owner 为对应 nodeId 的节点如果自己正在 imporing 这个 slot,而且自己的 configEpoch 不是最大的,那么会增加 currentEpoch ,并且作为自己的 configEpoch。也会打印这样的日志:
??? 知识点4:当一个节点slot迁入,其config epoch一般是全集群最高的
??? 黑幕揭晓?「综合以下条件,你会发现什么?」
slot 的变化传播需要时间。当争夺归属权失败(由于自己的 config epoch 比较低),那么自己的 slot 会被覆盖。一个节点 slot 迁出后,其他节点看这个节点的 slot 只有等到“正主”发送 gossip 协议消息才会更新。当一个节点 slot 迁入,其 config epoch 一般是全集群最高的。?让我来梳理下:
自己刚刚迁入的slot被覆盖,根据条件2可能是由于自己的config epoch不是最高的导致被覆盖了,那最高的是哪个节点?
「很有可能是正在迁入的节点!」
去查日志看看当时正在迁入的节点有哪些,也许能发现什么线索!
结果一看原来是这样~,还记得 7940 这个 slot 和 10.20.22.92 这个目标节点吗,当时是 10.20.22.47 迁出7940 到 10.20.22.92 上,结果在 10.20.22.47 这个源节点上看到日志:
欸?10.20.22.47 居然在迁入 slot。。。
好了一切线索终于拼接起来,下面让我来还原案发现场...
还原那个现场...?「假设」:(节点A = 10.20.22.47, 节点B = 10.20.22.92)
「场景」:节点A原有 slot=[1,2,3] epoch=2, 节点B epoch=1,slot 从节点A 迁移 slot=3 到节点B
?步骤:
在节点A 上执行 cluster setslot 3 node_B 将 slot3 分配给节点B (也包括其他主节点)。在节点B 上执行 cluster setslot 3 node_B ,节点B epoch 增加为 2 + 1 = 3。节点A 接收到 B 的 ping 消息,currentEpoch 更新为3, 之后节点 A 由于同时发生迁入,epoch 增加了(由于 setslot 命令),epoch= 3 + 1 = 4。节点A 发送 ping 传播自己的 slot=[1,2] epoch=4 给 节点C(任意一个从节点),节点C 上 节点A 的信息:slot[1,2,3], epoch=2 更新为 slot[1,2,3], epoch=4 (节点C是从节点,因为主节点全部都执行了 setslot,从节点是没办法执行 setslot 的)。节点B 发送 ping 到节点C,消息内容:slot=[3], epoch=3 ,由于 epoch=3 epoch=4, 因此 节点C 返回 UPDATE 包,返回 节点A 的 slot 分布,节点B 重置 slot=[3] 的 owner 为节点A。?我画了一张图,方便你理解
?原因总结「一个节点在迁出 slot 的同时,又在迁入 slot」,导致这个节点缩减的 slot 信息在传播到所有节点之前,其 epoch 由于迁入导致比目标节点还高,最终导致目标节点的 slot 分布被覆盖,进而引发惨案...
为什么一个节点在迁出 slot 的同时,又在迁入 slot 呢?
再次查看日志,原来是我在生成迁移计划的时候,误将这个节点同时作为源节点和目标节点,所以还是操作失误导致的??
如何修复社区中遇到类似问题的解决办法是在setslot执行后,等待拓扑传播完毕,再继续迁移其他节点当源节点不再声明某些 slot 时,需要标记这些 slot 为不确定归属的 slot ,在目标节点发送 ping 并且声明这些 slot 时,应该能正常更新 slot 的归属,而不是由于 epoch 较小被发送 UPDATE 消息。社区相关PR(已merge 到 Redis 社区主干,Redis7.0 版本后)https://github.com/redis/redis/pull/12344/files
操作层面禁止一个节点同时迁入迁出slot
?? 写在最后这次故障分析过程还是比较酣畅淋漓的,大家看完一定有一些收获,代价只不过是我的绩效没了??。
这次故障其实比较尴尬??,因为其根本原因是由于 Redis 的 bug —— 「同时迁入和迁出 slot 时会触发」,但触发操作是我导致的,操作失误让一个节点同时迁入迁出 slot 了,「所以大家还是注意线上操作要谨慎」。
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线