Redis Cluster:为高性能付出了不安全的代价 - emil


本文旨在解释为什么 Redis 不适合用作 NoSQL 数据库,其中持久化数据的持久性和一致性是必不可少的。
很难想到比 Redis 更广为人知的数据存储。在 Stack Overflow 上,它连续三年被评为最受欢迎的数据库。它也是AWS 上最受欢迎的数据库,胜过 MySQL 和 Postgres 之类的数据库,以及亚马逊专有的 NoSQL 产品。
澄清一下,这不是对 Redis 的攻击。Redis 是出色的缓存或辅助数据存储,具有便捷的 NoSQL 功能、分片、复制和尾随持久性。它在持久性方面放弃了的是用吞吐量和延迟弥补了。在正确的上下文中,Redis 是一个极好的技术选择。
本次讨论的背景是它用作高价值业务记录的主要数据存储,这些记录受到严格的持久性要求,并且如果丢失或损坏则无法轻易恢复。

(省去分布式系统理论...点击标题见原文)
 
Redis 集群属于由领导者 - 追随者架构支持的复制状态机类别,在具有基于多数的仲裁系统的配置中包含多个进程。这是分布式系统文献领域中众所周知且经过实战检验的拓扑。
根据我们声明的持久性和一致性要求,系统需要满足 Brewer CAP定理 的 CP(一致和分区容忍)标准— 换句话说,面对网络分区,它需要优先考虑数据一致性而不是可用性。我强烈怀疑大多数采用 Redis Cluster 并对持久性和一致性有严重期望的人自然会认为 Redis Cluster 是一个 CP 系统,而不是一个 AP 系统。也就是说,Redis 被认为是安全的,或者至少是足够安全的。
 
安全定义
关于“足够安全”的含义没有普遍的共识,也不可能有统一的共识,因为安全要求是由每个组织独特的商业驱动因素决定的。在这里,我将尝试制定一个较弱的安全定义,这对于相当多的应用程序可能是明智的。我从不同行业的众多客户的个人经验中汲取了经验,我的建议是大多数从业者都会做出的合理假设。有人可能会说,这种对安全性的削弱不够客观;我说,如果没有某种基线,我们就无法继续。
一个“足够安全”的基于leader-follower的复制状态机通常需要满足以下标准:

  1. 领导者的单一性。最多有一个领导者可以在任何给定时间运行,并被配置中的仲裁副本(包括领导者)统一接受。前任被取代的领导者不能干涉当前领导者的行动。严格来说,安全属性允许在琐碎的情况下为零领导者;however, at some time there must be one elected leader for progress to eventually occur, thereby satisfying the liveness property.
  2. 复制功效。所有副本(包括领导者)都按照领导者规定的顺序忠实地将领导者发出的更新应用于其状态机。领导者执行的所有更新都会传递给法定数量的副本(包括领导者)。未由完整仲裁副本接受的写入不得由主服务器安装或向启动应用程序(客户端)确认。此属性与 CAP 定理的 CP 约束有关。
  3. 法定人数交集。根据我们之前的通信,仲裁系统中的每个有效仲裁至少通过一个进程与其他仲裁相交。
  4. 一致的副本提升。在故障转移期间,只有一致的副本可以争夺领导权。其他人必须保持休眠状态。一旦该副本被选中,它就会从领导者离开的地方继续。注意,leader 上可能有未确认的写入待处理;安全属性不关心这些写入;继任领导人可以自行决定合并他们。
  5. 持久性有限的陈旧性。每个副本都附加到一个持久性存储设备上,该设备可以在进程失败后幸存下来。持久状态反映了故障前进程的瞬态状态,关于在某些商定的过时范围内已被副本确认的写入. 可以根据经过的时间或未提交记录的数量来衡量有界过时。(后者更可取,因为它更准确地反映了最坏情况损失的大小。)在运行时,该进程不会丢弃其临时存储中的数据,直到持久存储确认相关写入。重新启动过程可以确定最近提交的写入的身份(偏移量、序列号等)。作为推论,重新启动的过程绝不能自动承担领导权,即使它在失败之前碰巧成为领导者。

