redis1111

概述

一、内存统计和分类

1.1.1 内存统计命令

注意info这个命令可以输出Redis服务相关的所有信息

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> info memory
# Memory
used_memory:1108392
used_memory_human:1.06M
used_memory_rss:4489216
used_memory_rss_human:4.28M
...
mem_fragmentation_ratio:4.13
mem_allocator:jemalloc-5.2.1
...
  • used_memory和used_memory_human是同一个数据表示Redis分配器的内存总量(包括虚拟内存),human只是便于人查看
  • used_memory_rss表示Redis服务从系统申请占用的真实内存(不包括虚拟内存),包括了服务自身的内存开销,而used_memory相当于业务占用内存
  • mem_fragmentation_ratio:内存碎片比率,该值是used_memory_rss / used_memory的比值,需要注意的是如果该数值小于1,说明使用了虚拟内存,服务会明显降速,需要进行排查
  • mem_allocator:Redis使用的内存分配器

1.1.2 内存分类

数据

一般就是业务数据,五种类型:字符串、哈希、列表、集合、有序集合

进程运行内存

Redis主进程运行需要的内存(代码、常量池等),大概只有几兆,另外一些子进程(比如持久化的AOF、RDB进程)运行会占用内存,这些内存都不会被统计出来

缓冲内存

缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等(所以上面提到的进程内存指的是进程运行需要的内存数据小不统计影响也不大),会被纳入统计

内存碎片

这个之前遇到过故障,当大批量的从Redis中删除数据时,Redis是不会将对应的内存还给操作系统的,也就是说内存没被释放(可以显式的执行命令释放,但是效果并不理想),这些碎片导致机器内存资源吃紧,所以需要预先根据分配器策略去设计业务的存储方案,因为走到这一步就很难处理了

例如,机器只有8G内存,业务数据4G,理论上是完全够用的,但是业务数据不断更新迭代,Redis使用的内存越来越大,达到了7G(有效数据仍为4G,3G内存碎片),这个时候已经会影响服务性能,但此时通过命令让分配器释放内存,不能达到预期效果,只会释放一小部分碎片或耗时太长,彻底的解决方案为重启Redis服务,风险很大,一是业务上是否允许,二是持久化策略配置是否合理,当时对Redis进行了持久化的配置,但是不知道没生效还是空闲内存太少导致失败,最终检查持久化文件只有几十兆的情况下进行服务重启,导致数据丢失,所幸数据还不是太重要,经过一周的爬虫补充回来了

二、存储细节

iShot2023-05-04 18.32.15

  • dictEntry:每个键值对都会有一个dictEntry
  • SDS:简单动态字符串,最终所有的key、value数据都是存储在SDS中
  • redisObject:redis中5种类型的value都会使用redisObject进行包装存储,所以这里面会标记数据类型

2.1 jemalloc

Redis默认的内存分配器,另外两种暂不进行了解,这个分配器在减小内存碎片方面做得比较好,注意jemalloc是给数据预分配内存空间,而不是多少数据占多少空间,比如130字节对象会放到160字节内存单元中,内存单元划分如下:

1174710

2.2 redisObject

1
2
3
4
5
6
7
8
9
10
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;

dictEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /* Next entry in the same hash bucket. */
void *metadata[]; /* An arbitrary number of bytes (starting at a
* pointer-aligned address) of size as returned
* by dictType's dictEntryMetadataBytes(). */
} dictEntry;

SDS

1
2
3
4
5
struct sdshdr {
int len;
int free;
char buf[];
};

添加free和len而不直接使用c字符串的作用

  • 获取字符串长度:O(1),字符串为O(n)
  • 缓冲区溢出:之前提到jemalloc分配内存是按长度划分内存单元,数据内容变更时,对长度进行判断溢出时自动重新分配,当然没有len也能去获取一次长度做判断,只是效率更低
  • 优化内存分配:c中字符串修改必然重新分配内存,这里相当于做了封装,预先给的空间会大一些,这样可以避免一部分情况下的内存分配,提高效率
  • 存取二进制数据:由于c字符串以空字符作为字符串结束标识,对于一些二进制文件可能出现误读,sds有精确的len标记解决此问题

三、内存估算

这里先不对5种数据类型内部的编码优化进行探索,只要结合上面的存储结构图和分配器内存单元图,对数据存储进行预估得到的结果也足够精准了

demo:90000个键值对,每个key的长度是7个字节,每个value的长度也是7个字节(都是字符串)

3.1 模糊预估

  • key
    • dictEntry:24Byte
    • sds:9Byte+7Byte
  • value
    • redisObject:16Byte+7Byte
    • sds:9Byte+7Byte
  • bucket数组:90000*8Byte
  • 总数:(24+9+7+16+7+9+7+8)*90000=7830000=7.46M

3.2 精准预估

  • 一个dictEntry,24字节,jemalloc会分配32字节的内存块
  • 一个key,7字节,所以SDS(key)需要7+9=16个字节,jemalloc会分配16字节的内存块
  • 一个redisObject,16字节,jemalloc会分配16字节的内存块
  • 一个value,7字节,所以SDS(value)需要7+9=16个字节,jemalloc会分配16字节的内存块
  • 综上,一个dictEntry需要32+16+16+16=80个字节
  • bucket数组的大小为大于90000的最小的2^n,是131072;每个bucket元素为8字节(因为64位系统中指针大小为8字节
  • 估算出这90000个键值对占据的内存大小为:9000080 + 1310728 = 8248576=7.86M

检验

通过脚本批量创建Redis数据,运行结果:8247552z

3.4 总结

精准预估确实厉害(博主还介绍了一下误差是由于bucket预分配),但模糊预估也能估算个大概规模了