键逐出

Redis 键逐出策略概述(LRU、LFU 等)

Redis 通常用作缓存,以加快对较慢的服务器或数据库的读取访问速度。由于缓存条目是持久存储数据的副本,因此当缓存内存不足时,逐出它们通常是安全的(如果需要,将来可以再次缓存)。

Redis 允许您指定逐出策略,以便在缓存大小超出设定的内存限制时自动逐出键。每当客户端运行向缓存添加更多数据的新命令时,Redis 都会检查内存使用情况。如果超出限制,Redis 将根据所选的逐出策略逐出键,直到使用的总内存回到限制以下。

请注意,当一个命令向缓存添加大量数据时(例如,一个大型集合交集存储到一个新键中),这可能会暂时大幅超出限制。

以下部分解释了如何配置缓存的内存限制,并描述了可用的逐出策略以及何时使用它们。

使用 maxmemory 配置指令

maxmemory 配置指令指定了缓存数据使用的最大内存量。您可以在启动时通过 redis.conf 文件设置 maxmemory。例如,要配置 100 兆字节的内存限制,您可以在 redis.conf 中使用以下指令

maxmemory 100mb

您还可以使用 CONFIG SET 在运行时使用 redis-cli 设置 maxmemory

> CONFIG SET maxmemory 100mb

maxmemory 设置为零表示您不想限制数据集的内存。这是 64 位系统的默认行为,而 32 位系统使用隐式内存限制 3GB。

当缓存大小超出 maxmemory 设置的限制时,Redis 将强制执行您选择的逐出策略,以防止缓存进一步增长。

为复制或持久化实例设置 maxmemory

如果您正在为服务器使用复制持久化,Redis 将使用一些 RAM 作为缓冲区来存储等待写入副本或 AOF 文件的一组更新。此缓冲区使用的内存不计入与 maxmemory 比较的总内存中,以判断是否需要逐出。

这是因为键逐出本身会生成必须添加到缓冲区的更新。如果这些更新被计入已使用的内存中,那么在某些情况下,通过逐出键节省的内存将立即被添加到缓冲区的更新数据占用。反过来,这将触发更多的逐出,由此产生的反馈循环可能会不必要地逐出缓存中的许多项目。

如果您使用复制或持久化,我们建议您设置 maxmemory 以留出少量空闲 RAM 来存储缓冲区。请注意,这对于 noeviction 策略来说不是必需的(有关逐出策略的更多信息,请参阅以下部分)。

使用 INFO 命令在 memory 部分返回一个 mem_not_counted_for_evict 值(您可以使用 INFO memory 选项仅查看此部分)。这是缓冲区当前使用的内存量。尽管确切的数量会有所不同,您可以使用它来估算在设置 maxmemory 之前需要从总可用 RAM 中减去多少。

逐出策略

使用 maxmemory-policy 配置指令选择当达到 maxmemory 设置的限制时要使用的逐出策略。

提供以下策略

  • noeviction:键不会被逐出,但当您尝试执行缓存新数据的命令时,服务器将返回错误。如果您的数据库使用复制,则此条件仅适用于主数据库。请注意,仅读取现有数据的命令仍正常工作。
  • allkeys-lru:逐出最近最少使用(LRU)的键。
  • allkeys-lfu:逐出最不常用(LFU)的键。
  • allkeys-random:随机逐出键。
  • volatile-lru:逐出设置了 expire 字段为 true 的最近最少使用的键。
  • volatile-lfu:逐出设置了 expire 字段为 true 的最不常用的键。
  • volatile-random:仅当键设置了 expire 字段为 true 时,随机逐出键。
  • volatile-ttl:逐出设置了 expire 字段为 true 且剩余生存时间(TTL)值最短的键。

如果没有键设置了 expire 字段为 true,或者对于 volatile-ttl,如果没有键设置了生存时间值,则 volatile-xxx 策略的行为类似于 noeviction