除了上述标准之外,我还提出了几个假设来补充弱安全属性:
  1. 仲裁系统旨在容忍f 次故障,其中f ≥ 1。换句话说,我们正在处理一个高可用性系统,其中单个进程的故障不会导致系统无法运行。实际上,对于基于多数的仲裁系统,我们至少需要三个进程。
  2. 进程不受可能的共模故障的影响。共模故障涉及影响多个独立元素的单一原因;例如,电源故障或物理入侵事件。将服务器分散在不同的机架或更好的数据中心之间,可以降低共模故障的可能性。
  3. 过程配备了*P故障检测器。所有失败的进程最终都会被所有活着的进程怀疑;最终不会怀疑所有实时进程。准确度-完整性二元性的精确调整虽然对活跃度具有实际意义,但不会影响安全性。
  4. 弱持久的持久性。保留的数据以商定的可能性被原始地召回,该可能性随着时间而减少。可以以足够高的可能性检测到数据丢失和损坏。

上面概述的弱安全特性与其传统(强)变体之间的差异集中在标准 5 和假设 4 中。它们放宽了通常的持久性标准,这些标准几乎肯定会在更理论的环境中假设。通常,标准 5 类似于以下内容:
持久状态准确地反映了故障前进程的瞬态状态,相对于已被副本确认的写入。
同样,假设 4 应为:
保留的数据在未来的每个点都会被原始地调用。
我们在这里做出两个重要的让步。首先,我们说确认的写入不一定要提交到稳定存储,只要这些写入在某个“合理”的时间内被刷新,并且我们可以在重新启动时准确地检测到最后一次提交是什么。在实践中,这可能不是一个硬界限。
 
分析Redis
首先,来自官方文件的几个条款作为序言。有些甚至是非常有趣的。至少,Redis 的所有用户都应该非常熟悉他们,无论他们对 Redis 或本文的看法如何。在整个分析过程中,我们将通过它们的编号来引用它们。
  1. 通过开箱即用的默认设置,Redis 可作为半持久数据存储运行,牺牲持久性以支持写入的吞吐量和延迟。这在官方文档的Redis 持久化章节中有详细说明。具体来说,AOF(仅附加文件)的默认刷新间隔会留下一秒的窗口,在该窗口内确认的写入可能会不可挽回地丢失。这是appendfsync everysec选项,它是 AOF 配置中的默认选项和推荐选项。
  2. 持久化规范列出了appendfsync always选项,其中所有暂存的 AOF 写入都受制于fsync()返回之前;然而,在这种模式下,Redis的性能在官方文档中被描述为“非常[原文如此]非常慢,非常安全”。因此,appendfsync everysec推荐该选项,“非常快且非常安全”。读者会发现 Redis 文档似乎回避了实证或定量的断言,而倾向于使用诸如“非常”、“慢”、“快”等定性形容词。
  3. 关于复制章节表明 Redis 在领导者-跟随者(用它的说法是主从)拓扑中采用异步复制协议,其中从服务器重放主服务器应用的写入,而与主服务器对发起客户端的响应无关。在主站响应时,从站可能会任意落后于主站。
  4. 从 3.0.0 版本开始,Redis 包含WAIT命令。WAIT在客户端上阻塞,直到所有先前的写入在指定的超时内复制到用户特定数量的副本,并在超时过去之前返回已确认的副本数量。表面上,WAIT把Redis的异步复制机制变成了同步复制机制。
  5. WAIT文档指出“WAIT不会使Redis的强一致店:同时同步复制是一个复制的状态机的一部分,它不是唯一需要的东西”。它继续澄清“Sentinel 和 Redis Cluster 都将尽最大努力在可用副本集中提升最佳副本。然而 [原文如此] 这只是尽力而为的尝试 [原文如此] 因此仍有可能丢失同步复制到多个副本的写入”。在这里,文档没有明确说明“尽力而为”的含义。具体而言,维护者不会公开最佳副本提升失败的条件。我们有责任解决这个问题。
  6. 该WAIT文档并未声称在主节点和/或副本刷新暂存写入之前命令会阻塞,仅声明“命令已成功传输并确认”由指定的最小副本数。换句话说,该WAIT命令是对各个副本的瞬时状态而非持久状态的断言。
  7. 复制章规定,“Redis的是不是一个CP系统,具有很强的一致性”和“承认写仍然可以在故障转移期间丢失”。它继续说道,“WAIT在故障事件后丢失写入的可能性大大降低到某些难以触发的故障模式”,但没有详细说明导致数据丢失的特定故障模式,或量化这些事件发生的可能性。表面上,这指的是第 5 条,这是不完整的。它也可能指的是第 6 条。Redis 将细节保留为众所周知的“读者练习”。
  8. Redis官方集群规范描述了“从属等级”算法,其中从属根据复制数据量建立相对于其对等方的等级,并将领导者选举延迟一个与其等级大致成比例的时间。(我们说“大致”是因为等待时间包含一个随机延迟组件。)更完整(排名接近 0)的从属被设置为在不完整之前投票(排名接近 N-1,其中 N 是配置的副本数)奴隶。
  9. 在同一部分,集群规范指出“masters 不会以任何方式选择最好的 slaves”。没有机制可以确保后续的领导者在幸存的副本中排名最好。

