内存消耗
内存统计
执行info memory命令获取内存相关指标。读懂每个指标有助于分析Redis内存使用情况
属性 | 属性说明 |
---|---|
used_memory | Redis分配器分配的内存总量,也就是内部存储的所有数据内存占用量 |
used_memory_human | 以可读的格式返回used memory |
used_memory_rss | 从操作系统的角度显示 Redis 进程占用的物理内存总量 |
used_memory_peak | 内存使用的最大值,表示 used memory 的峰值 |
used_memory_peak_human | 以可读的格式返回used memory peak |
used_memory_lua | Lua 引擎所消耗的内存大小 |
mem_fragmentation_ratio | used memory rss/used memory 比值,表示内存碎片率 |
mem_allocator | Redis 所使用的内存分配器。默认为jemalloc |
需要重点关注的指标有:used_memory_rss和used_memory以及它们的比值mem_fragmentation_ratio。
- 当mem_fragmentation_ratio>1时,说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储,而是被内存碎片所消耗,如果两者相差很大,说明碎片率严重。
- 当mem_fragmentation_ratio<1时,这种情况一般出现在操作系统把Redis内存交换(Swap)到硬盘导致,出现这种情况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差,甚至僵死。
内存消耗划分
主要包括:自身内存,对象内存,缓冲内存,内存碎片,其中空进程自身内存消耗非常小。
- 对象内存
是Redis内存占用最大的一块,存储着用户所有的数据 - 缓冲内存:
客户端缓冲、复制积压缓冲区、AOF缓冲区。- 客户端缓冲:指的是所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制,最大空间为1G,如果超过将断开连接。
- 复制积压缓冲区:Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能,根据repl-backlog-size参数控制,默认1MB。对于复制积压缓冲区整个主节点只有一个,所有的从节点共享此缓冲区,因此可以设置较大的缓冲区空间,如100MB,这部分内存投入是有价值的,可以有效避免全量复制
- AOF缓冲区:这部分空间用于在Redis重写期间保存最近的写入命令
- 内存碎片
Redis默认的内存分配器采用jemalloc,可选的分配器还有:glibc、tcmalloc。内存分配器为了更好地管理和重复利用内存,分配内存策略一般采用固定范围的内存块进行分配。 - 子进程内存消耗
子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。
子进程内存消耗:- Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出。
- 需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败。
- 排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-onwrite期间内存过度消耗。
内存管理
Redis主要通过控制内存上限和回收策略实现内存管理。
内存上限
- 在配置文件修改 maxmemory数值
- 动态修改内存:config set maxmeory xGB
内存回收
- 删除到达过期的建对象。
- 内存使用率到达maxmemory上限时出发的内存溢出控制策略。
删除到达过期的建对象
- 惰性删除
读取到key过期后,就删除对应的键对象 - 定时删除
定时任务删除,定期删除过期的键对象
内存溢出控制策略
- noeviction:默认策略,不删除任何数据,拒绝所有写入操作并返回客户端错误信息,只响应读操作。
- volatile-lru:根据lru算法,删除设置超时时间的键,直到腾出足够空间为止。
- allkeys-lru:根据LRU算法,不管数据 有没有设置超时属性,直到腾出足够空间为止
- allkeys-random:随机删除所有键,直到腾出空间为止
- volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据,如果没有退回到noeviction策略。
LUR(least recently used)算法:是一种缓存置换算法,本质通过获取键的最后一次访问时间来淘汰最长未访问数据。
内存优化
Redis所有的数据都在内存中,而内存又是非常宝贵的资源。
redisObject对象
Redis存储的所有值对象在内部定义为redisObject结构体。
Redis存储的数据都使用redisObject来封装,包括string、hash、list、set、zset在内的所有数据类型。理解redisObject对内存优化非常有帮助,下面针对每个字段做详细说明:
- type字段:表示当前对象使用的数据类型,Redis主要支持5种数据类型:string、hash、list、set、zset。可以使用type{key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。
- ·encoding字段:表示Redis内部编码类型,encoding在Redis内部使用,代表当前对象内部采用哪种数据结构实现。理解Redis内部编码方式对于优化内存非常重要,同一个对象采用不同的编码实现内存占用存在明显差异。
- ·lru字段:记录对象最后一次被访问的时间,当配置了maxmemory和maxmemory-policy=volatile-lru或者allkeys-lru时,用于辅助LRU算法删除键数据。可以使用object idletime{key}命令在不更新lru字段情况下查看当前键的空闲时间,可以使用scan+object idletime命令批量查询哪些键长时间未被访问。
- refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount{key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。
- *ptr字段:与对象的数据内容相关,如果是整数,直接存储数据;否则表示指向数据的指针。Redis在3.0之后对值对象是字符串且长度<=39字节的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作即可。
缩减键值对象
降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长
度。
- key长度:如在设计键时,在完整描述业务情况下,键值越短越好。如user:{uid}:friends:notify:{fid}可以简化为u:{uid}:fs:nt:{fid}。
- value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。以Java为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如:protostuff、kryo等。
其中java-built-in-serializer表示Java内置序列化方式。值对象除了存储二进制数据之外,通常还会使用通用格式存储数据比如:json、xml等作为字符串存储在Redis中。这种方式优点是方便调试和跨语言,但是同样的数据相比字节数组所需的空间更大,在内存紧张的情况下,可以使用通用压缩算法压缩json、xml后再存入Redis,从而降低内存占用,例如使用GZIP压缩后的json可降低约60%的空间。Google的Snappy效率远远高于GZIP等传统压缩工具,且支持所有主流语言环境。
共享对象池
共享对象池是指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。除了整数值对象,其他类型list、hash、set、zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。
整数对象池在Redis中通过变量REDIS_SHARED_INTEGERS定义,不能通过配置修改。可以通过object refcount命令查看对象引用数验证是否启用整数对象池技术,如下:
redis> set foo 100
OK
redis> object refcount foo
(integer) 2
redis> set bar 100
OK
redis> object refcount bar
(integer) 3
设置键foo等于100时,直接使用共享池内整数对象,因此引用数是2,再设置键bar等于100时,引用数又变为3。
使用共享对象池后,相同的数据内存使用降低30%以上。需要注意的是对象池并不是只要存储[0-9999]的整数就可以工作。当设置maxmemory并启用LRU相关淘汰策略如:volatile-lru,allkeys-lru时,Redis禁止使用共享对象池。
共享对象池与maxmemory+LRU策略冲突,使用时需要注意。对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高。
字符串优化
字符串对象是Redis内部最常用的数据类型。所有的键都是字符串类型,值对象数据除了整数之外都使用字符串存储。比如执行命令:lpushcache:type"redis"“memcache”“tair”“levelDB”,Redis首先创建"cache:type"键字符串,然后创建链表对象,链表对象内再包含四个字符串对象,排除Redis内部用到的字符串对象之外至少创建5个字符串对象。可见字符串对象在Redis内部使用非常广泛,因此深刻理解Redis字符串对于内存优化非常有帮助。
- 字符串结构
Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构,内部简单动态字符串(simple dynamic string,SDS)。
Redis自身实现的字符串结构有如下特点:
- O(1)时间复杂度获取:字符串长度、已用长度、未用长度。
- 可用于保存字节数组,支持安全的二进制数据存储。
- 内部实现空间预分配机制,降低内存再分配次数。
- 惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留。
- 预分配机制
因为字符串(SDS)存在预分配机制,日常开发中要小心预分配带来的内存浪费。
从测试数据可以看出,同样的数据追加后内存消耗非常严重。尽量减少字符串频繁修改操作如append、setrange,改为直接使用set修改字符串,降低预分配带来的内存浪费和内存碎片化。 - 字符串重构
字符串重构:指不一定把每份数据作为字符串整体存储,像json这样的数据可以使用has结构,使用二级结构存储也能帮我们节省内存。同时可以使用hmget、hmset命令支持字段的部分读取修改,而不用每次整体存取。例如下面的json数据:
分别使用字符串和hash结构测试内存表现{ "vid": "413368768", "title": "搜狐屌丝男士", "videoAlbumPic":"http://photocdn.sohu.com/60160518/vrsa_ver8400079_ae433_pic26.jpg", "pid": "6494271", "type": "1024", "playlist": "6494271", "playTime": "468" }
根据测试结构,第一次默认配置下使用hash类型,内存消耗不但没有降低反而比字符串存储多出2倍,而调整hash-max-ziplist-value=66之后内存降低为535.60M。因为json的videoAlbumPic属性长度是65,而hash-max-ziplist-value默认值是64,Redis采用hashtable编码方式,反而消耗了大量内存。调整配置后hash类型内部编码方式变为ziplist,相比字符串更省内存且支持属性的部分操作。
编码优化
- 了解编码
Redis对外提供了string、list、hash、set、zet等类型,但是Redis内部针对不同类型存在编码的概念,所谓编码就是具体使用哪种底层数据结构来实现。编码不同将直接影响数据的内存占用和读写效率。使用objectencoding{key}命令获取编码类型。如下所示:redis> set str:1 hello OK redis> object encoding str:1 "embstr" // embstr编码字符串 redis> lpush list:1 1 2 3 (integer) 3 redis> object encoding list:1 "ziplist" // ziplist编码列表
Redis针对每种数据类型(type)可以采用至少两种编码方式来实现.
2. 控制编码类型
编码类型转换在Redis写入数据时自动完成,这个转换过程是不可逆的,转换规则只能从小内存编码向大内存编码转换。
3. ziplist编码
ziplist编码主要目的是为了节约内存,因此所有数据都是采用线性连续的内存结构。
测试数据采用100W个36字节数据,划分为1000个键,每个类型长度统一为1000。从测试结果可以看出:
1)使用ziplist可以分别作为hash、list、zset数据类型实现。
2)使用ziplist编码类型可以大幅降低内存占用。
3)ziplist实现的数据类型相比原生结构,命令操作更加耗时,不同类型
耗时排序:list<hash<zset。
ziplist压缩编码的性能表现跟值长度和元素个数密切相关,正因为如此Redis提供了{type}-max-ziplist-value和{type}-max-ziplist-entries相关参数来做控制ziplist编码转换。最后再次强调使用ziplist压缩编码的原则:追求空间和时间的平衡。针对性能要求较高的场景使用ziplist,建议长度不要超过1000,每个元素大小控制在512字节以内。命令平均耗时使用info Commandstats命令获取,包含每个命令调用次数、总耗时、平均耗时,单位为微秒。
4. intset编码
intset编码是集合(set)类型编码的一种,内部表现为存储有序、不重
复的整数集。当集合只包含整数且长度不超过set-max-intset-entries配置时被
启用。
根据以上测试结果发现intset表现非常好,同样的数据内存占用只有不到hashtable编码的十分之一。intset数据结构插入命令复杂度为O(n),查询命令为O(log(n)),由整数占用空间非常小,所以在集合长度可控的基础上,写入命令执行速度也会非常快,因此当使用整数集合时尽量使用intset编码。表8-8测试第三行把ziplist-hash类型也放入其中,主要因为intset编码必须存储整数,当集合内保存非整数数据时,无法使用intset实现内存优化。这时可以使用ziplist-hash类型对象模拟集合类型,hash的field当作集合中的元素,value设置为1字节占位符即可。使用ziplist编码的hash类型依然比使用hashtable编码的集合节省大量内存。