dot 快速的未来即将在您的城市举办的活动中到来。

加入我们在 Redis 发布会

Redis 复制循环无休止:问题、原因和解决方案

Redis 的复制是一个不可或缺的工具——它既可以用来提高 Redis 设置的可用性(您可以在 这篇文章 中阅读更多关于 Redis 可用性的内容),也可以用来通过对只读从机进行读操作来扩展 Redis。

在实施复制时,Redis 利用其核心功能——即 RDB 文件——提供了一种简单而优雅的机制,该机制在大多数情况下都是有效的。复制被广泛采用,包括在我们自己的 Redis 云服务中,并且是 Redis 的另一个有用且经过验证的功能。

但是,在某些情况下,激活 Redis 的复制可能并非易事。这些通常是罕见且极端的场景,例如当您的数据集大小显着增长时。但在深入探讨这些细节之前,让我们先了解一下 Redis 复制的工作原理。

Yoda and Luke, Master and Apprentice
“在一个遥远的银河系中”

Redis 中的复制就像从机(或学徒)与其主机的对话。
这种对话遵循以下思路

  1. 学徒:“我想学习,成为像您一样的人。”
  2. 主机:“耐心你必须有,我的年轻学徒。嗯哼。”
  3. 主机复制自身,然后……
    1. 复制后的进程将数据集转储到磁盘文件 (RDB) 中,
    2. 主进程继续为常规客户端请求提供服务,并且
    3. 对数据进行的任何更改都会复制到主进程上的复制缓冲区中。
  4. 转储完成后,主机说:“趁热来拿吧。
  5. 学徒通过网络读取文件,并将其写入自己的磁盘。
  6. 学徒将文件存储在本地后,会加载该文件。
  7. 加载完成后,学徒问道:“好了,我已经完成了我的循环。我准备好了。
  8. 如果缓冲区中存在任何更改,主机就会说:“准备好了?你了解准备好了意味着什么吗?感受原力!”并将存储的更改重播到从机。
  9. 缓冲区中没有剩余更改需要重播后,主机会说:“你不再需要训练了。你已经知道了你需要的东西。
  10. 从那一刻起,主机接收到的任何新的更改请求都会被重播到学徒。

上述交换本质上允许从机在两个阶段同步主机数据集的内容:首先,复制完整的,尽管稍微过时的数据主体;然后,在更短的追赶期间应用一个仅包含更新的局部子集。

“尺寸无关紧要。看看我。你难道要凭我的尺寸来评判我吗?”

如上所述,某些数据库(取决于其使用目的和用途)的大小会显着增长。增长可能是突然发生的,也可能随着时间的推移逐渐发生,但无论如何,事实仍然是——您的数据库已经变得相当大。而更大并不总是更好,尤其是在尝试引导复制从机时。

当从机尝试从管理着大型数据集(大约 25GB 或更大)的主机同步时,会遇到几个问题。首先,主机服务器可能需要大量的 RAM,甚至高达数据库大小的 3 倍,才能进行快照。虽然对于小型数据库来说也是如此,但随着数据库的增长,这项要求变得越来越难以满足。其次,数据集越大,用于快照目的复制另一个进程就越耗时且越困难,这直接影响主服务器进程。

这种现象被称为“由于 fork 导致的延迟”,并在 这里redis.io 上进行了说明。但是,让我们假设后者不是问题,并且通过投入足够的硬件,您已经为主机服务器提供了足够的资源,以便创建快照和 fork 延迟可以忽略不计。请记住,在完成所有 fork 操作后,需要将文件从主机复制到从机。

遗憾的是,这是通过与客户端用来从数据库访问数据库的相同互连进行的。大型数据库在大多数情况下由许多生成大量流量的客户端使用。此外,在云设置中,相同的网络也可能用于访问类似 EBS 的网络附加存储。将 10GB 的文件传输流量添加到该传输介质中几乎不会减少任何现有的拥塞。实际上,恰恰相反。即使假设存在最佳网络条件,仍然存在关于“胖乎乎的”RDB 文件能够通过电线传播并写入本地磁盘的速度的物理限制。

底线是,考虑到这些因素以及它们所产生的复合效应,从机需要时间才能准备好并准备好文件以供加载。并且一旦到位,从机也需要时间来加载文件。您不需要详细的模型或复杂的数学证明来直观地理解一个事实,即数据集越大,复制、转储、复制和加载到从机中所需的时间就越长。

但这又怎样呢?”你可能会说,“我并不需要每天设置一个新的从机。我有时间,我可以等。“你必须忘记你已经学到的东西”,你将无休止地等待。

