内存分析

通常我们可以通过执行info memory来查看Redis的内存指标,通过分析指标来了解Redis的内存使用情况,其中的指标说明如下

  • used_memory:Redis分配的内存总量,也就是所有数据占用的内存(Byte)
  • used_memory_human:used_memory换算成MB输出
  • used_memory_rss:从操作系统角度显示Redis进程占用的物理内存
  • used_memory_peak:内存使用的最大值,表示used_memory的占用峰值
  • used_memory_lua:Lua引擎消耗的内存大小
  • mem_fragmentation_ratio:used_memory_rss/used_memory的比值,表示内存碎片率
  • mem_allocator:Redis使用的内存分配器,默认为jemalloc

其中需要重点关注used_memory和mem_fragmentation_ratio,当mem_fragmentation_ratio大于1时,说明used_memory_rss - used_memory多出的部分内存没有用于数据存储,而是被碎片占用,如果两者相差很大说明碎片较为严重;当mem_fragmentation_ratio小于1时,这种情况一般出现在操作系统把Redis内存交换到硬盘导致,这种情况需要多关注。

Redis内存消耗主要包括自身内存+对象内存+缓冲内存+碎片。其中对象内存是内存占用最大的一块,存储了所有的用户数据,由于Redis采用键值对,每次在创建时都需要创建key和value两个对象,占用的内存可以简单理解为size(key)+size(value),应该避免使用过长的键。

缓冲内存

缓冲内存主要包括客户端缓冲、复制积压缓冲区、AOF缓冲区。客户端缓冲指的是所有接入到Redis服务器TCP连接的输入输出缓冲,输入缓冲无法控制,最大为1G,如果超过则断开连接。输出缓冲通过参数client-output-buffer-limit控制。

  • 普通客户端:除了复制和订阅的客户端之外的所有连接,Redis的默认配置是client-output-buffer-limit normal 0 0 0,Redis默认对普通客户端的输出缓冲区进行限制,一般情况下,可以忽略不计,但是当有大量慢连接客户端就会造成内存大量消耗,可以设置maxclients做限制。
  • 从客户端:主节点会为每个slave节点建立一条连接用于复制,默认配置是client-output-buffer-limit slave 256MB 64MB 60。当主从节点之间网络延迟较高或者主节点有大量从节点时,内存消耗将会占用很多,建议从客户端不要超过两个。
  • 订阅客户端:当使用发布订阅功能时,连接客户端使用单独的输出缓冲区,默认配置为client-output-buffer-limit pubsub 32MB 8MB 60,当订阅服务的消息生产快于消费速度时,输出缓冲区会产生积压造成输出缓冲区空间溢出。

复制积压缓冲区在Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能,由参数repl-backlog-size参数控制,默认1MB。对于复制积压缓冲区一个master只有一个,所有slave共享此缓冲区,建议将其调大,能够在一定程度上避免全量复制。

当开启了AOF持久化,AOF缓冲区可以在重写期间保存写入的命令,缓冲区大小无法控制,其内存使用量基于AOF重写时间和写入的量。

内存碎片

Redis默认使用jemalloc的内存分配器,可选的还有glibc、tcmalloc。内存分配器可以更好的管理和复用内存,分配内存策略一般采用固定范围的内存块进行分配。当保存5KB的对象时,jemalloc可能会采用8KB的块存储,剩下的3KB无法分配给其它对象,就成为了内存碎片。频繁做更新操作或者大批量过期键删除时,空间无法充分利用都会导致碎片率上升。

子进程消耗

在Redis fork子进程时,由于Linux的写时复制技术(copy-on-write),父进程会和子进程共享相同的内存页,当父进程处理写操作时会对需要修改的页复制出一份副本完成写操作,而子进程依旧读取fork时的整个父进程内存快照。

Linux在2.6.38内核中加入了THP透明大页机制,虽然开启THP能降低fork子进程的速度,但是它会将复制页的单位从4K加到2MB,如果父进程有大量写操作,会造成内存过度消耗,建议关闭THP。

内存管理

设置内存上限

Redis使用maxmemory参数限制最大可用内存,防止Redis内存超过物理内存。其中需要注意的是maxmemory限制的是used_memory统计的内存,由于内存碎片的存在,实际使用的内存大小会比maxmemory要大,应该注意这方面的内存溢出。

