在我们深入细节之前,先来谈谈一个显而易见的问题:云上的 DBaaS 产品,即“数据库即服务”。毫无疑问,了解 Redis 如何扩展以及如何部署它很有用。但是部署和维护一个 Redis 集群需要相当多的工作。所以如果你不想自己部署和管理 Redis,那么可以考虑注册 Redis Cloud,我们的托管服务,让我们来为你负责扩展。当然,这条路并不适合所有人。正如我所说,这里有很多东西需要学习,所以我们开始深入探讨吧。
我们从可伸缩性开始。这是一个定义
“可伸缩性是系统通过增加资源来处理不断增长的工作量的属性。” 维基百科
两种最常见的伸缩策略是垂直伸缩和水平伸缩。垂直伸缩,也称为“向上伸缩”,意味着为服务器增加更多资源,如 CPU 或内存。水平伸缩,或“向外伸缩”,意味着向资源池中添加更多服务器。这就像是只买一台更大的服务器和部署一个服务器集群之间的区别。
举个例子。假设您有一台服务器,内存为 128 GB,但您知道数据库需要存储 300 GB 的数据。在这种情况下,您有两种选择:您可以为服务器增加更多内存,使其能够容纳 300GB 数据集,或者您可以再增加两台服务器,并将 300GB 数据分摊到这三台服务器上。达到服务器内存限制是您可能想要向上或向外伸缩的一个原因,但达到吞吐量或每秒操作次数的性能限制也表明需要进行伸缩。
由于 Redis 大部分是单线程的,Redis 无法利用服务器 CPU 的多核进行命令处理。但是,如果我们将数据分散到两个 Redis 服务器之间,我们的系统可以并行处理请求,从而将吞吐量提高近 200%。实际上,通过向系统添加更多 Redis 服务器,性能几乎可以线性扩展。这种为了扩展而将数据分散到多个服务器之间的数据库架构模式称为分片(sharding)。承载部分数据的服务器称为分片(shards)。
这种性能提升听起来很棒,但并非没有代价:如果我们将数据分割并分布到两个分片(即两个 Redis 服务器实例)上,我们如何知道在哪里查找每个键?我们需要一种方法来将键一致地映射到特定的分片。实现这一点有多种方法,不同的数据库采用不同的策略。Redis 选择的方法称为“算法分片”,其工作原理如下
为了找到键所在的分片,我们计算键名的数字哈希值,并将其对总分片数取模。因为我们使用确定性哈希函数,只要分片数量不变,键“foo”将始终位于同一个分片上。
但是,如果我们想进一步增加分片数量(这个过程通常称为重新分片),会发生什么呢?假设我们添加一个新分片,使总分片数变为三个。现在当客户端尝试读取键“foo”时,他们会像以前一样运行哈希函数并对分片数量取模,但这次分片数量不同了,我们是对三而不是对二取模。可以理解,结果可能会不同,将我们指向错误的分片!
重新分片是算法分片策略的一个常见问题,可以通过重新计算键空间中所有键的哈希值,并将它们移动到适合新分片数量的分片来解决。然而,这不是一项简单的任务,它可能需要大量的时间和资源,在此期间数据库可能无法达到其全部性能,甚至可能变得不可用。
Redis 选择了一种非常简单的方法来解决这个问题:它引入了一个新的逻辑单元,位于键和分片之间,称为哈希槽(hash slot)。
一个分片可以包含许多哈希槽,而一个哈希槽包含许多键。数据库中的哈希槽总数始终是 16384 (16K)。这次,取模运算不再是对分片数量进行,而是对哈希槽数量进行,即使在重新分片时,哈希槽数量也保持不变,最终结果会告诉我们正在查找的键所在的哈希槽位置。当确实需要重新分片时,我们只需将哈希槽从一个分片移动到另一个分片,根据需要在不同的 Redis 实例之间分布数据。
既然我们了解了什么是分片以及它在 Redis 中的工作原理,我们终于可以介绍 Redis 集群(Redis Cluster)了。Redis 集群提供了一种运行 Redis 安装的方式,其中数据会自动分散到多个 Redis 服务器或分片上。Redis 集群还提供了高可用性。因此,如果您部署 Redis 集群,则不需要(或使用)Redis Sentinel。
Redis 集群可以检测到主分片何时发生故障,并在没有任何外部手动干预的情况下将副本提升为主。它是如何做到的?它如何知道主分片已经发生故障,以及如何将其副本提升为新的主分片?我们需要启用复制功能。假设每个主分片有一个副本。如果我们的所有数据分布在三个 Redis 服务器之间,我们将需要一个六个成员的集群,包含三个主分片和三个副本。
所有 6 个分片通过 TCP 相互连接,并不断地互相 PING,使用二进制协议交换消息。这些消息包含哪些分片已回复 PONG(因此被认为是活着的)以及哪些未回复的信息。
当足够多的分片报告说某个主分片没有响应时,它们可以达成一致,触发故障转移,并将该分片的副本提升为新的主分片。需要有多少分片同意某个分片处于离线状态才能触发故障转移?这是可配置的,您可以在创建集群时进行设置,但有一些非常重要的指导原则需要遵循。
如果集群中有偶数个分片,比如六个,并且发生网络分区将集群分成两部分,那么您将得到两个各包含三个分片的分组。左侧的分组将无法与右侧分组中的分片通信,因此集群会认为它们已离线,并将触发任何主分片的故障转移,导致左侧拥有所有主分片。在右侧,这三个分片会认为左侧的分片已离线,并将触发左侧所有主分片的故障转移,导致右侧拥有所有主分片。双方都认为自己拥有所有主分片,将继续接收修改数据的客户端请求,这是一个问题,因为客户端 A 可能在左侧将键“foo”设置为“bar”,但客户端 B 在右侧将同一键的值设置为“baz”。
当网络分区移除并且分片尝试重新加入时,我们将面临冲突,因为我们有两个分片,它们持有不同的数据并都声称是主分片,我们不知道哪个数据是有效的。
这被称为脑裂(split brain)情况,是分布式系统领域一个非常常见的问题。一个流行的解决方案是始终在集群中保持奇数个分片,这样当发生网络分裂时,左右分组会进行计数,并查看它们属于较大分组还是较小分组(也称为多数或少数)。如果它们是少数分组,则不会尝试触发故障转移,也不会接受任何客户端写请求。
总结来说:为了防止 Redis 集群中的脑裂情况,请始终保持奇数个主分片,并且每个主分片有两个副本。