文档:键逐出

当 Valkey 用作缓存时,通常很方便让它在您添加新数据时自动逐出旧数据。这种行为在开发者社区中广为人知,因为它是流行的 memcached 系统的默认行为。

本页面涵盖了 Valkey `maxmemory` 指令的更一般主题,该指令用于将内存使用限制在固定量。它还广泛涵盖了 Valkey 使用的 LRU 逐出算法,该算法实际上是精确 LRU 的近似。

`Maxmemory` 配置指令

`maxmemory` 配置指令将 Valkey 配置为对数据集使用指定量的内存。您可以使用 `valkey.conf` 文件设置该配置指令,或稍后在运行时使用 `CONFIG SET` 命令设置。

例如,要配置 100 兆字节的内存限制,您可以在 `valkey.conf` 文件中使用以下指令

maxmemory 100mb

将 `maxmemory` 设置为零表示没有内存限制。这是 64 位系统的默认行为,而 32 位系统使用 3GB 的隐式内存限制。当达到指定的内存量时,**逐出策略**的配置方式决定了默认行为。Valkey 可以针对可能导致更多内存使用的命令返回错误,或者在每次添加新数据时逐出一些旧数据以回到指定的限制。

使用复制时 `maxmemory` 的注意事项

如果您已设置复制,Valkey 需要一些 RAM 作为缓冲区来向副本和 AOF 文件发送数据。此内存不包含在与 `maxmemory` 比较以触发逐出的已用内存计数中。

原因是键逐出过程本身会生成一些需要添加到复制和 AOF 缓冲区的更改。如果这些缓冲区被计入键逐出,这将导致一个循环:释放的内存会立即被这些更新占用,从而导致更多的键被重复删除,直到数据库为空。

对于配置了复制的 Valkey,建议将 `maxmemory` 值设置得低于没有复制的单实例。这样可以确保有足够的内存用于 AOF 和复制缓冲区以及其他进程。

您可以使用 INFO memory 命令输出中的 `mem_not_counted_for_evict` 值来估算复制和 AOF 缓冲区占用的内存量。

逐出策略

当达到 `maxmemory` 限制时 Valkey 遵循的确切行为是使用 `maxmemory-policy` 配置指令配置的。

以下策略可用

  • **noeviction**:达到内存限制时,新值不会被保存。当数据库使用复制时,这适用于主数据库
  • **allkeys-lru**:保留最近最少使用的键;移除最近最少使用的 (LRU) 键
  • **allkeys-lfu**:保留最常用键;移除最不常用 (LFU) 键
  • **volatile-lru**:移除设置了存活时间 (TTL) 的最近最少使用的键。
  • **volatile-lfu**:移除设置了 TTL 的最不常用键。
  • **allkeys-random**:随机移除键以腾出空间用于新添加的数据。
  • **volatile-random**:随机移除设置了 TTL 的键。
  • **volatile-ttl**:移除设置了 TTL 的键,优先移除剩余存活时间最短的键。

如果不存在符合先决条件的键可供逐出,策略 **volatile-lru**、**volatile-lfu**、**volatile-random** 和 **volatile-ttl** 的行为将与 **noeviction** 类似。

**LRU**、**LFU** 和 **volatile-ttl** 采用近似随机算法实现。

根据您的应用程序的访问模式选择正确的逐出策略非常重要,但是您可以在应用程序运行时动态重新配置策略,并使用 Valkey `INFO` 输出监控缓存未命中和命中的数量来调整您的设置。

一般经验法则

  • 当您预计请求的流行度呈现幂律分布时,请使用 **allkeys-lru** 策略。也就是说,您预计一小部分元素将被访问的频率远高于其余部分。**如果您不确定,这是一个不错的选择**。

  • 如果您有循环访问(其中所有键都被连续扫描),或者当您预计分布是均匀的时,请使用 **allkeys-random**。

  • 如果您想在创建缓存对象时通过使用不同的 TTL 值来向 Valkey 提供哪些是好的过期候选对象的提示,请使用 **volatile-ttl**。

**volatile-lru** 和 **volatile-random** 策略主要在您希望使用单个实例进行缓存并拥有一组持久键时有用。然而,解决此类问题通常更好的方法是运行两个 Valkey 实例。

值得注意的是,为键设置 TTL 值会消耗内存,因此使用 **allkeys-lru** 这样的策略更节省内存,因为在内存压力下,键无需 TTL 配置即可被逐出。

