Redis cluster, no longer vaporware.

Salvatore Sanfilippo

Redis 集群,不再是空中楼阁

原文由 Salvatore Sanfilippo 发布,订阅该博客

在我的 git 历史记录里,能找到的关于 Redis Cluster 的最早一次提交是在 2011 年 3 月 29 日,但那其实是一次“复制后提交”的合并:cluster 分支原本的历史已经被丢弃,因为里面全是为初步构思 API 以及与系统其余部分交互方式而产生的、乱成一团的半成品提交。

说到底,这已经是一个将近四年的项目。这差不多占了 Redis 整个项目历史的三分之二。然而直到今天,我才发布了 Redis 3.0.0 的第一个候选版本,也是首个支持 Cluster 的版本。

曲折的历程

要理解为什么拖了这么久,其实很简单:我当初非常仓促地启动了 Cluster 项目,那时看起来如果没有自动化的扩展方式,Redis 很快就会变得毫无用处。但那根本不是启动 Cluster 项目的合适时机,原因很简单——Redis 本身还太不成熟,我们甚至还没能把“单实例”这件事做扎实、讲清楚。

虽然我在错误的时机启动项目犯了错,但至少我没有掉进无视社区需求的陷阱,所以这个项目被一次又一次地中断、搁置,以便把更多精力投入到其他更基础的功能上。持久化、复制、延迟、自省能力得到的关注都远多于 Cluster,仅仅因为它们对用户群体而言更重要。

这个项目的另一个制约是,刚开始时我对分布式编程一无所知。我做的第一版设计非常糟糕,唯一做对的只有抓住了“产品层面”的需求:低延迟、线性可扩展,以及小集群下的低开销。然而所有细节都是错的,设计远比必要的复杂,使用的算法也不安全,诸如此类。

在取得一些微小进展的同时,我开始学习分布式编程的基础,重新设计了 Redis Cluster,并把同样的思路应用到了新版 Sentinel 上。这两个系统所用的分布式算法至今仍然很原始,因为它们都是异步复制、最终一致的系统,所以我无需去处理共识等复杂难题。不过即便面对的是一个相对简单的问题——至少比起编写一个 CP 存储来说——你也必须清楚自己在做什么,否则最终得到的系统可能完全是错的。

尽管有这么多问题,我还是坚持做这个项目,不断修正设计、修正实现,努力让它走向成熟,因为有一个简单的事实,就像房间里的大象一样,弥漫在整个 Redis 社区中:人们一次又一次地,靠自己的力量,并且很多时候是以完全错误的方式,在做两件事:

  1. 在 N 个节点间分片数据集。
  2. 实现一套灵敏的故障转移流程,以在特定故障发生时存活下来。

问题“2”严重到某种程度,以至于我在 Cluster 完成之前就决定先启动 Redis Sentinel 项目,以便尽快提供一套高可用方案,而且对于大多数只需要“2”而不需要“1”的用例来说,它比 Redis Cluster 更合适。

而现在,我终于开始看到这些努力的初步实际成果,我们有了一个候选版本,这是获得采用、修复剩余缺陷、并以更渐进方式改进系统的关键里程碑。

它究竟做了什么?

Redis Cluster 本质上是一种数据分片策略,能够在集群运行期间将键从一个节点重新分片到另一个节点,同时搭配一套故障转移机制,确保系统能够在特定类型的故障中存活。

从分布式数据库的角度看,Redis Cluster 在分区期间提供有限的可用性,以及一种弱一致性。基本上,它既不是 CP 系统,也不是 AP 系统。换句话说,Redis Cluster 并没有追求分布式系统在理论上所能达到的极限,而是为了换取某些现实世界中的特性。

它的一致性模型就是著名的“最终一致性”模型。基本上,如果节点因分区而出现不一致,可以保证当分区愈合后,所有负责某个给定键的节点都会对它的值达成一致。

不过它的合并策略是“最后一次故障转移胜出”,因此在网络分区期间接收到的写入可能会丢失。一个常见的例子是,主节点被划分到少数派分区,而客户端仍在向它写入。如果当分区愈合时,在多数派一侧已有从节点被提升来替代这个主节点,那么旧主节点接收到的那些写入就会丢失。

这反过来意味着,Redis Cluster 不必在数据结构中携带元数据去尝试合并值,因此 Redis 所支持的那些丰富命令和数据结构,Redis Cluster 也同样支持。也就是说,没有额外的内存开销,没有 API 限制,值的元素数量也不受限制,只是在分区期间的安全性会降低。

很容易理解,在像 Redis Cluster 这样设计的系统中,节点间出现分歧不是好事,因此系统会试图通过降低两个节点分歧的概率(以及分歧的程度)来缓解自身的不足。这通过以下几种方式实现:

  1. 少数派一侧的分区将变为不可用。
  2. 复制机制的设计使得通常对客户端的回复和向从节点的复制流是同时发送的。
  3. 当有多个从节点可用于接管故障主节点时,系统会尝试挑选那个看起来与故障主节点分歧最小的。

这些策略并不会改变系统的理论属性,但为常见的 Redis Cluster 故障模式提供了更多现实层面的保护。

对于 Redis 的 API 和使用场景,我认为这种设计是合理的,尽管过去有很多人并不认同。不过我的看法是,每位设计者都有权按自己的想法去设计系统,只有一条规则:实话实说,因此 Redis Cluster 在官方文档中对其局限性和故障模式作了清晰的说明。

一个系统是否有用,取决于用户以及眼前的用例。我的感觉是,六年来用户即使在完全没有任何集群支持的情况下仍持续使用 Redis,正是因为用例让这成为可能,而 Redis 所提供的某些特定功能和性能,使其非常适合解决特定问题。我希望 Redis Cluster 能让许多这类用户的生活变得更好。

未来的路

我们终于有了一个可交付的最小可行产品,它已经足够稳定,让用户可以认真开始测试,并在某些场景下直接采用。采用的人越多,我们就能把它改进得越好。这是从 Redis 和 Sentinel 身上学到的:接下来就是让软件从“可用”走向“成熟”的渐进过程。倾听用户、修复缺陷、用测试覆盖更多代码……

与此同时,我也开始思考 Redis Cluster 的下一个版本,在 v1 的基础上加入许多目前还来不及添加的有用特性,比如多数据中心支持、通过命令重放提升少数派分区的写入安全性、自动的节点均衡(目前如果某些节点过空而另一些过满,还需要手动重新分片),以及更多其他功能。

此外,我认为 Redis Cluster 还可以从一种专为缓存设计的特殊执行模式中受益,在该模式下,节点会接受对其不负责的哈希槽的写入,以便在少数派分区中仍保持可用。

改进和修正我们的实现与设计永远都有时间,但如果过于执着于软件“应该”是什么样子,就有风险让它在“雾件”范畴里停留得比必要时间更久。是时候放手了。祝大家尽情享用 Redis Cluster!

Redis Cluster RC1 已在 GitHub 上以 '3.0.0-rc1' 标签发布,也可在 Redis.io 下载页面 http://redis.io/download 以 tarball 形式获取。

本文章由 muse-spark-1.2-contributor 进行翻译

评论