参考自《redis开发与运维》
1. 内存消耗
1.1 内存使用统计
info memory指令,重点内容如下:
used_memory:redis内部数据所占内存总量
used_memory_rss:从操作系统角度看redis占用的内存总量
used_memory_peak:used_memory的峰值
mem_fragmentation_ratio:内存碎片率,used_memory_rss/info memory
如果内存碎片率大于1很多,则说明碎片化严重,如果小于1表示redis把内存里的数据交换到了硬盘,需要特别注意这个参数。
1.2 内存组成
(1)自身内存:很小,可忽略不计
(2)对象内存:包括redis中所有的key和对应的值所占的内存空间,因此应当避免过长的键,而值需要根据具体类型选择相应的编码。
(3)缓冲内存:
客户端缓冲:
输入输出缓冲区,输入缓冲区不可控,最大1G,输出缓冲区可以通过client-output-buffer-limit 参数控制。
普通客户端输出缓冲区:redis默认是不限制,一般普通客户端的内存消耗可以忽略不计,但某些指令如monitor就需要特别关注。
从客户端输出缓冲区:默认为client-output-buffer-limit slave256mb64mb60,当主从节点网络不好(如跨机房)或者主节点挂载多个从节点时,可能导致输出缓冲区过大。
订阅客户端输出缓冲区:默认为client-output-buffer-limit pubsub32mb8mb60,当消息生产者消息生成过快而消费速度过慢时,也有可能造成输出缓冲区溢出
复制积压缓冲区:redis2.8后才有,在主节点中存在,和所有从节点共享,用于实现部分复制功能,可以通过参数repl-backlog-size适当调大(如100M),它可以有效避免全量复制,默认为1M。
AOF缓冲区:用于redis在使用AOF方式时持久化数据到磁盘,无法控制,内存消耗取决于AOF持久时间和写入命令量。一般较小。
(4)内存碎片:根redis内存分配有关
redis默认使用jemalloc进行内存分配。它将内存空间划分为小、中、大三个范围,每个范围又划分为多个小的内存块单位,比如保存5k对象时,需要用8k的块进行存储,一定程度上会出现内存碎片。内存碎片率正常在1.03左右。
内存碎片出现场景:频繁做更新操作,如对已经存在的键做append、setrange操作;大量过期键删除,释放的空间不能有效利用时。
内存碎片解决方案:安全重启(单实例则直接重启服务,有主从集群等关系就切换节点为从节点然后重启服务)和数据对齐(数据尽量采用固定长度字符串或数字类型)
1.3 子进程内存消耗
当redis在执行AOF或RDB持久化时,需要fork一个子进程来进行持久化,而子进程的内存消耗和父进程相同,因此Linux出现了写时复制的技术,即父进程只有在写的时候才会复制相关页,而子进程读取父进程的内存进行持久化,即子进程读取的是fork时的父进程内存快照。
THP问题:即父进程在复制相关内存时由原来的4k(内存页单位)变成2M,如果父进程有大量写,会加重内存拷贝,从而造成过度内存消耗。因此需要关闭THP功能。
2. 内存管理
2.1 设置内存上限
maxmemory限制最大可用内存,可用防止redis的内存超过物理内存。
注意:maxmemory是redis实际使用的内存量,也就是used_memory统计的内存,即对象内存区域,不包括缓冲区、内存碎片,因此需要注意这部分内存溢出。
2.2 动态调整内存上限
通过config set maxmemory进行动态修改最大可用内存。
2.3 内存回收策略
2.3.1 删除过期键对象
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。
惰性删除,当客户端读取带有超时属性的键时,如果该键已经过时,那么删除该键值,并返回空。如果过期键一直没有读取,则不会及时释放。因此有了定时任务删除模式。
定期任务删除: Redis 默认会每秒进行10次(通过hz控制)过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
从过期字典中随机 20 个 key;
删除这 20 个 key 中已经过期的 key;
如果过期的 key 比率超过 1/4,那就重复步骤 1;
该策略中采用了自适应快慢两种算法运行模式来回收键,流程如下:
1)定时任务在每个数据库空间随机检查20个键,发现过期时删除对应的键。
2)如果检索出来的键超过25%都是过期的键,那么将会循环执行回收逻辑操作 直到比例不足 25% 或运行到超时为止(为了避免过期扫描或者循环回收过度导致卡死,慢模式回收默认是25毫秒超时)
3)如果之前的回收键逻辑超时,则在 Redis出发内部事件之前再次以快模式运行回收过期任务,快模式下超时时间为1毫秒,且 2 秒内只能运行一次。
4)快慢模式的内部删除回收逻辑相同,只是超时事件不同罢了。
如果某一时刻,有大量key同时过期,Redis 会持续扫描过期字典,造成客户端响应卡顿,因此设置过期时间时,就尽量避免这个问题,在设置过期时间时,可以给过期时间设置一个随机范围,避免同一时刻过期。
2.3.2 内存溢出控制策略
(1)noeviction:默认策略,不删除数据,也拒绝写入,即redis变成只读操作。
(2)volatile-lru:根据LRU算法删除设置了超时属性的键,直到有足够的空间为止。如果没有可删除对象,回退到默认策略。
(3)allkeys-lru:根据lru算法删除键,不管数据是否设置了超时属性,直到腾出足够空间为止
(4)allkeys-random:随机删除所有键,直到腾出足够空间为止。
(5)volatile-random:随机删除过期键,直到腾出足够空间为止。
(6)volatile-ttl:根据键值对象的ttl属性删除最近将要过期数据,如果没有回退到默认策略
内存溢出控制策略可以采用config set maxmemory-policy{policy}动态配置。当Redis一直工作在内存溢出(used_memory>maxmemory)的状态下且设置非noeviction策略时,会频繁地触发回收内存的操作,影响Redis的性能。建议线上Redis内存工作在maxmemory>used_memory状态下,避免频繁内存回收开销。
3.内存优化
3.1 redisObject对象
Redis存储的所有值对象在内部定义为redisObject结构体,如下图所示:
高并发写入场景中,在条件允许的情况下,建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数,从而提高性能。
3.2 缩减键值对象
降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。
·key长度:如在设计键时,在完整描述业务情况下,键值越短越好。
·value长度:值对象缩减比较复杂,一般为二进制数组或格式化存储(如json)。
在业务上精简对象,去掉不必要的属性
二进制数组在序列化时,选择更高效的序列化工具来降低字节数组大小
通用格式存储数据时,应考虑压缩速度和计算成本,如Google的Snappy压缩工具,降低存储空间。
3.3 共享对象池
共享对象池是指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。除了整数值对象,其他类型如list、hash、set、zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。
可以通过object refcount key 命令查看对象引用数。假设set foo 100,set bar 100这2个指令执行完后,这内存变成如下图所示:
注意:以下情况无法使用共享对象
内存回收策略为maxmemory+LRU时
值对象编码为ziplist时,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高
3.4 字符串优化
redis没有采用原生C的字符串类型,而是进行了封装,包括字符串已用长度、空闲长度、字符数组3个部分组成。内部实现空间预分配机制。第一次set时,正常分配,第二次使用append或setrange时,会进行空间的动态扩展,类似Java的ArrayList动态扩容机制,造成内存空间的浪费和内存碎片化加剧。因此在开发时,不要使用append或setrange指令。直接使用 set
字符串重构,对于json作为值时,可以使用hash结构进行重构优化。这样能提高内存使用率,可以使用hmget和hmset读取和修改部分字段,降低网络开销。
3.5 编码优化
在redis中,每种类型都对应一个或多个底层数据结构,我们称之为值的编码,编码不同将直接影响数据的内存占用和读写效率。可通过object encoding key来查询。
编码类型在redis写入数据时自动完成,这个过程是不可逆的,转化规则只能从小内存编码向大内存编码转换。
3.5.1 ziplist编码
ziplist编码主要目的是为了节约内存,因此所有数据都是采用线性连续的内存结构。ziplist编码是应用范围最广的一种,可以分别作为hash、list、zset类型的底层数据结构实现。
ziplist压缩编码的性能表现跟值长度和元素个数密切相关,正因为如此Redis提供了{type}-max-ziplist-value和{type}-max-ziplist-entries相关参数来做控制ziplist编码转换。最后再次强调使用ziplist压缩编码的原则:追求空间和时间的平衡,即减少内存空间的占用,但读写性能会降低。
针对性能要求较高的场景使用ziplist,建议长度不要超过1000,每个元素大小控制在512字节以内。
命令平均耗时使用info Commandstats命令获取,包含每个命令调用次数、总耗时、平均耗时,单位为微秒。
3.6 控制键的数量
当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消耗大量内存。Redis本质是一个数据结构服务器,它为我们提供多种数据结构,如hash、list、set、zset等。使用Redis时不要进入一个误区,大量使用get/set这样的API,把Redis当成Memcached使用。对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存。
使用hash重构后节省内存量效果非常明显,特别对于存储小对象的场景,下面分析这种内存优化技巧的关键点:
(1)hash类型节省内存的原理是使用ziplist编码,如果使用hashtable编码方式反而会增加内存消耗。
(2)ziplist长度需要控制在1000以内,否则读写长列表会导致CPU消耗严重,得不偿失。
(3)ziplist适合存储小对象,对于大对象不但内存优化效果不明显还会增加命令操作耗时。
(4)需要预估键的规模,从而确定每个hash结构需要存储的元素数量。
(5)根据hash长度和元素大小,调整hash-max-ziplist-entries和hash-max-ziplist-value参数,确保hash类型使用ziplist编码。
缺点:
hash重构后所有的键无法再使用超时(expire)和LRU淘汰机制自动删除,需要手动维护删除。
对于大对象,如1KB以上的对象,使用hash-ziplist结构控制键数量反而得不偿失。
优点:
对于大量小对象的存储场景,非常适合使用ziplist编码的hash类型控制键的规模来降低内存。