昨天,分布式系统专家 Aphyr 发布了一条关于一家未知公司(希望保持匿名)遇到的 Redis Sentinel 问题的推文:“哦,Redis Sentinel 上的“他们杀死了 -9 d 的 master,这导致了脑裂...”
昨天,分布式系统专家 Aphyr 发布了一条关于一家未知公司(希望保持匿名)遇到的 Redis Sentinel 问题的推文:
“哦,Redis Sentinel 上的“他们杀死了 -9 的主人,导致了脑裂……”
“然后旧主节点弹出,没有数据,并将缺乏数据的情况复制到所有其他节点。实际上必须从备份中恢复。”
天啊,我想我们有一些讨厌的错误。然而,我试图从凯尔那里获取更多信息,他回答说用户实际上完全从主进程中禁用了磁盘持久性。是的:主服务器被故意配置为以擦除的数据集重新启动。
你猜怎么着?推特上的一场闹剧立刻开始了。人们对 Redis 用户深感担忧。可怜的 Redis 用户!总是处于危险之中。
然而,虽然非常担心是一种明智的品质,但我想采取另一条路:提供更多信息。此外,这篇博文写起来很有趣,因为实际上 Kyle 在几乎没有上下文的情况下报告了这个问题,后来的几条推文能够,恕我直言,隔离出我认为可以在 Redis Sentinel 中改进的真正方面,这与 Redis Sentinel 不相关所描述的事件,并且已经在我的待办事项列表中很长时间了。
但在此之前,让我们更仔细地检查一下 Redis / Sentinel 在戏剧性事件中的行为。
欢迎来到崩溃恢复系统模型
===
大多数现实世界的分布式系统必须设计为能够适应进程可以随机重新启动的事实。请注意,这与分区问题非常不同,分区问题是无法与其他进程交换消息。相反,这是一个失去状态的问题。
为了更准确地描述这个问题,我们可以说,如果设计了一个分布式算法,使得进程必须保证在重新启动后保留状态,并且未能做到这一点,那么从技术上讲,它正在经历 bizantine 故障:状态已损坏,进程不再可靠。
现在,在由 Redis 实例和 Redis Sentinel 实例组成的分布式系统中,重新启动的实例能够使用旧数据集重新启动是至关重要的。从擦除的数据集开始是拜占庭故障,Redis Sentinel 无法从该问题中恢复。
但是让我们退后一步。实际上Redis Sentinel可能并没有直接参与这样的事件。典型的例子是,如果配置错误的主服务器重新启动得足够快,以至于哨兵根本没有检测到任何故障,就会发生什么情况。
1.节点 A 是主节点。
2.节点 A 重新启动,并禁用持久性。
3.哨兵(可能)发现节点 A 不可访问……但不足以达到配置的超时。
4.节点 A 再次可用,但它以完全空的数据集重新启动。
5.所有从节点 B、C、D……都会很乐意从其中同步一个空数据集。
毕竟,根据配置,所有内容都从主设备上擦除了。所有从奴隶身上删除的东西,都是从被认为是数据集当前真相来源复制的。
让我们从方程中删除 Sentinel,即上述时间线的点“3”,因为 Sentinel 在示例场景中根本没有起作用。
这就是你得到的。您有一个 Redis 主服务器与 N 个从服务器进行复制。主设备重新启动,配置为以新的(空)数据集启动。 Sals 再次从中复制(空数据集)。
我认为这对于 Redis 用户来说并不是什么大新闻,这就是 Redis 复制的工作原理:从属服务器将始终尝试成为其主服务器的精确副本。不过,让我们考虑一下替代模型。
例如,Redis 实例可以有一个保存在 RDB / AOF 文件中的节点 ID。每次节点重新启动时,都会加载其节点 ID。如果节点 ID 错误,从站根本不会从主站进行复制。安全得多吧?实际上,只是轻微的。主服务器可能有不同的错误配置,因此在重新启动后,它可能会重新加载几周前的数据集,因为快照由于某种原因失败。
因此,在一次错误的重启之后,我们仍然拥有正确的节点 ID,但数据集太旧了,基本上与被擦除或多或少相同,只是更微妙地进行检测。
然而,以稍微提高安全性为代价,我们现在拥有一个操作起来可能更加复杂的系统,并且由于操作错误,由于 ID 不匹配,从设备面临无法从主设备复制的危险与禁用持久性类似,只是比这不那么明显。
所以,让我们换个话题,看看 Sentinel *实际上*涉及的故障模式,并且可以改进。
并非所有副本都是一样的
===
从技术上讲,Redis Sentinel 提供了一组非常有限的、易于理解的保证。
1) 所有哨兵一旦能够通信就会就配置达成一致。实际上每个子分区总是会一致。
2) 如果没有大多数 Sentinel 进程的授权,Sentinel 无法启动故障转移。
3) 故障转移是严格排序的:如果故障转移发生得较晚,则它具有更大的配置“编号”(Sentinel 俚语中的配置纪元),这将始终胜过旧配置。
4) 最终,Redis 实例被配置为与获胜的逻辑配置(具有更大配置纪元的配置)进行映射。
这意味着数据集语义是“最后一次故障转移获胜”。然而,这里缺少的信息是,在故障转移期间,选择哪个从站来代替主站?总而言之,这是一项基本属性。例如,如果 Redis Sentinel 由于选择了已擦除的从属设备(刚刚以错误的配置重新启动)而失败,则*这*是 Sentinel 的问题。 Sentinel 应该确保,即使在 Redis 是异步复制系统这一事实的限制范围内,它也会尝试通过选择最好的从属设备来实现用户的最大利益,并且在没有可行的情况下根本拒绝故障转移奴隶可达。
这是一个可以改进的地方,这就是今天在主服务器失败时选择从服务器时发生的情况:
1)如果一个slave重启了,重启后从未与master连接,执行成功同步(数据传输),则跳过。
2) 如果从设备与其主设备断开连接的时间超过配置超时的 10 倍(对于一组哨兵检测主设备故障而无法访问主设备的时间),则认为该从设备不合格。
3)从剩余的从站中,Sentinel 选择具有最佳“复制偏移量”的一个。
复制偏移量是 Redis 主从复制用来计算通过复制通道发送的字节数的数字。它在很多方面都有用,而不仅仅是用于故障转移。例如,在网络分割后的部分重新同步中,从设备将询问主设备,给我从偏移量 X 开始的数据,这是我收到的最后一个字节,等等。
但是,当在选择要升级的最佳从属设备时使用此复制数时,存在两个问题。
1) 重启后重置。乍一看这听起来无害,因为我们想选择编号较大的从属设备,无论如何,重新启动后如果从属设备无法连接,它就会被跳过。但它并非完全无害,请阅读更多内容。
2) 它只是一个数字:它并不意味着 Redis 从机是从*给定的*主机复制的。
还要注意,当从机升级为主机时,它会继承主机的复制偏移量。于是取模重新开始,数字不断增加。
为什么“1”和/或“2”是次优选择并且可以改进?
想象一下这个设置。我们有节点 A B C D E。
D 是当前的主分区,与 E 一起划分为少数分区。 E 仍然从 D 复制,从他们的 POV 来看一切都很好。
但是在多数分区中,A B C 可以交换消息,A 被选为主。
稍后 A 重新启动,重置其偏移量。 B 和 C 从中复制,并以较低的偏移量重新开始。
一段时间后,A 失败,同时 E 重新加入多数分区。
E 的数据集与 B 和 C 数据集相比更新较少,但其复制偏移量较高。不仅如此,实际上E可以声称它最近连接到了它的主人。
改进这一点很容易。每个 Redis 实例都有一个“runid”,这是一个随着每次新的 Redis 运行而变化的唯一 ID。这在部分重新同步中非常有用,以避免从错误的主设备获取增量流。从站应该发布他们成功复制的最后一个主站运行ID,并且Sentinel故障转移应该确保只选择从他们实际故障转移的主站复制的从站。
一旦你将复制偏移量限制在给定的 runid 上,你得到的就是从属更新量的*绝对*计量。如果有两个从站可用,并且两者都可以声明与旧主站的连续性,则复制偏移量较高的一个肯定是最佳选择。
然而,在数据不是很重要但可用性很重要的所有情况下,这也会产生可用性问题。例如,如果当 A 崩溃时,只有 E 可用,即使它曾经从 D 进行复制,也比没有好。我想说,当你需要一个高可用的缓存并且一致性不是一个大问题时,使用 Redis 集群 ala-memcached(N 个主节点之间的客户端一致性哈希)是正确的选择。
请注意,即使不检查 runid,为了使复制偏移在重新启动后持久,也已经大大改善了行为。在上面的示例中,仅当与从属设备隔离在少数分区中时,E 才会比多数一侧的其他从属设备接收到更多写入操作。
TLDR:我们必须解决这个问题。它与在没有数据集的情况下重新启动主机无关,但对于拥有更正确的实现很有用。然而,这只会限制一类非常难以触发的问题。
这在我的 TODO 列表中已经有一段时间了,祝贺 Aphyr 在交换的几条推文中发现了真正的实施问题。关于来自未知公司的 Aphyr 报告的故障,我认为目前尝试防止严重的错误配置是不可行的,但是这是一个明显的迹象,表明我们需要更好的 Sentinel 文档,与我们现有的文档相比,这些文档更具增量性现在,尝试描述系统如何工作。更明智的方法可能是从通用的理智配置开始,并列出“不要做”列表,例如,不要关闭持久性,除非您同意擦除实例。
评论