内存优化
此页面正在施工中。目前,它只是一个列表,列出了当您遇到内存问题时应该检查的事项。
#
小型聚合数据类型的特殊编码许多数据类型都经过优化,在特定大小以下使用更少的空间。哈希、列表、仅由整数组成的集合以及有序集合,当元素数量小于给定值且不超过最大元素大小时,会以一种非常节省内存的方式进行编码,可以节省高达10倍的内存(平均可节省5倍内存)。
从用户和API的角度来看,这是完全透明的。由于这是CPU与内存之间的权衡,因此可以使用以下 KeyDB.conf 指令来调整特殊编码类型的最大元素数和最大元素大小。
如果一个特殊编码的值超过了配置的最大大小,KeyDB 会自动将其转换为正常编码。对于小值,此操作非常快,但如果您更改设置以将特殊编码用于更大的聚合类型,建议运行一些基准测试来检查转换时间。
#
使用32位实例使用32位目标编译的 KeyDB 每个键占用的内存要少得多,因为指针更小,但这样的实例最大内存使用量将被限制在4GB。要将 KeyDB 编译为32位二进制文件,请使用 make 32bit。RDB 和 AOF 文件在32位和64位实例之间(当然也包括大端和小端字节序之间)是兼容的,因此您可以毫无问题地从32位切换到64位,反之亦然。
#
位和字节级操作位和字节级操作:GETRANGE
、SETRANGE
、GETBIT
和 SETBIT
。使用这些命令,您可以将 KeyDB 的字符串类型视为一个随机访问数组。例如,如果您有一个应用程序,其中用户由唯一的递增整数标识,您可以使用位图来保存用户在邮件列表中的订阅信息,设置位表示已订阅,清除位表示未订阅,或者反之。对于1亿用户,这些数据在 KeyDB 实例中仅占用12MB的RAM。您也可以使用 GETRANGE
和 SETRANGE
为每个用户存储一个字节的信息。这只是一个例子,但实际上可以用这些新的原语在非常小的空间内解决许多问题。
#
尽可能使用哈希小哈希以非常小的空间进行编码,因此您应尽可能尝试使用哈希来表示您的数据。例如,如果您在Web应用程序中有代表用户的对象,不要为姓名、姓氏、电子邮件、密码使用不同的键,而是使用一个包含所有必需字段的哈希。
如果您想了解更多相关信息,请阅读下一节。
#
使用哈希在 KeyDB 之上抽象出一个非常节省内存的纯键值存储基本上,可以使用 KeyDB 来构建一个纯键值存储模型,其中值只能是字符串,这不仅比 KeyDB 的纯键更节省内存,而且比 memcached 也节省得多。
让我们从一些事实开始:少数几个键比包含少量字段的单个哈希使用更多的内存。这怎么可能呢?我们用了一个技巧。理论上,为了保证我们以常数时间(在“大O表示法”中也称为O(1))执行查找,需要使用一个在平均情况下具有常数时间复杂度的数据结构,比如哈希表。
但很多时候,哈希只包含几个字段。当哈希很小时,我们可以用一个O(N)的数据结构来编码它,比如一个带有长度前缀键值对的线性数组。由于我们只在N很小时这样做,HGET和HSET命令的摊销时间仍然是O(1):一旦哈希包含的元素数量增长过大(您可以在keydb.conf中配置限制),它将被转换为真正的哈希表。
这不仅在时间复杂度方面表现良好,而且在常数时间方面也很好,因为键值对的线性数组恰好与CPU缓存配合得很好(它比哈希表有更好的缓存局部性)。
然而,由于哈希字段和值并不(总是)表示为功能齐全的 KeyDB 对象,哈希字段不能像真正的键那样有关联的生存时间(过期),并且只能包含字符串。但我们对此没问题,这本就是哈希数据类型API设计时的初衷(我们更信任简单性而不是功能,所以不允许嵌套数据结构,也不允许单个字段过期)。
所以哈希是内存高效的。这在使用哈希表示对象或当存在一组相关字段时建模其他问题时非常有用。但是,如果我们有一个纯键值业务呢?
假设我们想用 KeyDB 作为许多小对象的缓存,这些对象可以是 JSON 编码的对象、小的 HTML 片段、简单的键 -> 布尔值等等。基本上,任何东西都是一个键和值都很小的 字符串 -> 字符串 映射。
现在,我们假设要缓存的对象是编号的,比如
- object:102393
- object:1234
- object:5
我们可以这样做。每当需要执行 SET 操作来设置新值时,我们实际上将键分成两部分,一部分用作键,另一部分用作哈希的字段名。例如,名为 "object:1234" 的对象实际上被分割为
- 一个名为 object:12 的键
- 一个名为 34 的字段
所以我们使用除了最后两个字符之外的所有字符作为键,最后两个字符作为哈希字段名。要设置我们的键,我们使用以下命令
如您所见,每个哈希最终将包含100个字段,这是CPU和节省内存之间的最佳折衷。
还有一点非常重要需要注意,使用这种模式,无论我们缓存了多少对象,每个哈希都会有大约100个字段。这是因为我们的对象总是以数字结尾,而不是随机字符串。在某种程度上,末尾的数字可以被视为一种隐式的预分片形式。
#
内存分配为了存储用户键,KeyDB 最多会分配 maxmemory
设置所允许的内存量(但可能会有一些小的额外分配)。
确切的值可以在配置文件中设置,也可以稍后通过 CONFIG SET
设置(更多信息请参阅将内存用作LRU缓存)。关于 KeyDB 如何管理内存,有几点需要注意
- 当键被删除时,KeyDB 不会总是释放(返回)内存给操作系统。这并非 KeyDB 特有,而是大多数 malloc() 实现的工作方式。例如,如果您向一个实例填充了5GB的数据,然后删除了相当于2GB的数据,驻留集大小(RSS,即进程消耗的内存页数)可能仍然在5GB左右,即使 KeyDB 会声称用户内存约为3GB。这是因为底层的分配器无法轻易释放内存。例如,通常大部分被删除的键与仍然存在的其他键分配在相同的内存页中。
- 前一点意味着您需要根据您的峰值内存使用量来规划内存。如果您的工作负载偶尔需要10GB,即使大多数时候5GB就足够了,您也需要为10GB做准备。
- 然而,内存分配器是智能的,能够重用空闲的内存块,所以在您释放了5GB数据集中的2GB后,当您再次开始添加更多的键时,您会看到RSS(驻留集大小)保持稳定而不会增长,直到您添加了多达2GB的额外键。分配器基本上是在尝试重用之前(逻辑上)释放的2GB内存。
- 由于所有这些原因,当您的峰值内存使用量远大于当前使用的内存时,碎片率是不可靠的。碎片率的计算方法是实际使用的物理内存(RSS值)除以当前正在使用的内存量(KeyDB执行的所有分配的总和)。因为RSS反映了峰值内存,当由于大量键/值被释放而(虚拟)使用的内存很低,但RSS很高时,
RSS / mem_used
的比率会非常高。
如果未设置 maxmemory
,KeyDB 将根据需要持续分配内存,因此它可能会(逐渐)耗尽您所有的可用内存。因此,通常建议配置一些限制。您可能还想将 maxmemory-policy
设置为 noeviction
(在某些旧版本的 KeyDB 中,这不是默认值)。
这使得 KeyDB 在达到限制时会对写命令返回内存不足错误——这反过来可能会导致应用程序出错,但不会因为内存耗尽而使整台机器宕机。