Redis内存模型原来是这样的啊!
来源:左耳君(ID:qaqzuoer)
作者: 湿兄
本文通过内存使用情况、内存划分情况、内存管理、Redis数据存储细节几部分来分析redis的内存部分,下一篇我们会说Redis对象类型的内部编码,一篇说清篇幅过长,容易引起阅读不适
前言
上一篇Redis原来不止五种类型啊(含常用命令)中,我们介绍了Redis的5种基本对象类型(字符串、哈希、列表、集合、有序集合)和一些高级数据结构HyperLogLog、Geo、Bit
丰富的类型是Redis相对于Memcached等的一大优势。在了解Redis的对象类型的用法和特点的基础上,进一步了解Redis的内存模型,对Redis的使用有很大帮助,例如估算Redis内存使用量,优化内存使用,分析Redis的阻塞、内存占用等问题
这篇文章主要介绍Redis的内存模型(以4.0为例),包括Redis占用内存的情况及如何查询、不同的对象类型在内存中的编码方式、内存分配器(jemalloc)、简单动态字符串(SDS)、RedisObject等,然后在此基础上介绍几个Redis内存模型的应用。
内存使用情况
在客户端通过redis-cli连接服务器后(后面如无特殊说明,客户端一律使用redis-cli),我们可以通过info memory命令查看内存使用情况(info命令可以显示redis服务器的许多信息,包括服务器基本信息、CPU、内存、持久化、客户端连接信息等等;memory是参数,表示只显示内存相关的信息):
127.0.0.1:6379> info memory
# Memory
used_memory:673056
used_memory_human:657.28K
used_memory_rss:2416640
used_memory_rss_human:2.30M
used_memory_peak:692400
used_memory_peak_human:676.17K
used_memory_peak_perc:97.21%
used_memory_overhead:659810
used_memory_startup:609488
used_memory_dataset:13246
used_memory_dataset_perc:20.84%
total_system_memory:820248576
total_system_memory_human:782.25M
used_memory_lua:25600
used_memory_lua_human:25.00K
maxmemory:3221225472
maxmemory_human:3.00G
maxmemory_policy:noeviction
mem_fragmentation_ratio:3.59
mem_allocator:jemalloc-4.0.3
active_defrag_running:0
lazyfree_pending_objects:0
整理好了给大家:
重点介绍:
1、used_memory:Redis分配器分配的内存总量(单位是KB),包括虚拟内存(即swap),used_memory_human是让结果显示的更友好;
2、used_memory_rss:Redis进程占据操作系统的内存(单位是KB),和top及ps命令看到的值是一致的,除了分配器分配的内存之外,还包括内存碎片和进程本身的内存等,但是不包括虚拟内存
used_memory和used_memory_rss,前者是从Redis角度得到的量,后者是从操作系统角度得到的量。
二者之所以有所不同,一方面是因为内存碎片和Redis进程运行需要占用内存,使得前者可能比后者小,
另一方面虚拟内存的存在,使得前者可能比后者大。
3、mem_fragmentation_ratio : 内存碎片比率,该值是used_memory_rss / used_memory的比值。由于在实际应用中,Redis的数据量会比较大,此时进程运行占用的内存与Redis数据量和内存碎片相比,都会小得多;因此used_memory_rss和used_memory的比例,便成了衡量Redis内存碎片率的参数
mem_fragmentation_ratio > 1 碎片率过大,导致内存资源浪费,值越大,说明碎片越严重。mem_fragmentation_ratio < 1: 一般出现在操作系统把Redis内存交换到硬盘导致,redis已使用swap分区。由于虚拟内存的媒介是磁盘,比内存速度要慢很多,当这种情况出现时,应该及时排查,如果内存不足应该及时处理,如增加Redis节点、增加Redis服务器的内存、优化应用等。
参数在1.03左右是比较健康的状态(对于jemalloc来说),上面例子中的mem_fragmentation_ratio值很大,是因为还没有向Redis中存入数据,Redis进程本身运行的内存使得used_memory_rss 比used_memory大得多。
4、mem_allocator:Redis使用的内存分配器,在编译时指定,可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc;上面例子中使用的便是默认的jemalloc-4.0.3。
内存划分情况
Redis作为内存数据库,在内存中存储的内容主要是数据(键值对),除了数据以外,Redis的其他部分也会占用内存,Redis内存主要划分为以下几部分(自身数据内存、自身进程内存、缓冲内存、内存碎片)
1、自身数据内存:作为数据库,数据肯定是最主要的部分,这部分内存也在used_memory中。Redis所有数据都是Key-Value型,每次创建Key-Value都是创建2个对象,即Key对象和Value对象。Key对象都是字符串,使用过程中尽量避免使用过长的Key。
Value对象则包括5种类型(String,List,Hash,Set,Zset),每种类型占用内存不同,在Redis内部,每种类型可能有2种或更多的内部编码实现。Redis在存储数据的时候会对数据进行包装,如redisObject、SDS等,在下面会讲解每种类型的详细内部编码。
2、自身进程内存:Redis进程自身也会消耗内存(如代码、常量池),不过很少,大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。
当然除了这些,Redis创建的子进程也会运行占用内存,如Redis执行AOF或者RDB重写时创建的子进程,这部分内存不属于Redis进程,也不会统计在used_memory和used_memory_rss中。
3、缓冲内存:缓冲内存包括客户端缓冲区、复制积压缓冲区和AOF缓冲区,这部分内存由jemalloc分配,因此会统计在used_memory中。
客户端缓冲区指的是所有连接到Redis服务器的连接的输入输出缓冲区,输入缓冲无法控制,最大空间为1G,输出缓冲区可通过client-output-buffer-limit控制。客户端缓冲区又分为普通客户端、从客户端和订阅客户端。
复制积压缓冲区用于部分复制,部分复制我们会在持久化机制文章中详细介绍,根据repl-backlog-size参数控制,默认1MB。对于复制积压缓区,主节点有一个,所有从节点会共享这个缓冲区,根据实际情况设置相应的大小 可以有效的避免全量复制。
AOF缓冲区用于在进行AOF重写时,保存最近的写入命令,等待被刷到磁盘。命令会先写入到缓冲区,然后根据响应的策略向磁盘进行同步,消耗的内存取决于写入的命令量和重写时间,通常很小。
4、内存碎片:Redis默认的内存分配器是jemalloc,内存分配器的作用就是为了更好的管理和重复利用内存。内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致redis释放的空间在物理内存中并没有释放,但redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中。
内存碎片的产生与对数据进行的操作、数据的特点以及使用的内存分配器等都有关;如果Redis服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:因为重启之后,Redis重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。
内存管理
Redis通过maxmemory(redis.conf中)参数限制最大可用内存。限制内存目的主要有:用于缓存场景,当超出内存上限maxmemory时候使用LRU等删除策略释放空间,防止所用内存超过服务器物理内存。
在生产环境中尽量设置最大内存,因为一旦物理内存用爆了就会大量使用Swap,写RDB文件的时候会很慢。
若是启用了Redis快照功能,应该设置最大内存值值为系统可使用内存的45%,因为快照时需要一倍的内存来复制整个数据集,也就是说如果当前已使用45%,在快照期间会变成95%(45%+45%+5%),其中5%是预留给其他的开销。如果没开启快照功能,maxmemory最高能设置为系统可用内存的95%。我们可以通过命令config set maxmemory NGB来动态调整内存上限;
Redis中有定期删除和惰性删除两种删除缓存的方式,定期就是每隔一段时间,随机检查一些key然后过期的删除掉,为什么不能是全部删除?因为全部会是灾难,100ms一次,太耗时。定期删除可能会导致一些key未删掉,惰性删除就是当用户用这个key的时候再检查是否过期,如果过期主动删除掉,没过期继续
这个时候出现了一个问题,最后如果定期没删,我也没查询,那可咋整?
我们有内存淘汰机制,当Redis所用内存达到maxmeory上限时,会触发相应的溢出控制策略。即按照配置的Policy进行处理, 默认策略为volatile-lru,对设置了expire time的key进行LRU清除(不是按实际expire time)。
官网上给到的内存淘汰机制是以下几个:
- noenviction:不清除数据,只是返回错误,这样会导致浪费掉更多的内存,对大多数写命令(DEL 命令和其他的少数命令例外)
- allkeys-lru:从所有的数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,以供新数据使用
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰,以供新数据使用
- allkeys-random:从所有数据集(server.db[i].dict)中任意选择数据淘汰,以供新数据使用
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰,以供新数据使用
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,以供新数据使用
Redis数据存储细节
上面介绍了Redis的内存的使用、划分和管理的一些细节问题,接下来要讲的是具体数据类型在存储过程中的细节问题,涉及到内存分配器、多种对象类型以及其使用的内部编码、内部简单动态字符串SDS以及redisObject。
- dictEntry:Redis中的每个键值对都会对应一个dictEntry,里面存储了Key和Value对应的指针,指针指向相应数据的位置。next指向下一个dictEntry,与本Key-Value无关。
- Key:图中Key对应的hello并不是直接以字符串存储,而是存储在SDS结构中。
- redisObject:Value对应的值world既不是直接以字符串存储,也不是存储在SDS中,而是存储在redisObject中。在Redis中的Value的多种类型都是通过redisObject来存储。我们看redisObject中的type字段指向了Value对象的类型身体string,ptr则指向对象的内存地址,该地址仍然是一个SDS结构存储数据。
- 内存分配器jemalloc:无论是DictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储。以dictEntry对象为例,有3个指针组成,在64位机器下占24个字节,jemalloc会为它分配32字节大小的内存单元。
接下来详细介绍各部分:
redisObject
redis源码点击https://github.com/antirez/redis,拉取代码到本地
redis的多种对象类型都不是直接存储的,都是通过redisObject对象存储的,redisObject对象包含对象类型、内部编码、内存回收、共享对象等多个部分。redisObject结构如下所示:
typedef struct redisObject {
unsigned type:4; //对象类型
unsigned encoding:4; //内部编码
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) 计时时钟*/
int refcount; //引用计数器
void *ptr; //数据指针
} robj;
1、type对象类型:
type字段表示对象的数据类型,占据4个bit,当我们执行type object指令时可以查看相应的类型
127.0.0.1:6379> set key value
OK
127.0.0.1:6379> type key
string
127.0.0.1:6379> hset std name xiaoming
(integer) 1
127.0.0.1:6379> type std
hash
127.0.0.1:6379> sadd lst member1 member2
(integer) 2
127.0.0.1:6379> type lst
set
2、encoding内部编码类型
encoding表示对象的内部编码,占4个bit。redis支持的各种数据类型,每种至少存在两种内部编码,比如string存在int、embstr、raw三种类型编码,list存在ziplist、linkedlist两种类型编码。
每种类型存在多种不同编码的好处在于可以根据不同的使用场景自动切换内部不同的编码来提高效率,大大的提高了灵活性,也做到了解耦用户和底层编码优化。我们可以通过object encoding key命令来查看相应的编码。
127.0.0.1:6379> object encoding key
"embstr"
127.0.0.1:6379> set key 100
OK
127.0.0.1:6379> object encoding key
"int"
每种类型的具体编码会在下一篇redis对象类型的内部编码部分讲解,耐心读下去。
3、LRU计时时钟
lru记录的是对象最后一次被命令访问的时间,占据的比特数不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。通过计算lru时间和当前时间差值我们可以得到某个对象的空转时间,object idletime key命令可以显示该空转时间(单位是秒)。object idletime命令的一个特殊之处在于它不改变对象的lru值。
127.0.0.1:6379> set name xiaoming
OK
127.0.0.1:6379> object idletime name
(integer) 18
127.0.0.1:6379> object idletime name
(integer) 23
127.0.0.1:6379> object idletime name
(integer) 26
127.0.0.1:6379> get name
"xiaoming"
127.0.0.1:6379> object idletime name
(integer) 1
lru值还和内存回收有关系,如果redis打开了maxmemory选项,并且内存回收算法是volatile-lru或者allkeys-lru,当内存占用超过maxmemory指定的值得时候,redis会优先选择空转时间最长的对象进行释放。
4、refcount引用计数器
refcount记录的是对象被引用的次数,refcount主要用于对象的引用计数和内存回收,是不是想起点什么?对了,就是类似jvm的对象存活判断方法之一引用计数法。当创建新对象时,refcount初始化为1,有程序调用时加一,对象不再被调用时减一,refcount变为0时对象占据的内存则会释放。
这里额外讲解一下共享对象,redis中被多次使用的对象成为共享对象(refcount>1)。redis为了节省内存,当有一些重复对象出现的时候新程序不会创建新的对象,而是使用原来的对象,这个被重复使用的对象叫做共享对象。Redis的共享对象目前只支持整数值的字符串对象。
之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)
就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是0-9999的整数值;当Redis需要使用值为0~9999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整参数OBJ_SHARED_INTEGERS的值进行改变。共享对象的引用次数可以通过object refcount命令查看。
5、*ptr数据指针
ptr指针指向具体的数据,redisObject的结构与对象类型、内存编码、内存回收、共享对象都有关系,一个redisObject对象的大小为16字节:4bit+4bit+24bit+4Byte+8Byte=16Byte。
SDS
Redis使用了SDS作为默认的字符串表示,SDS是简单动态字符串(Simple Dynamic String)的缩写。SDS结构如下所示:
struct sdshdr {
int len; //buf已使用的长度
int free; //buf未使用的长度
char buf[]; //buf表示字节数组,用来存储字符串
};
通过SDS的结构可以看出,buf数组的长度=free+len+1(其中1表示字符串结尾的空字符);
所以,一个SDS结构占据的空间为:free所占长度+len所占长度+ buf数组的长度=4+4+free+len+1=free+len+9
那么问题来了,既然redis是C语言写的,为什么不直接用C语言的字符串,换种问法也就是SDS结构优点?
1、存取二进制:SDS可以存储二进制数据,C字符串不可以。因为C中的字符串是以空字符串结尾,而对于二进制数据内容可能会包含空字符串,因此C字符串无法正确读取。而SDS以字符串长度len来作为字符串结束标识,因此没有这个问题。
2、内存重分配:使用C字符串时,若要修改字符串需要重新分配内存(先释放再申请),如果没有重新分配,字符串增大时容易造成内存溢出,字符串减小容易造成内存泄漏。而SDS由于可以记录len和free,因此解除了字符串长度和底层数组长度的耦合,可以进行优化。空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。
3、缓冲区溢出:由于上一个的内存重分配问题,造成了这里的缓冲区溢出问题。使用C字符串时,如果字符串长度增加时忘记重新分配内存,很容易造成内存溢出的问题。而SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
内存分配器jemalloc
redis在编译的时候可以指定内存分配器:libc、jemalloc、tcmalloc,默认是jemalloc。jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。jemalloc的内存划分单元如下:
例如,如果需要存储大小为200字节的对象,jemalloc会将其放入224字节的内存单元中,若是656字节对象,则会放在768字节的内存单元。