逐出过程的工作原理

重要的是要理解逐出过程是这样工作的

  • 客户端运行新命令,导致更多数据被添加。
  • Valkey 检查内存使用情况,如果超过 `maxmemory` 限制,它会根据策略逐出键。
  • 执行新命令,依此类推。

因此,我们通过超出内存限制,然后通过逐出键来回到限制之下,从而不断地跨越内存限制的边界。

如果一个命令在一段时间内导致大量内存被使用(例如将一个大的集合交集存储到一个新键中),则内存限制可能会被明显超出。

监控逐出

要监控 Valkey 何时开始逐出数据,请使用 `INFO MEMORY` 命令。以下字段提供有关内存使用情况和触发键逐出的条件的信息

  • `used_memory`:服务器为存储数据分配的总字节数。它是 `used_memory_overhead` 和 `used_memory_dataset` 输出的总和。
  • `mem_not_counted_for_evict`:未计入逐出的内存量。这包括复制缓冲区和 AOF 缓冲区。

因此,触发逐出的内存使用量计算如下

used_memory - mem_not_counted_for_evict > maxmemory

让我们看看这在实践中是如何运作的。

考虑以下 INFO memory 输出

# Memory
used_memory:12498952
...
maxmemory:10737418240
...
mem_not_counted_for_evict:12336
...

在此示例中,Valkey 不会开始数据逐出,因为实际内存使用量为 `12498952 - 12336 = 12486616`,这远低于 `maxmemory`。

以下示例显示我们正接近逐出

# Memory
used_memory:12498952
...
maxmemory:12500000
...
mem_not_counted_for_evict:12336

一旦发生逐出,可以通过 `INFO STATS` 指标获取额外信息。`total_eviction_exceeded_time` 指标显示 `used_memory` 超出 `maxmemory` 的总毫秒数。

近似 LRU 算法

Valkey LRU 算法并非精确实现。这意味着 Valkey 无法选择逐出的*最佳候选者*,即过去访问时间最远的键。相反,它会尝试通过采样少量键来运行 LRU 算法的近似版本,并在采样键中逐出最佳(访问时间最旧)的那个,同时还管理一个好的逐出候选池。这种算法比精确 LRU 算法消耗更少的内存。

Valkey LRU 算法的重要之处在于,您**能够通过更改每次逐出检查的样本数量来调整**算法的精度。此参数由以下配置指令控制

maxmemory-samples 5

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

LRU comparison

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

您可以在图中看到三种点,形成三个不同的条带。

  • 浅灰色条带是已被逐出的对象。
  • 灰色条带是未被逐出的对象。
  • 绿色条带是已添加的对象。

在理论 LRU 实现中,我们期望在旧键中,前一半将被逐出。而 Valkey LRU 算法则只会*概率性地*逐出较旧的键。

如您所见,Redis OSS 3.0 在 5 个样本下表现良好。使用 10 个样本大小,近似效果非常接近精确的 LRU 实现。(自执行此测试以来,LRU 算法没有显著改变,因此 Valkey 在这方面的性能类似。)

请注意,LRU 只是一个模型,用于预测给定键在未来被访问的可能性。此外,如果您的数据访问模式与幂律密切相关;大多数访问都将在 LRU 近似算法能够很好处理的键集中。

在模拟中我们发现,使用幂律访问模式时,真正的 LRU 和 Valkey 近似之间的差异微乎其微或不存在。

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

在生产环境中,通过使用 `CONFIG SET maxmemory-samples ` 命令来试验不同样本大小的值,非常简单。

LFU 模式

最不常用 (Least Frequently Used) 逐出模式可作为 LRU 的替代方案。在某些情况下,此模式可能效果更好(提供更好的命中/未命中率)。在 LFU 模式下,Valkey 将尝试跟踪项目的访问频率,因此很少使用的项目会被逐出。这意味着经常使用的键有更大的机会保留在内存中。

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

  • `volatile-lfu` 在设置了存活时间 (TTL) 的键中,使用近似 LFU 进行逐出。
  • `allkeys-lfu` 使用近似 LFU 逐出任何键。

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

该信息与 LRU 的情况类似地进行采样(如本文档前一节所述),以选择逐出候选者。

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

Valkey 默认配置为

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

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

有关如何调整这些参数的说明可以在源代码分发中的示例 `valkey.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        |
+--------+------------+------------+------------+------------+------------+

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