dot Redis 8 来了——而且是开源的

了解更多

Redis RAM 影响 – 第一部分

“……正如我们所知,存在已知的已知;有些事情我们知道我们知道。我们也知道存在已知的未知;也就是说,我们知道有一些事情我们不知道。但也存在未知的未知——我们不知道我们不知道的事情。”

美国国防部长唐纳德·拉姆斯菲尔德,2002 年

Redis 需要多少 RAM?

如果说有一个被问及最多的 Redis 问题,那就是这个问题。这也是最难准确回答的问题之一,因为答案取决于非常多的因素。在这篇文章(以及我倾向于漫谈时会唠叨的一些后续文章)中,我将尝试绘制 Redis 的 RAM 消耗图。虽然我不能保证最终能回答所有问题,但我希望它能帮助:

  • 重复我们对已知已知 (KKs) 的了解
  • 尽可能将已知的未知 (KUs) 转化为 KKs,或者至少转化为已知的、大致理解的未知 (KRUUs)
  • 希望偶然发现一些未知的未知 (UUs),并将它们转化为 KUs、KRUUs 甚至 KKs!

旁注: 您可以通过订阅 RSS 源来跟踪本系列的进度。或者,注册 Redis Watch,每周将更新发送到您的收件箱。最后,但仅当您准备好时,才可以在 Twitter 上 关注我们 以获取实时操作。

因此,Redis 是一段软件,因此它需要 RAM 才能运行。但 Redis 不仅仅是任何软件,它是一个内存数据库,这意味着 Redis 管理的每条数据也保存在 RAM 中。让我们将 Redis 运行所需的 RAM 称为操作 RAM,将用于数据存储的 RAM 称为用户数据 RAM

一只未载货的燕子的空速是多少?

我们将从快速浏览 Redis 的操作 RAM 开始我们的旅程。

它用于 Redis 执行的许多目的和任务,一种考虑该块 RAM 的方法是将其视为 Redis 用于非用户数据的所有内存(我稍后可能会涉足)。Redis 的 RAM 占用空间受多种部署因素影响,包括:

  • 服务器处理器的架构
  • 操作系统
  • Redis 的版本和配置
  • 可能还有很多 KKs、KUs 和 UUs

但是,我们可以通过检查典型服务器上处于静止状态的 Redis 来轻松设置 Redis 操作 RAM 要求的基线。例如,虚拟化 Ubuntu 14 64 位服务器上 未载货的非洲燕子 v3.0.0 实例的内存占用空间为 7995392 字节(或大约 7.6MB)。您可以使用 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 数据库。您让它携带您的椰子,并依靠它每秒钟拍打翅膀四十三次(太接近了!)。那么用户数据占用多少 RAM?这取决于椰子 🙂

Redis 的无模式 schema* 基于键值模型。Redis 管理的每个用户数据主要是一个 KV 对。不难理解,您的键和值越长/越大,Redis 存储它所需的 RAM 就越多。但 Redis 确实有一些巧妙的技巧,旨在保持数据紧凑和有条理。

* 没有像无模式数据库这样的东西 – 最多它只能有一个隐式模式。

让我们直接从一个关于最简单的 Redis 数据类型的例子开始:

127.0.0.1:6379> SET swallow coconut
OK

我们刚刚使用了多少 RAM?由于所有数据都按键组织,因此键名是我们检查的第一个元素。我们知道 Redis 键名是二进制安全的字符串,最长可达 512MB。酷。Redis 中的字符串值也是二进制安全的,最长可达 0.5GB,因此在上面的示例中,我们可以假设 “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 个字节。甚至更多,秘密的 DEBUG SDSLEN 命令也提出了相同的声明,但两者都没有考虑数据的开销,并且每个 Redis 数据结构都有其自身的负担。这意味着除了实际的字符串(“swallow” 和 “coconut”)之外,Redis 还需要一些 RAM 来管理它们。

由于 Redis 中的每个键值元组都使用额外的 RAM 进行其内部簿记,并且因为该 RAM 的数量取决于数据结构和数据本身,所以我认为这种开销(虽然是元数据)是用户数据的一部分。换句话说,如果对于每 X 个字节的字符串,Redis 需要 X + Y 个字节,那么 Y 绝对是我想要转化为 KK 的 KU。

弦理论

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 个字节,空字符为额外的字节,这使每个字符串的总开销为 9 个字节。长度为 7 个字节的 “swallow” 突然需要超过该量的两倍 – 16 个字节的 RAM – 才能存储在 Redis 中!

旁注: 它实际上使用更多。对每个键的引用也存储在 Redis 的键空间哈希表中,这反过来需要更多的 RAM……而且还有 robj “对象”,Redis 使用它来促进一些东西,例如 LRU……但让我们暂时将键的管理 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_MINLONG_MAX 之间的整数值(如您的环境的 limits.h 中定义),并且还利用 shared.integers 构造来避免重复数据。因此,这些占用的空间更少。另一方面,长度超过 39 个字节的字符串存储为 “raw”,而较短的字符串使用 “embstr” 编码(魔术数字由 redis.h 中的 REDIS_ENCODING_EMBSTR_SIZE_LIMIT 定义)。

陷入(椰子)疯狂

其他椰子结构呢?字符串是 Redis 提供的最简单的数据结构,它们在内部用于制作其他更高级的结构。哈希由一堆字符串(字段和值)组成,并添加了字典数据结构,其中每个条目都是一个链表……但是它们可以完全编码为 ziplist。说到列表,我们有链表和 ziplist(甚至可能还有 Matt Stancliff @mattstaquicklists),它们也被集合和排序集合使用……

我可以 无限期地 继续深入研究 Redis 中数据编码的复杂性以及它们如何影响内存消耗。但是,我担心这很快会让我们所有人无聊致死。或者,您可以 git checkout 源代码 并开始阅读它 – 我知道有些人已经这样做了,但您需要一定的编程技能才能做到这一点。

这仍然使大 KU – Redis 需要多少 RAM? – 及其小兄弟 KU – 椰子需要多少 RAM? – 几乎没有得到解答。但是,有志者事竟成。请随时通过常用渠道与我互动 – 我是高可用的 🙂

旁注: 待续。