基于对 Redis Cluster 文档和“光鲜亮丽的小册子”声明的随意阅读,更不用说它在行业中的多产采用水平,大多数人会天真地认为 Redis Cluster 的工作方式大致如下。
当足够的进程和网络链接是可选的时,写入从主(进程A)镜像到法定数量的副本。(建立在我们之前示例中的三进程配置的基础上。)所有副本都参与八卦协议——定期交换心跳——其中每个副本都知道其他人的存在和复制高水位线。(高水位标记是最近复制写入的标识。)如果客户端调用 ,则写入不会被客户端确认,直到它们暂时反映在大多数进程上WAIT。(没有WAIT,客户端会立即返回。)此外,Redis 进程为每个连续的视图分配一个单调递增的纪元编号,以便副本同意它们属于哪个视图。
在某些时候,主节点要么发生故障,要么与其余节点隔离。这两种情况实际上是等价的。结果是临时停机,因为客户端要么无法将其请求路由到主服务器,要么主服务器无法将写入传递到仲裁中,因此它永远不会响应。
副本最终会注意到主节点没有心跳,并通过其本地*P检测器推测其故障。两个副本都启动一个倒数计时器,该计时器大致与其相对等级成比例。(添加了随机性元素。)
最新副本的计时器最快到期,并且进程B确保C的投票成为新的主节点。Redis 使用一条名为FAILOVER_AUTH_REQUEST从对等副本获得投票的消息。那些支持将请求者指定为拟议纪元的新领导者的人以FAILOVER_AUTH_ACK. 既乙和Ç递增其元计数器,从而排除甲来自干扰。
一旦B在接下来的 epoch纪元 中被命名为新的主节点,客户端应用程序最终会发现这个事实并将它们的请求路由到B。可能有写入在A 中排队——也许A还活着;无论如何,这无关紧要——这些写入尚未得到确认,它们的损失对客户来说应该无关紧要。到那时,任何进行中的请求都将路由到新的主节点。
请记住,B和C无法判断A是否发生故障、是否被分区或响应缓慢。如果A还活着,它也会启动自己的倒计时;然而,它将无法获得足够的选票来形成法定人数。A可以稍后重新加入,但它必须先经过一个发现过程;如果A试图承担主控权,则纪元计数器会在合并时适当地阻止它。现在应该很明显,view-epoch二重唱是至关重要的。没有它,主人将获得无条件的许可,可以与他们的前slaver从节点发生关系。
理论如何与现实叠加?
Redis 集群在其默认配置中是不安全的。
 
....

建议
提供的唯一明智的指导是针对我们在此处考虑的各种工作负载强烈避免使用 Redis Cluster。如果系统未能证明我们弱化的安全概念,我们可能会通过降低系统暴露和易受攻击的事件发生的可能性来使其更安全一些。因此,如果无情的约束会阻止您从 Redis 切换到更合适的持久性堆栈,那么以下内容可能会有所启发。
我们对脏读无能为力;这是你需要忍受的东西。WAIT在那里使用对您没有帮助。
.. 
结论
Redis Cluster 在任何配置下都是不安全的,即使考虑到已经做出的所有让步。在其默认配置中,Redis 的性能非常出色,但客观上并不安全。当我们确定配置时,Redis 显示出微小的改进,但即使是我们愿意考虑的最宽松的安全属性也无法满足,而性能权衡变得明显。就目前而言,可以这么说,“在任何速度下”都是不安全的。
Redis 是针对特定工作的有效工具,并在其他关键领域做出了某些妥协。知道这些区域是什么!作为磁盘支持的缓存,Redis 已经是无与伦比的。它的一些数据结构是伟大的。但是,如果您追求可扩展的 NoSQL 数据库,该数据库提供一致性、持久性和某种程度的隔离(即,一个体面的安全概念),那么您会在市场上找到客观上更适合您需求的替代品。