将 KeyDB 用作 LRU 缓存
当 KeyDB 用作缓存时,通常很方便地让它在您添加新数据时自动逐出旧数据。这种行为在开发者社区中非常有名,因为它是流行的 memcached 系统的默认行为。
LRU 实际上只是支持的逐出方法之一。本页涵盖了 KeyDB maxmemory
指令这一更广泛的主题,该指令用于将内存使用限制在固定数量,并且还深入介绍了 KeyDB 使用的 LRU 算法,该算法实际上是精确 LRU 的近似值。
一种新的 LFU(最不常用)逐出策略也已可用,并将在本文档的单独部分中介绍。
#
Maxmemory 配置指令maxmemory
配置指令用于配置 KeyDB 为数据集使用指定的内存量。可以使用 keydb.conf
文件设置该配置指令,或者稍后在运行时使用 CONFIG SET
命令进行设置。
例如,要配置 100 兆字节的内存限制,可以在 keydb.conf
文件中使用以下指令。
将 maxmemory
设置为零意味着没有内存限制。这是 64 位系统的默认行为,而 32 位系统使用 3GB 的隐式内存限制。
当达到指定的内存量时,可以选择不同的行为,称为**策略**。KeyDB 可以为可能导致使用更多内存的命令返回错误,或者可以在每次添加新数据时逐出一些旧数据以恢复到指定的限制。
#
逐出策略当达到 maxmemory
限制时,KeyDB 所遵循的确切行为是使用 maxmemory-policy
配置指令进行配置的。
以下策略可用
- noeviction:当达到内存限制且客户端尝试执行可能导致使用更多内存的命令时返回错误(大多数写命令,但
DEL
和一些其他例外情况除外)。 - allkeys-lru:通过尝试首先删除最近最少使用(LRU)的键来逐出键,以便为新添加的数据腾出空间。
- volatile-lru:通过尝试首先删除最近最少使用(LRU)的键来逐出键,但仅限于设置了**过期时间**的键,以便为新添加的数据腾出空间。
- allkeys-random:随机逐出键,以便为新添加的数据腾出空间。
- volatile-random:随机逐出键,以便为新添加的数据腾出空间,但仅逐出设置了**过期时间**的键。
- volatile-ttl:逐出设置了**过期时间**的键,并尝试首先逐出剩余生存时间(TTL)较短的键,以便为新添加的数据腾出空间。
如果没有符合先决条件的键可以逐出,**volatile-lru**、**volatile-random** 和 **volatile-ttl** 策略的行为与 **noeviction** 相同。
选择正确的逐出策略很重要,具体取决于应用程序的访问模式。但是,您可以在应用程序运行时动态重新配置策略,并使用 KeyDB 的 INFO
输出监控缓存未命中和命中的次数,以调整您的设置。
一般来说,经验法则是
- 当您预期请求的流行度呈幂律分布时,请使用 **allkeys-lru** 策略,也就是说,您预期一部分元素的访问频率远高于其余元素。**如果您不确定,这是一个不错的选择**。
- 如果您有循环访问,其中所有键都被连续扫描,或者当您预期分布是均匀的(所有元素被访问的概率相同)时,请使用 **allkeys-random**。
- 如果您希望通过在创建缓存对象时使用不同的 TTL 值来向 KeyDB 提供关于哪些是良好过期候选项的提示,请使用 **volatile-ttl**。
当您想使用单个实例进行缓存并拥有一组持久键时,**volatile-lru** 和 **volatile-random** 策略主要有用。然而,通常更好的做法是运行两个 KeyDB 实例来解决此类问题。
还值得注意的是,为键设置过期会消耗内存,因此使用像 **allkeys-lru** 这样的策略更节省内存,因为在内存压力下不需要为键设置过期即可被逐出。
#
逐出过程如何工作理解逐出过程的工作方式很重要
- 客户端运行一个新命令,导致添加更多数据。
- KeyDB 检查内存使用情况,如果它大于
maxmemory
限制,它会根据策略逐出键。 - 执行一个新命令,依此类推。
因此,我们不断地跨越内存限制的边界,先是超过它,然后通过逐出键来回到限制之下。
如果一个命令导致使用了大量内存(例如将一个大的集合交集存储到一个新键中),在一段时间内内存限制可能会被明显超过。
#
近似 LRU 算法最初的 LRU 算法(来自 Redis <3.0)并不是一个精确的实现。这意味着它曾经无法选择*最佳的*逐出候选者,即过去访问次数最多的那个。相反,它会尝试运行一个 LRU 算法的近似值,通过对少量键进行采样,并逐出在采样键中最好的那个(访问时间最旧的那个)。
该算法后来在 Redis 3.0 中得到改进,并随后传播到 KeyDB,它还考虑了一个良好的逐出候选池。这提高了算法的性能,使其能够更接近真实 LRU 算法的行为。
关于 KeyDB LRU 算法的重要之处在于,您**能够通过更改**每次逐出时要检查的样本数量来调整算法的精度。此参数由以下配置指令控制
KeyDB 不使用真正的 LRU 实现的原因是它会消耗更多内存。然而,对于使用 KeyDB 的应用程序来说,这种近似几乎是等效的。以下是 KeyDB 使用的 LRU 近似与真实 LRU 的图形比较。
生成上述图表的测试用给定数量的键填充了一个 KeyDB 服务器。这些键从第一个到最后一个被访问,因此第一个键是使用 LRU 算法的最佳逐出候选项。随后又添加了 50% 的键,以强制逐出一半的旧键。
您可以在图表中看到三种点,形成三个不同的带。
- 浅灰色带是被逐出的对象。
- 灰色带是未被逐出的对象。
- 绿色带是新添加的对象。
在理论上的 LRU 实现中,我们期望在旧键中,前一半将被过期。而 KeyDB LRU 算法则只会*概率性地*过期较旧的键。
正如您所看到的,当前的 KeyDB(最初来自 Redis 3.0)使用 5 个样本比 Redis 2.8 做得更好,但是大多数在最近被访问的对象仍然被 Redis 2.8 保留。在 KeyDB(Redis 3.0)中使用 10 的样本大小,其近似值非常接近 KeyDB(Redis 3.0)的理论性能。
请注意,LRU 只是一个预测给定键将来被访问可能性的模型。此外,如果您的数据访问模式非常接近幂律分布,那么大部分访问都将在 LRU 近似算法能够很好处理的键集中。
在模拟中,我们发现使用幂律访问模式时,真实 LRU 和 KeyDB 近似之间的差异很小或不存在。
但是,您可以将样本大小提高到 10,代价是增加一些额外的 CPU 使用率,以非常接近地近似真实 LRU,并检查这是否对您的缓存未命中率产生影响。
在生产环境中通过使用 CONFIG SET maxmemory-samples <count>
命令来试验不同的样本大小值是非常简单的。
#
新的 LFU 模式一种新的最不常用(LFU)逐出模式可用。在某些情况下,此模式可能会工作得更好(提供更好的命中/未命中率),因为使用 LFU,KeyDB 将尝试跟踪项目的访问频率,从而使很少使用的项目被逐出,而经常使用的项目有更高的机会保留在内存中。
如果您考虑 LRU,一个最近被访问但实际上几乎从未被请求的项目将不会过期,因此风险是逐出一个将来有更高机会被请求的键。LFU 没有这个问题,并且通常应能更好地适应不同的访问模式。
要配置 LFU 模式,有以下策略可用
volatile-lfu
在设置了过期时间的键中,使用近似 LFU 进行逐出。allkeys-lfu
使用近似 LFU 逐出任何键。
LFU 的近似方式与 LRU 类似:它使用一个概率计数器,称为 Morris 计数器,仅用每个对象几个比特位来估计对象的访问频率,并结合一个衰减期,以便计数器随时间减少:在某个时候,我们不再希望将键视为频繁访问,即使它们过去是这样,这样算法就可以适应访问模式的变化。
这些信息以类似于 LRU 的方式进行采样(如本文档前一节所述),以选择一个逐出候选者。
然而,与 LRU 不同,LFU 具有某些可调参数:例如,如果一个频繁访问的项目不再被访问,它应该以多快的速度降低排名?还可以调整 Morris 计数器的范围,以更好地使算法适应特定的用例。
默认情况下,KeyDB 配置为
- 在大约一百万次请求时使计数器饱和。
- 每分钟衰减一次计数器。
这些应该是合理的值,并经过实验测试,但用户可能希望调整这些配置设置以选择最佳值。
有关如何调整这些参数的说明可以在源分发中的示例 keydb.conf
文件中找到,但简而言之,它们是
衰减时间是显而易见的,它是一个计数器应该被衰减的分钟数,当被采样并发现其年龄大于该值时。一个特殊的值 `0` 意味着:每次扫描时总是衰减计数器,这很少有用。
计数器的*对数因子*改变了需要多少次命中才能使频率计数器饱和,该计数器的范围仅为 0-255。因子越高,达到最大值所需的访问次数就越多。因子越低,对于低访问次数的计数器分辨率就越好,如下表所示
所以基本上,该因子是在更好地区分低访问量项目与区分高访问量项目之间的一个权衡。更多信息可在示例 `keydb.conf` 文件自述注释中找到。
由于 LFU 是一个新功能,我们非常感谢您就其在您的用例中与 LRU 相比的性能提供任何反馈。