学习

4.0 Redis 集群

Justin Castilla
作者
Justin Castilla, Redis 高级开发者倡导者

在我们深入细节之前,让我们先解决一个大问题:云端的 DBaaS 产品或“数据库即服务”。毫无疑问,了解 Redis 如何扩展以及如何部署它很有用。但是部署和维护 Redis 集群需要相当多的工作。因此,如果您不想自己部署和管理 Redis,请考虑注册 Redis Cloud,我们的托管服务,让我们为您进行扩展。当然,这种方式不适合所有人。正如我所说,这里有很多东西要学,所以让我们深入探讨吧。

我们将从可扩展性开始。以下是一个定义

“可扩展性是系统通过添加资源来处理越来越多的工作的能力。” 维基百科

两种最常见的扩展策略是垂直扩展和水平扩展。垂直扩展,也称为“向上扩展”,意味着向您的服务器添加更多资源,如 CPU 或内存。水平扩展,或“向外扩展”,意味着向您的资源池中添加更多服务器。这就像是在购买一台更大的服务器和部署整个服务器集群之间的区别。

举个例子。假设您有一台服务器,其内存为 128 GB,但您知道您的数据库需要存储 300 GB 的数据。在这种情况下,您有两种选择:您可以为您的服务器添加更多内存,以便它可以容纳 300 GB 的数据集,或者您可以添加两台服务器并将 300 GB 的数据分布在这三台服务器之间。达到服务器的内存限制是您可能需要向上扩展或向外扩展的原因之一,但达到吞吐量(每秒的操作)方面的性能限制也是需要扩展的指示。

由于 Redis 主要是单线程的,因此 Redis 无法利用您的服务器 CPU 的多个内核来处理命令。但是,如果我们将数据分布在两台 Redis 服务器之间,我们的系统可以并行处理请求,将吞吐量提高近 200%。事实上,通过向系统添加更多 Redis 服务器,性能将几乎线性地扩展。这种将数据分布在多台服务器之间以进行扩展的数据库架构模式称为分片。包含数据块的服务器被称为分片。

这种性能提升听起来很棒,但它并非没有代价:如果我们将数据划分为两个分片,它们只是两个 Redis 服务器实例,我们如何知道在哪里查找每个键?我们需要一种方法来一致地将一个键映射到一个特定的分片。有多种方法可以做到这一点,不同的数据库采用了不同的策略。Redis 选择的策略称为“算法分片”,它是这样工作的

为了找到一个键所在的 shard,我们会从键名中计算出一个数值哈希值,并将其模除以 shard 的总数。因为我们使用的是确定性哈希函数,所以键“foo”将始终位于同一个 shard 上,只要 shard 的数量保持不变。

但是,如果我们想进一步增加 shard 的数量,这个过程通常称为重新分片怎么办?假设我们添加一个新的 shard,使我们的 shard 总数变为三个。当客户端尝试读取键“foo”时,它们会像以前一样运行哈希函数并模除以 shard 的数量,但这次 shard 的数量不同,我们模除以 3 而不是 2。可以理解的是,结果可能不同,将我们指向错误的 shard!

重新分片是算法分片策略中常见的问题,可以通过对键空间中的所有键重新哈希并将它们移动到适合新的 shard 数量的 shard 来解决。但这并不是一项简单的任务,它可能需要大量的时间和资源,在此期间数据库无法达到其全部性能,甚至可能变得不可用。

Redis 选择了一个非常简单的方法来解决这个问题:它引入了一个新的逻辑单元,它位于键和 shard 之间,称为哈希槽。

一个 shard 可以包含多个哈希槽,一个哈希槽可以包含多个键。数据库中的哈希槽总数始终为 16384(16K)。这次,模除不再以 shard 的数量为准,而是以哈希槽的数量为准,即使在重新分片时也保持不变,最终结果将给出我们正在寻找的键所在的哈希槽的位置。当我们需要重新分片时,我们只需将哈希槽从一个 shard 移动到另一个 shard,根据需要将数据分布在不同的 Redis 实例中。

现在我们知道了什么是分片以及它在 Redis 中是如何工作的,我们终于可以介绍 Redis Cluster 了。Redis Cluster 提供了一种方法来运行 Redis 安装,其中数据会自动分布在多个 Redis 服务器或 shard 上。Redis Cluster 还提供了高可用性。因此,如果您部署了 Redis Cluster,则不需要(或使用)Redis Sentinel。

Redis Cluster 可以检测到主 shard 发生故障,并自动将一个副本提升为主 shard,无需外部手动干预。它是如何做到的呢?它如何知道主 shard 已经失效,以及如何将它的副本提升为主 shard?我们需要启用复制。假设每个主 shard 都有一个副本。如果我们的所有数据都分布在三台 Redis 服务器之间,我们需要一个六成员集群,包含三个主 shard 和三个副本。

所有 6 个 shard 通过 TCP 连接在一起,并不断互相 PING 以及使用二进制协议交换消息。这些消息包含有关哪些 shard 以 PONG 响应的信息,因此被认为是活动的,以及哪些没有响应。

当足够多的 shard 报告说某个特定 shard 没有响应时,它们可以协商触发故障转移并将该 shard 的副本提升为新的主 shard。在触发故障转移之前,需要多少个 shard 同意一个 shard 处于离线状态?好吧,这是可以配置的,您可以在创建集群时进行设置,但您需要遵循一些非常重要的指南。

如果您在集群中拥有偶数个 shard,例如 6 个,并且存在一个将集群划分为两部分的网络分区,那么您将拥有两组三个 shard。左侧的那组将无法与右侧的 shard 通信,因此集群会认为它们处于离线状态,并将触发对任何主 shard 的故障转移,导致左侧拥有所有主 shard。在右侧,三个 shard 将看到左侧的 shard 处于离线状态,并将触发对左侧任何主 shard 的故障转移,导致右侧拥有所有主 shard。双方都认为自己拥有所有主 shard,并将继续接收修改数据的客户端请求,这是一个问题,因为客户端 A 可能在左侧将键“foo”设置为“bar”,但客户端 B 可能在右侧将同一个键的值设置为“baz”。

当网络分区消失,shard 尝试重新加入时,我们将遇到冲突,因为我们有两个 shard 声称自己是主 shard,但它们持有不同的数据,我们不知道哪组数据有效。

这种情况被称为“脑裂”,在分布式系统中非常常见。一个流行的解决方案是在集群中始终保持奇数个分片,这样当出现网络分割时,左右两组会进行计数,查看它们是在较大的组还是较小的组(也称为多数派或少数派)。如果它们属于少数派,它们不会尝试触发故障转移,也不会接受任何客户端写入请求。

总结一下:为了防止 Redis 集群出现脑裂情况,始终保持奇数个主分片以及每个主分片两个副本。