内存回收策略

Redis的内存回收机制主要包括过期键删除和内存使用到maxmemory上限触发内存溢出控制策略。

Redis键的过期时间存储在字典中,由于内存中保存大量的键,维护每个键精准的过期删除机制会消耗大量CPU,因此Redis采用惰性删除和定时任务删除机制实现过期键的内存回收。

  • 惰性删除:惰性删除在读取带有过期时间的键时,如果已经超过了设置的过期时间,会执行删除操作并返回空。但是当过期键一直没有访问将无法得到及时删除,内存无法及时释放
  • 定时任务删除:Redis内部维护一个定时任务,默认每秒允许10次,由参数hz参数控制。定时任务中删除过期键采用了自适应算法,根据键的过期比例,使用快慢两种速率模式回收键

定时任务的工作流程如下:

  1. 定时任务在每个数据库空间随机检查20个键,当发现过期键时删除对应的键
  2. 如果超过检查数25%的键过期,循环执行回收逻辑,直到不足25%或允许超时为止
  3. 如果之前回收键超时,则在Redis触发内部事件之前再次以快模式运行回收过期键删除任务,快模式下超时时间为1毫秒且2秒内只能执行一次
  4. 快慢两种模式内部删除逻辑相同,只是执行的超时时间不同

当Redis所用内存超过了maxmemory上限时会触发相应的溢出控制策略,策略由参数maxmemory-policy参数控制,Redis支持6种策略:

  • noeviction:默认策略,不删除任务数据,拒绝写入操作返回错误信息,只响应客户端读操作
  • volatile-lru:根据LRU算法删除设置了超时的键,直到腾出足够的空间为止,如果没有可删除的则退回noeviction策略
  • allkeys-lru:根据LRU算法删除键,直到腾出足够的空间
  • allkeys-random:随机删除键,直到腾出足够的空间
  • volatile-ttl:根据键的TTL属性,删除最近要过期的数据,没有则退回noeviction策略

通过info stats命令查看evicted_keys可以得出当前Redis服务器已剔除的键的数量。

内存优化

Redis存储的所有value值在内部定义为RedisObject结构体,了解它对内存优化能够起到帮助。其结构如下:

  • type字段:表示当前对象使用的数据类型,可以通过type命令查看对象类型
  • encoding字段:表示内部编码类型,同一个对象不同编码实现占用的内存不同
  • lru字段:记录对象最后一次访问时间,用于辅助LRU算法删除键数据。可以使用object idletime {key}在不更新lru字段的情况下查看当前键的空闲时间
  • refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,为0时表示可以安全回收当前对象空间。使用object refcount {key}获取当前对象引用,非0时可以通过共享对象的方式节省空间
  • *ptr字段:如果value是整数,直接存储数据,否则表示指向数据的指针,Redis在3.0之后对值对象是字符串且长度不超过39字节,内部编码是embstr,字符串sds和redisobject一起分配,减少内存分配次数

常见优化建议

  1. 控制键值长度:精简key的长度,对value值进行精简,序列化,压缩等手段降低内存占用空间,例如protostuff、snappy。
  2. 共享对象池:共享对象池是指Redis内部维护一个0-9999的整数对象池,创建大量整数类型的redisobject存在内存开销,每个redisobject内部至少占16字节,所以Redis维护一个整数对象池,用于节省内存。整数对象池在Redis中通过变量REDIS_SHARED_INTEGERS定义,不能通过配置修改。但其与maxmemory+LRU策略环境冲突,并且不支持ziplist编码。
  3. 字符串优化:Redis自定义实现了字符串类型,内部采用了简单动态字符串(SDS),存在预分配机制,降低内存分配次数。预分配规则在第一次创建字符串对象时不做预分配,修改后如果已有free空间不足且小于1MB,则每次预分配一倍的量;修改后如果free空间不足且大于1MB,则每次分配1MB。因此,应该尽量减少字符串频繁append,setrange,改为直接set修改字符串,避免内存浪费。
  4. 编码优化:应该尽量采用更合适的内部编码,例如长度不超过1000,元素大小不超过512字节,可以采用ziplist来平衡内存和时间。