从机永远无法完成同步,并且复制将不会开始。这是因为在创建快照、传输快照并将其加载到从机中的过程中,主机一直在忙于处理请求(在大型繁忙的数据库中,可能处理了大量请求)。更新在专用的复制缓冲区中累积,但该缓冲区的大小最终是有限的,并且一旦耗尽,它就无法再用于将从机更新到最新状态。

如果没有有效的更新缓冲区来追赶,从机就无法完成必要的初步同步循环,从而实时地开始从主机复制更新。为了纠正这种情况,Redis 在这些情况下会重新启动从机的同步过程。

因此,学徒又回到了起点,忘记了之前学到的所有东西,并带着一个请求回到了主机:“我想学习,成为像您一样的人。”然而,由于基本情况保持不变,后续尝试启动复制很可能与最初的迭代一样注定要失败。

新希望

这种情况虽然罕见,但确实是存在的,正如 Manohar 在 这里 提出的那样。即将发布的 Redis v2.8 肯定会对此进行改进,在将来,开源社区几乎可以肯定能完全克服这个问题。同时,如果您正在寻找立竿见影的解决方案,您可以访问我们的 github 下载我们版本的 Redis 2.6.14。在这个版本中,我们包含了一个客户端节流机制,以谨慎地为从机赢得足够的时间来完成同步。我们的节流机制通过在主机服务器对应用程序客户端请求的响应中引入延迟来工作。虽然乍一看似乎违反直觉,但额外的延迟为从机提供了足够的“喘息空间”来完成传输并重播更新日志,然后再者耗尽空间,从而允许同步完成并开始复制。

在实施这种机制时,我们添加了新的配置变量

slave-output-buffer-throttling

该变量使用以下语法设置

CONFIG SET slave-output-buffer-throttling <low> <high> <rate> <delay>

其中

  • <low> 是缓冲区大小的字节阈值,一旦超过该阈值,就会激活节流
  • <high> 是缓冲区允许达到的最大字节大小,直到复制启动
  • <rate> 是每秒复制的估计速率(以字节为单位)
  • <delay> 是以毫秒为单位给出的最大强制延迟

因此,例如,以下设置

CONFIG SET slave-output-buffer-throttling 32768 131072 102400 1000

会导致复制过程按以下方式进行,但会有一些更改

  1. 主机复制自身,然后
    1. 复制后的进程将数据集转储到磁盘文件 (RDB) 中
    2. 主进程继续为客户端请求提供服务,但
      1. 只要缓冲区的大小小于 <low>(例如 32768 字节或 32MB)值,请求就会正常处理
      2. 一旦缓冲区的大小超过 <low> 阈值,主机就会估计完成复制循环所需的时间,并且可能会通过在其响应中添加最多 <delay>(例如 1000 毫秒)来强制客户端节流
    3. 对数据进行的任何更改都会复制到主进程上的复制缓冲区中

“…难以看清。未来永远在运动中。”

主机对完成复制循环所需时间的估计方式如下

  • 在转储创建、从机获取并处理以及从机准备好用于在线流式传输新更新后,复制循环被认为已完成。
  • 主机依赖于提供的 <rate> 参数作为每秒可以处理的有效复制量。

在我们的示例中,假设数据集的大小为 25GB。鉴于我们设置的速率为 100MB/s(或 102400 字节/秒)

slave-output-buffer-throttling

主服务器将估计复制周期将在 250 秒内完成(= 25GB / 100MB/s)。

然后,如果复制缓冲区增长过快,主服务器将通过延迟响应来启动流量控制。 `<high>` 参数确定复制过程结束时允许的最大缓冲区大小,因此流量控制会在任何时候被触发并按比例应用。这意味着,例如,在循环开始后的 125 秒,主服务器将假设它已经完成了 50%。此时,如果复制缓冲区的大小超过 64MB(这是 `<high>` 131072 字节值的 50%),主服务器将应用延迟。

引入的实际延迟本身与缓冲区超出限制的程度成正比,并且不会超过 `<delay>` 设置的 1000 毫秒,以保持服务器的响应能力。出于同样的原因,服务器永远不会限制新连接的第一个请求。

最后,

slave-output-buffer-throttling

以及标准 Redis

client-output-buffer-limit

(了解更多信息 这里)机制可以结合使用,因此您需要确保它们不会冲突。您可以通过设置

client-output-buffer-limit

高于

slave-output-buffer-throttling

例如

CONFIG SET client-output-buffer-limit 262144 131072 60

在此示例中,如果流量控制无法抑制缓冲区的大小(可能是由于创建大型键的请求导致的),那么标准

client-output-buffer-limit

机制将在缓冲区达到 256MB 或在超过 `<high>` 128MB 限制超过 60 秒时启动并中断循环。

我们希望这些解释和解决方案对你们中的一些人有用!愿原力与你们同在。