【发布时间】:2023-03-16 02:18:02
【问题描述】:
我使用 Redis 作为内存中的哈希集。在我将 1M 的 8 字节密钥(二进制)插入 Set 后,我发现 Redis USED_MEMORY 大约为 100M,这意味着单个成员需要 100 字节?为什么?
或者如何配置 Redis 以节省内存使用量。
【问题讨论】:
标签: c++ memory nosql redis hashset
我使用 Redis 作为内存中的哈希集。在我将 1M 的 8 字节密钥(二进制)插入 Set 后,我发现 Redis USED_MEMORY 大约为 100M,这意味着单个成员需要 100 字节?为什么?
或者如何配置 Redis 以节省内存使用量。
【问题讨论】:
标签: c++ memory nosql redis hashset
首先,您应该始终详细说明此类问题的设置,因为内存布局取决于操作系统、内存分配器、平台和 Redis 版本。
在带有 Redis 2.4 的 64 位 Linux 机器上,1M 项的 8 字节密钥集占用 87 MB。
与键的大小相比,这似乎很多,但是任何支持对其项目进行有效访问的动态数据结构都会产生开销。您的项目越小,开销就越大。
使用 Redis,大型集合是使用单独的链式哈希表实现的。每个条目由以下结构表示:
typedef struct dictEntry {
void *key;
void *val;
struct dictEntry *next;
} dictEntry;
因为内存分配器 (jemalloc) 不支持 24 字节类,所以使用 32 字节。在这个结构中,val被设置为NULL(这是一个集合),key指向一个对象,定义如下:
typedef struct redisObject {
unsigned type:4;
unsigned storage:2; /* REDIS_VM_MEMORY or REDIS_VM_SWAPPING */
unsigned encoding:4;
unsigned lru:22; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
这个结构只占用 16 个字节。它指向关键数据本身,由这个变长结构表示:
struct sdshdr {
int len;
int free;
char buf[];
};
键为 8 个字节,外加一个 nul 字符,因此每个键的大小为 17 个字节。下一个分配类是 jemalloc 的 32 字节,所以这个结构将占用 32 字节。
总而言之,每个项目将花费:32+16+32 = 80 字节。他们有1M ot。为哈希表本身添加一些空间(包含至少 1M 指向 dictEntry 结构的指针),你得到的结果非常接近我们在这个平台上可以测量的 87 MB。
优化大型集合的内存占用并不是一件容易的事。 Redis 在集合很小(默认小于 512 项)并且键实际上是整数时执行优化。查看更多信息here。
一种可能的优化是增加 set-max-intset-entries 参数,并将集合拆分为多个部分。例如,可以对项目键进行散列以将项目分布在不同的集合上。你有 myset:0, myset:1, myset:2 ... myset:n,而不仅仅是 myset。要检查给定项目是否为集合,在键上计算哈希值以找到正确的 myset:X 条目,然后检查此特定条目。目的是将所有这些集合的大小保持在 set-max-intset-entries 参数以下,以从内存优化中受益。当然,它使在集合上完成的所有操作更加复杂,因此它确实是复杂性和内存占用之间的权衡。
【讨论】:
如果不知道集合中每个成员的底层结构,就不可能说出来。但是,如果您要存储键/值,则每个成员都存储键和值(即使值是空的,它仍然需要为它保存一个引用)。
对于键的快速查找,底层结构很可能是一棵树,这意味着它需要为每个成员存储指向树中左右下降节点的左右(或红色/黑色)指针。在 64 位系统中,这些指针每个为 8 个字节。
为了有效地分配和取消分配键/值对,每个成员节点都可能有数据成员来指示它的大小和可用性(已分配、已删除),以便每个成员节点都可以从内存池和垃圾池中分配收集或标记为已删除和重复使用。每次前一个池被填满时,典型的池分配都会使池大小翻倍,以最大限度地减少堆争用,这对于多线程应用程序的性能非常重要。您的 100M 内存使用量可能包含 50M 未使用(但已分配)的密钥持有者。
为什么要节省内存使用量?您是否打算存储数十亿个哈希键?
【讨论】: