Redis("REmote DIctionary Service")是一个开源的键值数据库服务器。
对Redis最准确的描述是,它是一个数据结构服务器。Redis的这一特定性质导致了它在开发者中的大部分流行和采用。
与其迭代、分类和排序行,如果数据是你从头开始想要的数据结构,会怎么样呢?早期,它的使用方式很像Memcached,但随着Redis的改进,它在许多其他用例中变得可行,包括发布-订阅机制、流和队列。
主要而言,Redis是一个内存数据库,在另一个 "真正的 "数据库(如MySQL或PostgreSQL)前面用作缓存,以帮助提高应用程序的性能。它利用了内存的速度,减轻了中央应用数据库的负载。
- 不经常变化和经常被请求的数据
- 对任务要求不高但经常演变的数据。
上述数据的例子可以包括会话或数据缓存和排行榜或仪表盘的滚动分析。
然而,对于许多用例,Redis提供了足够的保证,它可以作为一个成熟的主数据库使用。再加上Redis插件和它的各种高可用性(HA)设置,Redis作为一个数据库对于某些场景和工作负载来说已经变得非常有用。
另一个重要方面是,Redis模糊了缓存和数据存储之间的界限。这里需要理解的重要一点是,在内存中读取和操作数据的速度比使用SSD或HDD的传统数据存储的速度要快得多。
最初,Redis最常被拿来与Memcached比较,后者在当时缺乏任何非易失性的持久性。
Memcached是由Brad Fitzpatrick在2003年创建的,比Redis早了6年。它最初是一个Perl项目,后来用C语言重写。它是当时事实上的缓存工具。它和Redis的主要区别在于它缺乏数据类型,而且它的驱逐策略有限,只有LRU(最近使用最少的)。
另一个区别是,Redis是单线程的,而Memcached是多线程的。Memcached在严格的缓存环境中可能表现良好,但在分布式集群中需要一些设置,而Redis天生就支持。
以下是目前这两个缓存之间的能力细分。
内存缓存 | 雷迪斯 | |
---|---|---|
亚毫秒延迟 | 是的 | 是的 |
开发者易用性 | 是的 | 是的 |
数据分区 | 是的 | 是的 |
支持广泛的编程语言 | 是的 | 是的 |
高级数据结构 | - | 是的 |
多线程架构 | 是的 | - |
快照 | - | 是的 |
复制 | - | 是的 |
交易 | - | 是的 |
发布/订阅 | - | 是的 |
Lua 脚本 | - | 是的 |
地理空间支持 | - | 是的 |
虽然现在可以配置它将数据持久化到磁盘的方式,但在刚推出时,Redis使用快照,将内存中的数据的异步拷贝持久化到磁盘上进行长期存储。不幸的是,这种机制有一个缺点,就是在快照之间可能会丢失你的数据。
自2009年成立以来,Redis已经发展成熟。我们将介绍它的大部分架构和拓扑结构,这样你就可以把Redis加入你的数据存储系统库中。
Redis 架构
在我们开始讨论Redis的内部结构之前,让我们讨论一下各种Redis的部署和它们的取舍。
我们将主要关注这些配置。
- 单个 Redis 实例
- Redis 高可用性
- Redis 哨兵
- Redis 集群
根据您的用例和规模,您可以决定使用一种设置或另一种设置。
单个 Redis 实例
单个 Redis 实例是最直接的 Redis 部署。它允许用户设置和运行可以帮助他们发展和加速服务的小型实例。但是,这种部署并非没有缺点。例如,如果此实例失败或不可用,则所有客户端对 Redis 的调用都会失败,从而降低系统的整体性能和速度。
如果有足够的内存和服务器资源,这个实例可以很强大。主要用于缓存的场景可能会以最少的设置显着提升性能。给定足够的系统资源,您可以在应用程序运行的同一机器上部署此 Redis 服务。
了解一些有关管理系统内数据的 Redis 概念是必不可少的。发送到 Redis 的命令首先在内存中处理。然后,如果在这些实例上设置了持久性,则在某个时间间隔上会有一个fork进程,以促进数据持久性 RDB(Redis 数据的非常紧凑的时间点表示)快照或 AOF(仅附加文件)。
这两个流程让 Redis 拥有长期存储,支持各种复制策略,并启用更复杂的拓扑。如果 Redis 未设置为保留数据,则在重新启动或故障转移时数据会丢失。如果在重启时启用了持久性,它会将 RDB 快照或 AOF 中的所有数据加载回内存中,然后实例可以支持新的客户端请求。
话虽如此,让我们看看您可能想要使用的更多分布式 Redis 设置。
Redis 高可用性
Redis 的另一个流行设置是主部署和与复制保持同步的辅助部署。当数据写入主实例时,它会将这些命令的副本发送到辅助实例的副本客户端输出缓冲区,这有助于复制。辅助实例可以是部署中的一个或多个实例。这些实例可以帮助扩展从 Redis 的读取或提供故障转移,以防 main 丢失。
高可用性
高可用性(HA)是一个系统的特点,旨在确保在高于平均水平的时间内,达到约定的运行性能水平,通常是正常运行时间。
在这些HA系统中,不出现单点故障是至关重要的,这样系统就可以优雅而快速地恢复。这导致了可靠的交叉,所以数据在从主站到副站的过渡期间不会丢失,此外,还可以自动检测故障并从中恢复。
由于我们现在已经进入了一个分布式系统,因此您需要在此拓扑中考虑许多新事物。以前简单的事情现在变得更加复杂。
Redis 复制
Redis 的每个主实例都有一个复制 ID 和一个偏移量。这两条数据对于确定副本可以继续其复制过程的时间点或确定它是否需要进行完整同步至关重要。对于主 Redis 部署上发生的每个操作,此偏移量都会增加。
Replication ID, offset
更明确地说,当 Redis 副本实例仅落后于主实例几个偏移量时,它会从主实例接收剩余的命令,然后在其数据集上重播,直到同步。如果两个实例无法就复制 ID 达成一致,或者主实例不知道偏移量,则副本将请求完全同步。这涉及到一个主实例创建一个新的 RDB 快照并将其发送到副本。在发生此传输时,主实例正在缓冲快照截止和当前偏移之间的所有中间更新,以便在与快照同步后发送到辅助实例。完成后,复制可以正常继续。
如果一个实例具有相同的复制 ID 和偏移量,则它们具有完全相同的数据。现在您可能想知道为什么需要复制 ID。当 Redis 实例被提升为主实例或作为主实例从头开始重新启动时,它会被赋予一个新的复制 ID。这用于推断此新提升的辅助实例从中复制的先前主实例。这允许执行部分同步(与其他辅助节点),因为新的主实例会记住其旧的复制 ID。
例如,两个实例(主实例和辅助实例)具有相同的复制 ID,但偏移量相差几百个命令,这意味着如果在偏移量后面的实例上重放这些实例,它们将具有相同的数据集。现在,如果复制 ID 完全不同,并且我们不知道新降级(并重新加入)辅助节点的先前复制 ID(没有共同祖先)。我们将需要执行昂贵的完全同步。
或者,如果我们知道以前的复制 ID,我们就可以推断如何使数据同步,因为我们能够推断出它们共享的共同祖先,并且偏移量对于部分同步再次有意义。
Redis 哨兵
Sentinel 是一个分布式系统。与所有分布式系统一样,Sentinel 有几个优点和缺点。Sentinel 的设计方式是,一组哨兵进程协同工作以协调状态,从而为 Redis 提供高可用性。毕竟,您不希望保护您免受故障影响的系统有自己的单点故障。
Sentinel 负责一些事情。首先,它确保当前的主实例和辅助实例正常运行并做出响应。这是必要的,因为哨兵(与其他哨兵进程)可以在主节点和/或辅助节点丢失的情况下发出警报并采取行动。其次,它在服务发现中发挥作用,就像其他系统中的 Zookeeper 和 Consul 一样。所以当一个新的客户端尝试向 Redis 写东西时,Sentinel 会告诉客户端当前的主实例是什么。
因此,哨兵不断监控可用性并将该信息发送给客户端,以便他们能够在他们确实进行故障转移时对其做出反应。
以下是它的职责:
- 监控——确保主要和次要实例按预期工作。
- 通知—通知系统管理员 Redis 实例中的事件。
- 故障转移管理——如果主实例不可用并且足够多的(法定人数)节点同意这是真的,Sentinel 节点可以启动故障转移过程。
- 配置管理——Sentinel 节点还充当当前主要 Redis 实例的发现点。
以这种方式使用 Redis Sentinel 可以进行故障检测。此检测涉及多个哨兵进程同意当前主实例不再可用。这个协议过程称为 Quorum。这可以提高鲁棒性并防止一台机器行为异常并且无法访问主 Redis 节点。
法定人数
法定人数是指一个分布式系统为了被允许执行故障转移等操作而必须获得的最低票数。这个数字是可配置的,但应该反映出该分布式系统中的节点数量。大多数分布式系统的规模为3个或5个,四分位数分别为2个和3个。在系统需要打破平局的情况下,奇数的节点是比较好的。
此设置并非没有缺点,因此我们将在使用 Redis Sentinel 时介绍一些建议和最佳实践。
您可以通过多种方式部署 Redis Sentinel。老实说,要提出任何明智的建议,我需要比目前有关您的系统的更多背景信息。作为一般指导,我建议在每个应用程序服务器旁边运行一个哨兵节点(如果可能),这样你也不需要考虑哨兵节点和实际使用 Redis 的客户端之间的网络可达性差异。
您可以将 Sentinel 与 Redis 实例一起运行,甚至可以在独立节点上运行,但这会以不同的方式使事情复杂化。我建议至少运行三个具有至少两个法定人数的节点。这是一个简单的图表,分解了集群中的服务器数量以及相关的法定人数和可容忍的可持续故障。
服务器数量 | 法定人数 | 允许的故障数 |
---|---|---|
1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
6 | 4 | 2 |
7 | 4 | 3 |
服务器数量和允许故障数量的仲裁表。
这会因系统而异,但总体思路是不变的。
让我们花点时间思考一下这样的设置会出现什么问题。如果你运行这个系统足够长的时间,你会遇到所有这些。
- 如果哨兵节点超出法定人数怎么办?
- 如果网络分裂将旧的主实例置于少数群体中怎么办?这些写入会发生什么?(剧透:当系统完全恢复时它们会丢失)
- 如果哨兵节点和客户端节点(应用程序节点)的网络拓扑错位会发生什么?
没有持久性保证,特别是因为磁盘的持久性(见下文)是异步的。还有一个烦人的问题,当客户发现新的primary时,我们失去了多少写给一个不知道的primary?Redis 建议在建立新连接时查询新的主节点。根据系统配置,这可能意味着大量数据丢失。
如果您强制主实例将写入复制到至少一个辅助实例,有几种方法可以减轻损失程度。请记住,所有 Redis 复制都是异步的,并且有其权衡。因此,它需要独立跟踪确认,如果它们没有得到至少一个辅助实例的确认,主实例将停止接受写入。
Redis 集群
我相信很多人都想过当您无法将所有数据存储在一台机器上的内存中时会发生什么。目前,单个服务器中可用的最大 RAM 为 24TIB,目前在 AWS 上在线列出。当然,这很多,但对于某些系统来说,这还不够,即使对于缓存层也是如此。
Redis Cluster 允许 Redis 的水平扩展。
垂直和水平缩放
随着系统的发展,您有三个选择。
- 少做(没有人会完全这样做,因为我们是贪得无厌的怪物)。
- 放大。
- 向外扩展。
认真对待后两者,放大和缩小分别称为垂直和水平缩放。垂直扩展是一种技术,您可以让更大更好的机器更快地完成工作,并希望您的所有问题都能与您的硬件一起很好地扩展。即使这是可能的,您最终也会受到您使用的硬件的限制。
一旦达到这一点(更有可能并且希望早于这一点),您将需要通过将工作负载分散到负责整体较小部分的多台较小机器上来水平扩展系统。
因此,让我们先弄清楚一些术语;一旦我们决定使用Redis集群,我们就决定将我们要存储的数据分散到多台机器上,称为分片。因此,集群中的每个Redis实例被认为是整个数据的一个分片。
这就带来了一个新的问题。如果我们向集群推送一个密钥,我们如何知道哪个Redis实例(分片)在保存该数据?有几种方法可以做到这一点,但Redis Cluster使用算法分片。
为了找到一个给定密钥的分片,我们对密钥进行散列,并将总结果与分片的数量相乘。然后,使用一个确定的哈希函数,这意味着一个给定的密钥将总是映射到同一个分片,我们可以推理出一个特定的密钥在未来读取它时将在哪里。
当我们后来想在系统中添加一个新的分片时会发生什么?这个过程被称为重新分片。
假设钥匙 "foo "在引入一个新的分片后被映射到零号分片,它可能被映射到五号分片。然而,如果我们需要快速增长系统,移动数据以反映新的分片映射将是缓慢和不现实的。这对Redis集群的可用性也有不利影响。
Redis Cluster为这个问题设计了一个叫做Hashslot的解决方案,所有的数据都被映射到这个Hashslot中。有16K个hashslot。这为我们提供了一个合理的方法,将数据分散到整个集群中,当我们添加新的分片时,我们只需在系统中移动哈希槽。通过这样做,我们只需要将哈希槽从分片区移动到分片区,并简化了向集群中添加新的主实例的过程。
这是在没有任何停机时间的情况下实现的,对性能的影响也很小。让我们通过一个例子来谈谈。
M1包含从0到8191的哈希槽。
M2包含从8192到16383的哈希槽。
因此,为了映射 "foo",我们对密钥(foo)进行确定性的哈希运算,然后用哈希槽的数量(16K)进行修改,从而得到M2的映射。现在我们假设增加一个新的实例,M3。新的映射将是
M1包含从0到5460的哈希槽。
M2包含从5461到10922的哈希槽。
M3包含从10923到16383的哈希槽。
所有映射M1中的哈希槽的键现在都映射到了M2中,这些键都需要移动。但是单个键对哈希槽的散列就不需要移动了,因为它们已经在哈希槽中被划分了。因此,这一级的误导解决了算法分片的重新分片问题。
Gossiping
Redis Cluster使用gossip协议来确定整个集群的健康状况。在上面的插图中,我们有3个M节点和3个S节点。所有这些节点不断进行通信,以了解哪些分片是可用的,并准备为请求提供服务。如果有足够多的分片同意M1没有响应,它们可以决定将M1的二级S1提升为一级,以保持集群的健康。触发这一点所需的节点数量是可配置的,而且必须正确对待这个问题。如果你做得不恰当,你可能会出现这样的情况:当分区的两边都相等时,如果不能打破平局,集群就会分裂。这种现象被称为分裂的大脑。作为一般规则,必须要有奇数的主节点和每个节点的两个副本,这样的设置才是最稳健的。
Redis 持久化模型
如果我们要使用Redis来存储任何种类的数据进行安全保管,那么了解Redis是如何做到的就很重要了。在很多情况下,如果你丢失了Redis所存储的数据,并不是世界末日。把它作为一个缓存,或者在它为实时分析提供动力的情况下,如果数据丢失,也不是世界末日。
在其他情况下,我们希望对数据的持久性和恢复有一些保证。
无持久性
无持久性:如果你愿意,你可以完全禁用持久性。这是运行Redis的最快方式,没有持久性保证。
RDB
RDB (Redis数据库)。RDB持久化在指定的时间间隔内对你的数据集进行时间点快照。
这种机制的主要缺点是,快照之间的数据会丢失。此外,这种存储机制还依赖于fork主进程,在一个较大的数据集中,这可能导致服务请求的瞬间延迟。也就是说,RDB文件在内存中的加载速度比AOF快得多。
AOF
AOF(仅附加文件):AOF持久性记录了服务器收到的每一个写操作,这些操作将在服务器启动时再次播放,重建原始数据集。
这种确保持久性的方式比RDB快照更持久,因为它是一个只需追加的文件。当操作发生时,我们把它们缓冲到日志中,但它们还没有被持久化。这个日志包含了我们实际运行的命令,以便在需要时进行重放。
然后在可能的情况下,我们用fsync把它冲到磁盘上(何时运行是可配置的),它将被持久化。缺点是格式不紧凑,比RDB文件占用更多磁盘。
fsync()将文件描述符fd所指的文件的所有修改的核心数据(即修改的缓冲缓存页)转移("冲刷")到磁盘设备(或其他永久存储设备),这样即使系统崩溃或重启,也可以检索到所有改变的信息。
由于各种原因,当对文件进行修改时,它们是在缓存中进行的,对fsync()的调用确保它们被持久化到磁盘上,以后可以访问。
能不能两个都用?
RDB + AOF:可以将 AOF 和 RDB 组合在同一个 Redis 实例中。如果以某种速度换取耐用性是一种折衷,那么您愿意做到。我认为这是设置 Redis 的一种可接受的方式。在重启的情况下,请记住如果两者都启用,Redis 将使用 AOF 来重建数据,因为它是最完整的。
Fork同步
现在我们了解了持久化的类型,让我们来讨论一下在像Redis这样的单线程应用程序中,我们究竟如何去做。
在我看来,Redis最酷的部分是它如何利用fork和写时拷贝来促进数据的持久性。
Fork是操作系统通过创建自己的副本来创建新进程的一种方式。通过这种方式,你可以得到一个新的进程ID和其他一些信息和句柄,所以新fork的进程(子进程)可以与原始进程的父进程对话。
现在是事情变得有趣的地方。Redis是一个分配有大量内存的进程,那么它如何在不耗尽内存的情况下进行复制呢?
当你fork一个进程时,父进程和子进程共享内存,在该子进程中Redis开始快照(Redis)进程。这是由一种称为 "写时复制 "的内存共享技术实现的--该技术传递对fork创建时的内存的引用。如果在子进程持久化到磁盘时没有发生变化,就不会进行新的分配。
在有变化的情况下,内核会跟踪每个页面的引用,如果对特定页面有多个引用,那么这些变化会被写入新的页面。子进程完全不知道这些变化,并且有一致的内存快照。因此,只有一小部分内存被使用,我们能够非常快速和有效地实现潜在的数千兆字节内存的时间点快照!
评论 (0)