Redis 是一种流行的内存数据存储,以其速度和灵活性而闻名。当特定工作负载的内存和计算需求可以在单个节点内管理时,它就能高效运行。然而,超出单个节点的扩展通常会导致考虑 Redis 集群。人们普遍认为过渡到 Redis 集群很简单,现有应用程序的行为也将相同,但实际情况并非如此。虽然 Redis 集群解决了某些扩展问题,但它也带来了巨大的复杂性。本文将讨论 Redis 集群扩展的局限性,并介绍一些可以满足许多组织需求的更简单的替代方案。
什么是Redis集群?
Redis Cluster 是一种分布式实现,允许您自动在多个主 Redis 实例之间共享数据,从而实现水平扩展。在 Redis 集群设置中,键空间被分为 16384 个哈希槽(了解更多 此处),有效设置 16384 个主实例的集群大小上限。然而,实际上,建议的最大大小约为 1000 个主实例。集群中的每个主实例管理这 16,384 个哈希槽的特定子集。为了确保高可用性,每个主实例可以与一个或多个副本实例配对。这种方法涉及跨多个实例的数据分片以实现可扩展性和复制以实现冗余,是许多分布式数据存储系统中的常见模式。一个具有三个主 Redis 实例的集群,每个主实例有一个副本,如下所示:
<图>
图>
如您所见,Redis Cluster 具有优雅的设计。但在没有深入研究实现细节的情况下,我们已经介绍了多个概念(分片、哈希槽等),只是为了有一个基本的了解。我们将在下面探讨更多挑战。
<小时/>
水平扩展的挑战
虽然 Redis 集群通常被认为只是独立 Redis 上的一个设置,但它在很多方面都有所不同。这意味着迁移到 Redis 集群充满了隐藏的挑战,即使是经验丰富的开发人员也会感到惊讶。
客户端库要求
从客户端库的角度来看,使用 Redis Cluster 需要适应不同的接口。这意味着您很可能需要使用特定于集群的客户端库 – 用于独立 Redis 的现有客户端将无法在集群中开箱即用。我们以 go-redis
Redis 为例就是一个例子。当连接到独立的Redis时,可以使用以下代码片段连接到具有指定主机、密码和逻辑数据库的实例:
导入“github.com/redis/go-redis/v9”
rdb := redis.NewClient(&redis.Options{
地址:“本地主机:6379”,
密码: "", // 未设置密码
DB: 0, // 使用默认逻辑数据库0(稍后详细介绍)
})
同一个包提供了特定于集群的客户端,但需要注意的是,它们是不同的结构:用于独立 Redis 的 redis.Client
和用于 Redis 的 redis.ClusterClient
簇。 redis.ClusterClient
的初始化过程有所不同,如下所示。这种区别至关重要,因为 redis.ClusterClient
在内部利用多个 redis.Client
对象来管理与集群中所有节点的通信。此设置中的每个 redis.Client
维护其单独的连接池。
导入“github.com/redis/go-redis/v9”
rdb := redis.NewClusterClient(&redis.ClusterOptions{
地址:[]字符串{
“本地主机:7001”,
“本地主机:7002”,
“本地主机:7003”,
“本地主机:7004”,
“本地主机:7005”,
},
})
附带说明一下,go-redis
包还提供了 redis.UniversalClient
接口来简化初始化过程,但它只是不同之上的抽象层客户类型。要使用 Redis 集群,需要开发如上所述的特定于集群的客户端来满足集群的其他要求。
多键操作
Redis 中的多键操作是指同时作用于多个键的命令或命令集。例如, MSET
命令是一个单个命令可以在单个原子操作中将多个键设置为多个值。类似地,事务或 Lua 脚本通常涉及多个键,但并非总是如此。 Redis 集群的一个关键限制是它无法处理多键操作,除非所涉及的键共享相同的主题标签,如规范 此处。您需要仔细检查应用程序中的所有多键操作,并且需要修改这些操作以避免使用多键操作或仅在同一主题标签的上下文中使用它们。
逻辑数据库
在 Redis 中,逻辑数据库本质上是同一 Redis 实例中的单独键空间。 Redis 支持多个数据库,这些数据库由数据库索引(一个简单的整数)来标识。除非另有说明,Redis 客户端在连接时通常使用默认数据库数据库0
。利用逻辑数据库可以是分离不同类型数据的有效策略(例如,将会话数据与应用程序数据隔离)。可以使用SELECT
命令进行切换不同逻辑数据库之间。但是,正如 SELECT
命令文档中所指定的那样,您将失去 Redis 集群的这种隔离性。因此,如果您要过渡到 Redis 集群,则需要从头开始彻底重新构建键空间架构。
设置和维护的复杂性
上面我们讨论的只是应用层面。然而,集群的影响延伸到基础设施的复杂性和运营成本。
首先,启用 Redis 集群会大大增加基础设施的复杂性以及管理基础设施的团队的运营负担。您不需要在单个 Redis 实例上监控指标和可用性,而是需要在数十个或数百个节点上进行监控。
其次,工作负载应在集群中均匀分布,同时最大限度地减少特定实例中的热点。但实际上,这种情况并不经常发生。一个常见的误解是创建集群会自动导致所有分片之间的流量统一。然而,特定的时段几乎总是能比其他时段获得更多的流量。虽然 Redis 允许您对实例进行重新分片,但它没有自动重新平衡功能。
最后但并非最不重要的一点是,实例越多,潜在的故障点就越多。虽然集群在节点出现故障时提供故障转移功能,但成倍增加的风险是真实存在的。我们假设任何单个 Redis 实例发生故障的可能性为 0.01%。在 100 次情况下,这种可能性会上升到 1-2%,这意味着更多的事故、消防演习以及可能的不眠之夜。
<小时/>
Redis 集群替代方案
鉴于 Redis 集群面临的挑战,值得考虑可以提供更简单的 Redis 扩展路径的替代方案。
应用级优化
调整应用程序对 Redis 的访问模式可以显着减少工作数据大小。通过对键、数据结构和查询进行小幅调整,您可以从现有的 Redis 部署中获得更多收益。例如,在 Abnormal Security,他们将 64 个字符的密钥字符串缩短为 32 个字符。这减少了每个键的内存占用,使它们能够容纳多 15% 的键值对。然而,应用程序级优化并不总是可行的。深入研究优化过程后,有时会感觉您是在满足数据存储的需求,而不是利用它来支持您的业务目标。
内存有限的工作负载
如果吞吐量不是问题,并且您只需要更多内存,最简单的解决方案是升级到更高内存的实例。当然,这意味着这个更强大的实例附带的额外 CPU 将处于闲置状态,不会加入其中。有时为它们付费很烦人,但至少你没有为全球变暖做出贡献。
但是,由于 Redis 是单线程的,如果您的工作负载受 CPU 限制(例如,涉及大量解析和修改 JSON 数据),那么仅仅添加更多 CPU 可能没有帮助。
数据分层到 SSD/闪存
由于闪存上的 Redis 将所有键保留在内存中,并且仅根据需要将值存储到磁盘,因此只有当您的键大小平均远小于值大小时,闪存上的 Redis 才是一个不错的选择。例如,如果 100 字节的密钥存储 120 字节的值,则使用闪存存储只能节省最少的内存。
此外,在 80% 的时间访问 20% 的数据集的通用原则适用于总数据量,包括键和值。因此,如果您仅检索 20% 的键,但这些键对应 80% 的值,则 Flash 上的 Redis 没有帮助。
<图>
图>
除了上述技术之外,另一种选择是采用更现代的 Redis 实现。 Dragonfly 是一种简单、高性能且经济高效的内存数据存储,提供与 Redis API 的完全兼容性。 Dragonfly 允许您继续垂直扩展至多达 128 个内核和 1TB 内存。单个 Dragonfly 节点可以提供与 Redis 集群类似的规模,但不会增加操作复杂性和上面讨论的 Redis 功能限制。
蜻蜓的扩展能力
Dragonfly 是一种简单的 Redis 替代品,无需集群即可满足扩展需求。通过在单个节点上利用多线程无共享架构,Dragonfly 可以处理大量内存工作负载和流量峰值,否则需要集群。
与集群设置相比,Dragonfly 的一些主要优势包括:
- 无需更改客户端库或应用程序代码
- 操作管理简单,没有分片复杂性
- 通过减少实例数量来节省成本
- 通过灵活的扩展避免性能热点
- 从任何 Redis 部署轻松迁移
将应用程序从 Redis 迁移到 Dragonfly 时,重要的是要认识到可能需要进行一些调整才能充分实现 Dragonfly 的优势。为单线程 Redis 构建的应用程序通常会针对最小并行度进行优化。相比之下,蜻蜓是多线程的。花时间调整客户端代码或配置以增加与 Dragonfly 的连接数量并将数据分散到更多键上是值得的。
结论
虽然超过一定规模(通常是 700GB 或 1TB 内存)后不可避免地需要水平扩展,但许多组织在用尽垂直扩展选项之前就过早地进行水平扩展。这会导致本可以安装在单个、功能充足的节点上的工作负载出现不必要的操作复杂性。
对于许多用例来说,容量限制是理论上的而不是实际的:即使您的业务增长,有时在 Redis 中保存数据的特定用例也更加有限。
即使您最终必须水平扩展,您仍然希望使用具有 16 或 32 个核心的相对强大的实例,而不是将工作分散到数百个小型实例上。在这种情况下,每个实例都成为潜在的薄弱环节:如果流量发生变化,或者吞吐量或负载突然激增,小型实例将无法应对。
虽然 Redis Cluster 提供水平扩展,但其复杂性和功能损失的成本很高。 Dragonfly 可以在单个实例上支持每秒数百万次操作和 TB 级工作负载,从而规避了集群解决方案的许多缺陷。