·内存消耗分析。
·管理内存的原理与方法。
·内存优化技巧。
1 内存消耗
理解Redis内存, 首先需要掌握Redis内存消耗在哪些方面。 有些内存消耗是必不可少的, 而有些可以通过参数调整和合理使用来规避内存浪费。 内
存消耗可以分为进程自身消耗和子进程消耗。
需要重点关注的指标有: used_memory_rss和used_memory以及它们的比值mem_fragmentation_ratio。
当mem_fragmentation_ratio>1时, 说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储, 而是被内存碎片所消耗, 如果两者相差很大, 说明碎片率严重。
当mem_fragmentation_ratio<1时, 这种情况一般出现在操作系统把Redis内存交换(Swap) 到硬盘导致, 出现这种情况时要格外关注, 由于硬盘速度远远慢于内存, Redis性能会变得很差, 甚至僵死。
1.2 内存消耗划分
Redis进程内消耗主要包括: 自身内存+对象内存+缓冲内存+内存碎片,
1.对象内存
对象内存是Redis内存占用最大的一块, 存储着用户所有的数据。
键对象都是字符串, 在使用Redis时很容易忽略键对内存消耗的影响, 应当避免使用过长的键。
2.缓冲内存
缓冲内存主要包括: 客户端缓冲、 复制积压缓冲区、 AOF缓冲区。
3.内存碎片
以下场景容易出现高内存碎片问题:
·频繁做更新操作, 例如频繁对已存在的键执行append、 setrange等更新操作。
·大量过期键删除, 键对象过期删除后, 释放的空间无法得到充分利用, 导致碎片率上升。
出现高内存碎片问题时常见的解决方式如下:
·数据对齐: 在条件允许的情况下尽量做数据对齐, 比如数据尽量采用数字类型或者固定长度字符串等, 但是这要视具体的业务而定, 有些场景无法做到。
·安全重启: 重启节点可以做到内存碎片重新整理, 因此可以利用高可用架构, 如Sentinel或Cluster, 将碎片率过高的主节点转换为从节点, 进行
安全重启。
1.3 子进程内存消耗
子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。 Redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同,
理论上需要一倍的物理内存来完成重写操作。 但Linux具有写时复制技术(copy-on-write) , 父子进程会共享相同的物理内存页, 当父进程处理写请求时会对需要修改的页复制出一份副本完成写操作, 而子进程依然读取fork时整个父进程的内存快照。
2 内存管理
Redis主要通过控制内存上限和回收策略实现内存管理, 本节将围绕这两个方面来介绍Redis如何管理内存。
2.1 设置内存上限
Redis使用maxmemory参数限制最大可用内存。 限制内存的目的主要有:
·用于缓存场景, 当超出内存上限maxmemory时使用LRU等删除策略释放空间。
·防止所用内存超过服务器物理内存。
2.3 内存回收策略
Redis的内存回收机制主要体现在以下两个方面:
·删除到达过期时间的键对象。
·内存使用达到maxmemory上限时触发内存溢出控制策略。
1.删除过期键对象
Redis所有的键都可以设置过期属性, 内部保存在过期字典中。 由于进程内保存大量的键, 维护每个键精准的过期删除机制会导致消耗大量的
CPU, 对于单线程的Redis来说成本过高, 因此Redis采用惰性删除和定时任务删除机制实现过期键的内存回收。
·惰性删除: 惰性删除用于当客户端读取带有超时属性的键时, 如果已经超过键设置的过期时间, 会执行删除操作并返回空, 这种策略是出于节省
CPU成本考虑, 不需要单独维护TTL链表来处理过期键的删除。 但是单独用这种方式存在内存泄露的问题, 当过期键一直没有访问将无法得到及时删除, 从而导致内存不能及时释放。 正因为如此, Redis还提供另一种定时任务删除机制作为惰性删除的补充。
·定时任务删除: Redis内部维护一个定时任务, 默认每秒运行10次(通过配置hz控制) 。 定时任务中删除过期键逻辑采用了自适应算法, 根据键的过期比例、 使用快慢两种速率模式回收键, 流程如图8-4所示。
2.内存溢出控制策略
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制, Redis支持6种策略, 如下所示:
1) noeviction: 默认策略, 不会删除任何数据, 拒绝所有写入操作并返回客户端错误信息(error) OOM command not allowed when used memory, 此时Redis只响应读操作。
2) volatile-lru: 根据LRU算法删除设置了超时属性(expire) 的键, 直到腾出足够空间为止。 如果没有可删除的键对象, 回退到noeviction策略。
3) allkeys-lru: 根据LRU算法删除键, 不管数据有没有设置超时属性,直到腾出足够空间为止。
4) allkeys-random: 随机删除所有键, 直到腾出足够空间为止。
5) volatile-random: 随机删除过期键, 直到腾出足够空间为止。
6) volatile-ttl: 根据键值对象的ttl属性, 删除最近将要过期数据。 如果没有, 回退到noeviction策略。
内存溢出控制策略可以采用config set maxmemory-policy{policy}动态配置。
频繁执行回收内存成本很高, 主要包括查找可回收键和删除键的开销, 如果当前Redis有从节点, 回收内存操作对应的删除命
令会同步到从节点, 导致写放大的问题, 如图8-5所示
3 内存优化
Redis所有的数据都在内存中, 而内存又是非常宝贵的资源。 如何优化内存的使用一直是Redis用户非常关注的问题。
3.1 redisObject对象
Redis存储的所有值对象在内部定义为redisObject结构体, 内部结构如图8-6所示
高并发写入场景中, 在条件允许的情况下, 建议字符串长度控制在39字节以内, 减少创建redisObject内存分配次数, 从而提高性能
3.2 缩减键值对象
降低Redis内存使用最直接的方式就是缩减键(key) 和值(value) 的长度。
3.3 共享对象池
共享对象池是指Redis内部维护[0-9999]的整数对象池。
共享对象池与maxmemory+LRU策略冲突, 使用时需要注意。 对于ziplist编码的值对象, 即使内部数据为整数也无法使用共享对象池, 因为ziplist使用压缩且内存连续的结构, 对象共享判断成本过高。
3.4 字符串优化
字符串重构: 指不一定把每份数据作为字符串整体存储, 像json这样的
数据可以使用hash结构, 使用二级结构存储也能帮我们节省内存。 同时可以
使用hmget、 hmset命令支持字段的部分读取修改, 而不用每次整体存取。 例
如下面的json数据:
{
"vid": "413368768",
"title": "搜狐屌丝男士",
"videoAlbumPic":"http://photocdn.sohu.com/60160518/vrsa_ver8400079_ae433_pic26.jpg",
"pid": "6494271",
"type": "1024",
"playlist": "6494271",
"playTime": "468"
}
3.5 编码优化
1.了解编码
Redis对外提供了string、 list、 hash、 set、 zet等类型, 但是Redis内部针对不同类型存在编码的概念, 所谓编码就是具体使用哪种底层数据结构来实现。 编码不同将直接影响数据的内存占用和读写效率。 使用object encoding {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) 可以采用至少两种编码方式来实现, 表8-5表示type和encoding的对应关系。
ziplist压缩编码的性能表现跟值长度和元素个数密切相关, 正因为如此Redis提供了{type}-max-ziplist-value和{type}-max-ziplist-entries相关参数来做
控制ziplist编码转换。 最后再次强调使用ziplist压缩编码的原则: 追求空间和时间的平衡。
针对性能要求较高的场景使用ziplist, 建议长度不要超过1000, 每个元素大小控制在512字节以内。
4.intset编码
intset编码是集合(set) 类型编码的一种, 内部表现为存储有序、 不重复的整数集。 当集合只包含整数且长度不超过set-max-intset-entries配置时被启用。
备注:文章参考《Redis开发与运维》,作者:付磊,张益军