“…正如我们所知,存在已知已知;有些事情我们知道我们知道。我们也知道存在已知未知;也就是说,我们知道有些事情我们不知道。但还存在未知未知——那些我们不知道我们不知道的事情。”
美国国防部长唐纳德·拉姆斯菲尔德,2002 年
如果有一个最常被重复的 Redis 问题,那就是这个问题。它也是最难准确回答的问题之一,因为答案取决于许多因素。在这篇文章中(以及我可能接下来发表的几篇文章中,因为我确实会在嘟嘟囔囔和喃喃自语时变得冗长),我将尝试绘制 Redis 的内存消耗图。虽然我不能保证对所有问题都给出最终答案,但我希望它能帮助到
旁注:您可以通过订阅 RSS Feed 来跟踪我在本系列中的进度。或者,注册 Redis Watch,并每周接收更新至您的邮箱。最后,如果您愿意,可以 关注我们 的 Twitter,获取实时信息。
因此,Redis 是一块软件,因此它需要 RAM 来运行。但是 Redis 不仅仅是任何软件,它是一个内存数据库,这意味着 Redis 管理的每条数据都也保存在 RAM 中。我们把 Redis 运行所需的 RAM 称为操作 RAM,并将用于数据存储的 RAM 称为用户数据 RAM。
我们将从快速了解 Redis 的操作 RAM 开始我们的旅程。
它用于 Redis 执行的许多目的和任务,我们可以将这部分 RAM 视为 Redis 用于除用户数据之外的所有内存(我可能以后会详细介绍这部分内容)。Redis 的内存占用受许多部署因素的影响,包括
但是,我们可以通过检查处于静止状态的典型服务器上的 Redis,轻松地为 Redis 的操作 RAM 需求设定一个基线。例如,未负载非洲燕子 v3.0.0 实例在虚拟化的 Ubuntu 14 64 位服务器上的内存占用为 7995392 字节(约 7.6 MB)。您可以使用 ps 的 RSS 列或 Redis 的 INFO 命令,从命令行快速确定 Redis 实例分配的总 RAM。
foo@bar:~$ uname -a Linux bar 3.13.0-49-generic #81-Ubuntu SMP Tue Mar 24 19:29:48 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux foo@bar:~$ ps aux | grep redis-server foo 20139 0.0 0.1 42304 7808 pts/1 Sl+ 19:18 0:00 ./redis-server *:6379 foo 20143 0.0 0.0 15940 944 pts/9 S+ 19:18 0:00 grep --color=auto redis-server foo@bar:~$ redis-cli INFO memory | grep used_memory_rss used_memory_rss:7995392
旁注:上述两种方法以及其他方法的结果可能并不总是相同。另外请注意,used_memory_rss 与 Redis 的 used_memory 相差很大,我们将在后面看到。
由于这是一个新初始化的 Redis 实例,我们可以假设这个数字是我们的操作基线的公平表示。Redis 的操作 RAM 可以增长,甚至显著增长,但现在我们不用担心这个问题。
Redis 很美,但你不会仅仅因为它的漂亮外表而留着它,对吧?不,你让 Redis 管理你的数据,因为你需要当今地球上最快的 NoSQL 数据库。你让它带着你的椰子,并依靠它每秒拍打 43 次(非常接近!)翅膀。那么用户数据占用多少 RAM?这取决于椰子 🙂
Redis 的无模式模式 * 基于键值模型。Redis 管理的每个用户数据主要是一个 KV 对。不难理解,您的键和值越长/越大,Redis 存储它所需的 RAM 就越多。但是 Redis 确实有一些巧妙的技巧旨在保持数据的紧凑和组织。
* 没有无模式数据库——最多只能有一个隐式模式。
让我们直接从一个关于最简单的 Redis 数据类型的例子开始——考虑以下内容
127.0.0.1:6379> SET swallow coconut OK
我们刚刚使用了多少 RAM?由于所有数据都按键组织,所以键的名称是我们将要检查的第一个元素。我们知道 Redis 键名是二进制安全的字符串,最多可以长达 512 MB。酷。Redis 中的字符串值也是二进制安全的,最多可以长达 0.5 GB,因此在我们上面的例子中,我们可以假设“swallow”为 7 个字节,“coconut”为另外 7 个字节……我们错了,至少部分错了。这里,让我告诉你
127.0.0.1:6379> STRLEN swallow (integer) 7 127.0.0.1:6379> DEBUG SDSLEN swallow key_sds_len:7, key_sds_avail:0, val_sds_len:7, val_sds_avail:0
如上所示,使用 STRLEN,Redis 坚持认为在“swallow”键下存储的值(“coconut”)的长度为 7 个字节。更重要的是,“secret” DEBUG SDSLEN 命令也做出了相同的声明,但两者都没有考虑数据的开销,并且每个 Redis 数据结构都有自己的负担。这意味着除了实际的字符串(“swallow”和“coconut”)之外,Redis 还需要一些 RAM 来管理它们。
由于 Redis 中的每个键值元组都使用额外的 RAM 来进行内部记账,并且由于这部分 RAM 的大小取决于数据结构和数据本身,因此我认为这部分开销,虽然是元数据,但也是用户数据的一部分。换句话说,如果对于每个 X 字节的字符串,Redis 需要 X + Y 字节,那么 Y 绝对是一个 KU,我想要把它变成一个 KK。
Redis 中的字符串主要由 Salvatore Sanfilippo @antirez 的子项目之一实现,称为 sds(或 C 语言的简单动态字符串库)。虽然 sds 字符串在 Redis 内部带来了很多功能和易用性,但它们确实带有一些开销,因为 sds 字符串由以下部分组成
+--------+-------------------------------+-----------+ | Header | Binary safe C alike string... | Null term | +--------+-------------------------------+-----------+ | `-> Pointer returned to the user.
(图表由 antirez 提供,https://github.com/antirez/sds/blob/master/README.md)
sds 字符串头的尺寸 (目前) 为 8 字节,空字符为另外 1 个字节,这使得每个字符串的总开销为 9 个字节。7 个字节的“swallow”突然需要超过两倍的内存——16 个字节的 RAM——才能存储在 Redis 中!
旁注:实际上它使用了更多内存。每个键的引用也存储在 Redis 的键空间哈希表中,这反过来需要更多 RAM……此外还有 Redis 用于促进一些内容(如 LRU)的 robj “对象”……但现在让我们将键管理 RAM 开销视为 KU,并将其扔到操作 RAM 的那一侧 🙂
回到我们的燕子和椰子——因此,现在我们知道 Redis 将使用 36 个字节来存储上述例子中构成键和值的两个字符串,但这都是吗?让我们更仔细地看看我们的椰子
127.0.0.1:6379> OBJECT ENCODING swallow "embstr"
情节变得越来越复杂(或者可能是椰子长了毛发?)。这个神秘的回应需要解释,但所有椰子都是以相同的方式编码的吗?让我们尝试携带几种不同形状的字符串椰子,然后看看
127.0.0.1:6379> SET swallow:0 "0" OK 127.0.0.1:6379> SET swallow:1 "An oversized and thickly-haired coconut" OK 127.0.0.1:6379> SET swallow:2 "Ok, this is the mother of all coconuts - it is something that would make Donkey Kong run back to his mama in tears" OK 127.0.0.1:6379> OBJECT ENCODING swallow:0 "int" 127.0.0.1:6379> OBJECT ENCODING swallow:1 "embstr" 127.0.0.1:6379> OBJECT ENCODING swallow:2 "raw"
我们的每个椰子都不同,因此 Redis 使用不同的编码。 “int”编码用于有效地存储介于 LONG_MIN 和 LONG_MAX(在您的环境的 limits.h 中定义)之间的整数值,并且还利用 shared.integers 结构来避免重复数据。因此,这些占用更少的空间。另一方面,长度超过 39 个字节的字符串将存储为“raw”,而较短的字符串使用“embstr”编码(神奇数字由 redis.h 中的 REDIS_ENCODING_EMBSTR_SIZE_LIMIT 定义)。
其他椰子结构呢?字符串是 Redis 提供的最简单的数据结构,它们在内部用于创建其他更高级的结构。哈希由一堆字符串(字段和值)组成,并使用字典数据结构添加,其中每个条目都是一个链表……但它们可以完全编码为一个 ziplist。说到列表,我们有链表和 ziplist(甚至可能还有 Matt Stancliff @mattsta 的 quicklist 即将出现),它们也被集合和有序集合使用……
我可以继续,无限地,详细介绍 Redis 中的精细数据编码以及它们如何影响内存消耗。但是,我担心这很快就会让我们所有人都感到厌烦。或者,您可以git checkout 源代码 并开始阅读——我知道一些人已经这么做了,但这需要一定的编程技能。
这仍然留下了最大的 KU——Redis 需要多少 RAM?——以及它的弟弟 KU——椰子需要多少 RAM?——基本上没有得到回答。但有志者事竟成。随时通过常用渠道与我互动——我很乐意提供帮助 🙂
旁注:待续。