您应该选择适合您的应用程序访问键方式的逐出策略。您可能能够提前预测访问模式,但您也可以在运行时使用 INFO 命令的信息来检查或改进您的策略选择(有关更多信息,请参阅下面的使用 INFO 命令)。

经验法则

  • 当您预计一小部分元素会被比其他元素更频繁地访问时,请使用 allkeys-lru。根据帕累托原则,这是一个非常常见的情况,因此如果您没有理由偏好其他策略,allkeys-lru 是一个不错的默认选项。
  • 当您预计所有键的访问频率大致相等时,请使用 allkeys-random。例如,当您的应用程序以重复循环读取数据项时。
  • 如果您的代码可以估计哪些键是逐出的良好候选者并为其分配较短的 TTL,请使用 volatile-ttl。另请注意,如果您充分利用键过期功能,则不太可能遇到缓存内存限制,因为键通常会在需要逐出之前过期。

volatile-lruvolatile-random 策略主要在您希望使用单个 Redis 实例进行缓存和存储一组持久性键时有用。但是,如果可能,在这种情况下您应该考虑运行两个独立的 Redis 实例。

另请注意,为键设置 expire 值会消耗内存,因此像 allkeys-lru 这样的策略更节省内存,因为它不需要 expire 值即可运行。

使用 INFO 命令

INFO 命令提供了几组数据,可用于检查缓存的性能。特别是,INFO stats 部分包含两个重要条目:keyspace_hits(键在缓存中成功找到的次数)和 keyspace_misses(请求了键但在缓存中未找到的次数)。下面的计算得出从缓存中满足的尝试访问百分比

keyspace_hits / (keyspace_hits + keyspace_misses) * 100

检查这是否与您对应用程序的预期大致相等(当然,百分比越高表示缓存性能越好)。

注意
EXISTS 命令报告键不存在时,这被视为键空间未命中。

如果命中率低于预期,则可能意味着您没有使用最佳的逐出策略。例如,如果您认为一小部分“热”数据(可以轻松放入缓存)应该占访问总量的 75% 左右,那么您可以合理地期望键空间命中率在 75% 左右。如果实际百分比较低,请检查 evicted_keys 的值(INFO stats 也返回该值)。高比例的逐出表明您选择的策略正在过于频繁地逐出错误的键(因此 allkeys-lru 在这里可能是一个不错的选择)。如果 evicted_keys 的值较低,并且您正在使用键过期功能,请检查 expired_keys 以查看有多少键已过期。如果此数字很高,则您使用的 TTL 可能太低,或者您选择了错误的键进行过期,这导致键在应该消失之前从缓存中消失。

INFO 返回的其他有用信息包括

  • used_memory_dataset:(memory 部分)用于缓存数据的内存量。如果此值大于 maxmemory,则差值表示超出 maxmemory 的量。
  • current_eviction_exceeded_time:(stats 部分)自缓存上次开始超出 maxmemory 以来的时间。
  • commandstats 部分:除其他外,此部分报告发送到服务器的每个命令被拒绝的次数。如果您正在使用 noeviction 或其中一个 volatile_xxx 策略,您可以使用此信息来查找哪些命令因 maxmemory 限制而被停止以及发生的频率。

近似 LRU 算法

Redis LRU 算法使用最近最少使用键的近似值,而不是精确计算它们。它随机抽取少量键样本,然后逐出自上次访问以来时间最长的键。

从 Redis 3.0 开始,该算法还会跟踪一个良好的逐出候选池。这提高了算法的性能,使其更接近真正的 LRU 算法。

您可以通过使用 maxmemory-samples 配置指令更改每次逐出前检查的样本数量来调整算法的性能

maxmemory-samples 5

Redis 不使用真正的 LRU 实现的原因是它会消耗更多内存。然而,对于使用 Redis 的应用程序来说,近似值几乎是等效的。此图比较了 Redis 使用的 LRU 近似值与真正的 LRU。

LRU comparison

生成以上图表的测试向 Redis 服务器填充了给定数量的键。这些键从第一个到最后一个被访问。使用 LRU 算法,第一个键是最佳的逐出候选者。之后又添加了 50% 的键,以强制逐出一半旧键。

