第八章 理解内存
Redis所有的数据都存在内存中,当前内存虽然越来越便宜,但跟廉价的硬盘相比成本还是比较昂贵,因此如何高效利用Redis内存变得非常重要。高效利用Redis内存首先需要理解Redis内存消耗在哪里,如何管理内存,最后才能考虑如何优化内存。掌握这些知识后能够实现用更少的内存存储更多的数据,从而降低成本。本章主要内容如下:
- 内存消耗分析。
- 管理内存的原理与方法。
- 内存优化技巧。
8.1 内存消耗
理解Redis内存,首先需要掌握Redis内存消耗在哪些方面。有些内存消耗是必不可少的,而有些可以通过参数调整和合理使用来规避内存浪费。内存消耗可以分为进程自身消耗和子进程消耗。
8.1.1 内存使用统计
要查看redis自身的内存使用情况,可以使用info memory命令来获取。下面是部分的指标解析:
属性名 | 属性说明 |
---|---|
used_memory_human | 以可读的格式返回used_memory |
used_memory_rss_human | 从操作系统的绝度显示redis进程占用的物理内存总量 |
used_memory_peak_human | 以可读的方式返回used_memory_peak |
used_memory_lua_human | Lua引擎所消耗的内存大小 |
mem_fragmentation_ratio | used_memory_rss/used_memory的比值,表示内存碎片率 |
mem_allocator | Redis所使用的内存分配器。默认是jemlloc |
这里需要特别说明的是mem_fragmentation_ratio这个指标。
当mem_fragmentation_ratio>1时,说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储,而是被内存碎片所消耗,如果两者相差很大,说明碎片率严重。
当mem_fragmentation_ratio<1时,这种情况一般出现在操作系统把Redis内存交换(Swap)到硬盘导致,出现这种情况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差,甚至僵死。
8.1.2 内存消耗划分
Redis进程内存的消耗主要包括:自身内存+对象内存+缓冲内存+内存碎片。
其中Redis自身内存消耗非常小,在空载的时候Redis的自身内存使用不超过1MB。Redis主要内存消耗的图如下所示:
8.1.3 子进程内存消耗
子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。由于Linux采用写时复制技术,Redis在fork操作的时候消耗的内存实际上很小。但是需要关注的一个参数是:Transparent Huge Pages(THP)。在Linux内核(2.6.38)
中增加这个参数后,写时复制的内存页的单位从4KB变成了2MB,如果父进程有大量写命令,会加重内存拷贝量,从而造成过度内存消耗。建议关闭。
子进程内存消耗总结如下:
- Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出。
- 需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败。
- 排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-onwrite期间内存过度消耗。
8.2 内存管理
8.2.1 设置内存上限
Redis使用maxmemory参数限制最大可用内存。限制内存的目的主要有:
- 用于缓存场景,当超出内存上限maxmemory时使用LRU等删除策略释放空间。
- 防止所用内存超过服务器物理内存。
Redis默认无限使用服务器内存,为防止极端情况下导致系统内存耗尽,建议所有的Redis进程都要配置maxmemory。
8.2.3 内存回收策略
Redis的内存回收机制主要体现在以下两个方面:
- 删除到达过期时间的键对象。
- 内存使用达到maxmemory上限时触发内存溢出控制策略。
1. 删除过期键对象
Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存大量的键,维护每个键精准的过期删除机制会导致消耗大量的CPU,对于单线程的Redis来说成本过高,因此Redis采用惰性删除和定时任务删除机制实现过期键的内存回收。
-
惰性删除:惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除。但是单独用这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。
-
定时任务删除:Redis内部维护一个定时任务,默认每秒运行10次(通过配置hz控制)。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例、使用快慢两种速率模式回收键,流程如图8-4所示。
流程说明:
- 定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键。
- 如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25毫秒。
- 如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次。
- 快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。
2. 内存溢出控制策略
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略。
- noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时Redis只响应读操作。
- volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
- allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
- allkeys-random:随机删除所有键,直到腾出足够空间为止。
- volatile-random:随机删除过期键,直到腾出足够空间为止。
- volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
内存溢出控制策略可以采用config set maxmemory-policy{policy}动态配置。Redis支持丰富的内存溢出应对策略,可以根据实际需求灵活定制,比如当设置volatile-lru策略时,保证具有过期属性的键可以根据LRU剔除,而未设置超时的键可以永久保留
8.3 内存优化
内存优化的点包括:
- redisObject对象,也就是要捣鼓redis数据结构的对象了,设置一个合适长度的encoding,可能会有一个较大的优化效果。
- 缩减键值对象,比如user:uid修改为u:uid;也可以使用序列化效率更高的工具。
- 共享对象池,redis内部维护一个[0-9999]的整数对象池。创建整数的对象的时候,直接新增一个共享连接即可,不需要创建新的对象。但是这在设置了maxmemory并启用LRU相关淘汰策略的时候,这个共享池是会被禁用的。
- 字符串优化。
- 编码的优化,使用ziplist压缩编码优化hash、list等结构,注重效率和空间的平衡。
- 控制键的数量,比如可以用hash结构来代替string结构,这样可以降低键的数量。