您可以在图表中看到三种不同类型的点,形成三个不同的区域。

  • 浅灰色区域是被逐出的对象。
  • 灰色区域是未被逐出的对象。
  • 绿色区域是新添加的对象。

在理论的 LRU 实现中,我们期望在旧键中,前半部分将会过期。而 Redis LRU 算法则只会概率性地使较旧的键过期。

正如您所见,与 Redis 2.8 相比,Redis 3.0 在 5 个样本的情况下表现更好,但 Redis 2.8 仍然保留了大多数最新访问的对象。在 Redis 3.0 中使用 10 个样本大小时,近似值非常接近 Redis 3.0 的理论性能。

请注意,LRU 只是一个模型,用于预测给定键将来被访问的可能性。此外,如果您的数据访问模式非常接近幂律分布,则大多数访问将集中在 LRU 近似算法可以很好处理的那部分键中。

在模拟中,我们发现使用幂律访问模式时,真正的 LRU 和 Redis 近似值之间的差异很小甚至不存在。

但是,您可以将样本大小增加到 10,这会带来一些额外的 CPU 使用,以更接近真正的 LRU,并检查这是否会影响您的缓存未命中率。

通过使用 CONFIG SET maxmemory-samples <count> 命令在生产环境中尝试不同的样本大小值非常简单。

LFU 逐出

从 Redis 4.0 开始,可以使用最不常用(Least Frequently Used)逐出模式。在某些情况下,此模式可能效果更好(提供更好的命中/未命中率)。在 LFU 模式下,Redis 将尝试跟踪项目的访问频率,以便很少使用的项目被逐出。这意味着经常使用的键有更高的机会保留在内存中。

要配置 LFU 模式,可以使用以下策略

  • volatile-lfu 在设置了过期的键中,使用近似 LFU 进行逐出。
  • allkeys-lfu 使用近似 LFU 逐出任何键。

LFU 与 LRU 一样采用近似方法:它使用一种概率计数器,称为Morris 计数器,每个对象仅使用少量位来估计对象访问频率,并结合衰减周期,使计数器随时间减少。在某个时刻,我们不再希望将键视为经常访问的,即使它们过去是,这样算法才能适应访问模式的变化。

该信息与 LRU 的处理方式类似地进行采样(如本文档上一节所述),以选择一个逐出候选者。

然而,与 LRU 不同,LFU 具有某些可调参数:例如,如果一个频繁使用的项目不再被访问,其排名应该下降多快?还可以调整 Morris 计数器的范围,以便更好地使算法适应特定的用例。

默认情况下,Redis 配置为

  • 在约一百万次请求时使计数器饱和。
  • 每隔一分钟衰减一次计数器。

这些应该是合理的值,并经过实验测试,但用户可能希望调整这些配置设置以选择最佳值。

有关如何调整这些参数的说明可以在源代码分发包中的示例 redis.conf 文件中找到。简要来说,它们是

lfu-log-factor 10
lfu-decay-time 1

衰减时间是显而易见的一个,它是计数器应该衰减的分钟数,当采样时发现比该值更旧时。特殊值 0 表示:我们永远不会衰减计数器。

计数器对数因子改变了使频率计数器饱和所需的命中次数,该计数器的范围仅在 0-255 之间。因子越高,达到最大值所需的访问次数越多。因子越低,计数器对于低访问量的分辨率越好,如下表所示

+--------+------------+------------+------------+------------+------------+
| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
+--------+------------+------------+------------+------------+------------+
| 0      | 104        | 255        | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 1      | 18         | 49         | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 10     | 10         | 18         | 142        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 100    | 8          | 11         | 49         | 143        | 255        |
+--------+------------+------------+------------+------------+------------+

所以基本上,该因子是在更好地区分低访问量项目与区分高访问量项目之间进行权衡。更多信息可在示例 redis.conf 文件中找到。

评价此页面
回到顶部 ↑