内存碎片是如何形成的?

  内存碎片的形成有内因和外因两个层面的原因。
1.内因是操作系统的内存分配机制;
2.外因是Redis 的负载特征

内因:内存分配器的分配策略

  内存分配器一般是按固定大小来分配内存,而不是完全按照应用程序申请的内存空间大小给程序分配。

  Redis 可以使用 libc、jemalloc、tcmalloc 多种内存分配器来分配内存,默认使用 jemalloc。

  jemalloc 的分配策略之一,是按照一系列固定的大小划分内存空间,例如 8 字节、16 字节、32 字节、48 字节,…, 2KB、4KB、8KB 等。当程序申请的内存最接近某个固定值时,jemalloc 会给它分配相应大小的空间。

外因:Redis 的负载特征

  键值对大小不一样和删改操作会导致空间的扩容和释放。

  一方面,如果修改后的键值对变大或变小了,就需要占用额外的空间或者释放不用的空间。

  另一方面,删除的键值对就不再需要内存空间了,此时,就会把空间释放出来,形成空闲空间。

内存碎片带来的问题

  大量内存碎片的存在,会造成 Redis 的内存实际利用率变低。

如何判断是否有内存碎片?

  Redis提供了 INFO 命令用来查询内存使用的详细信息。

> info memory
# Memory
used_memory:281382720
used_memory_human:268.35M
used_memory_rss:314126336
used_memory_rss_human:299.57M
used_memory_peak:3029275880
used_memory_peak_human:2.82G
used_memory_peak_perc:9.29%
used_memory_overhead:195311654
used_memory_startup:11403664
used_memory_dataset:86071066
used_memory_dataset_perc:31.88%
used_memory_lua:94208
used_memory_lua_human:92.00K
used_memory_scripts:26120
used_memory_scripts_human:25.51K
number_of_cached_scripts:55
maxmemory:4294967296
maxmemory_human:4.00G
maxmemory_policy:volatile-lru
mem_fragmentation_ratio:1.12
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
oom_err_count:0

  mem_fragmentation_ratio 的指标,它表示的就是 Redis 当前的内存碎片率。

mem_fragmentation_ratio 大于 1 但小于 1.5 是合理的;
mem_fragmentation_ratio 大于 1.5表明内存碎片率已经超过了 50%。一般情况下,我们就需要采取一些措施来降低内存碎片率了。

mem_fragmentation_ratio=used_memory_rss/ used_memory

used_memory_rss 是操作系统实际分配给 Redis 的物理内存空间,里面就包含了碎片;
used_memory 是 Redis 为了保存数据实际申请使用的空间。

如何清理内存碎片?

1.重启Redis实例

  重启过程Redis服务提供服务、AOF/RDB恢复时间受文件大小限制。

2.执行MEMORY PURGE命令
redis提供MEMORY PURGE命令手动进行回收内存(此命令会阻塞主进程)。

> MEMORY PURGE
OK

3.Redis提供的内存碎片自动清理的方法

  从 4.0-RC3 版本以后,Redis 自身提供了一种内存碎片自动清理的方法。

  redis.conf中配置:

# yes:开启内存碎片自动清理
# activedefrag yes

# 表示内存碎片的字节数达到 100MB 时,开始清理;
# active-defrag-ignore-bytes 100mb

# 表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。
# active-defrag-threshold-lower 10

# 内存碎片超过 100%,则尽最大努力整理
# active-defrag-threshold-upper 100

#为了尽可能减少碎片清理对 Redis 正常请求处理的影响,
#自动内存碎片清理功能在执行时,还会监控清理操作占用的 CPU 时间,
#而且还设置了两个参数,分别用于控制清理操作占用的 CPU 时间比例的上、下限,
#既保证清理工作能正常进行,又避免了降低 Redis 性能。这两个参数具体如下:
# 表示自动清理过程所用 CPU 时间的比例不低于 5%,保证清理能正常开展;
# active-defrag-cycle-min 5

# 表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,
#从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高。
# active-defrag-cycle-max 75

# Maximum number of set/hash/zset/list fields that will be processed from
# the main dictionary scan
# active-defrag-max-scan-fields 1000

  操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销。因为 Redis 是单线程,在数据拷贝时,Redis 只能等着,这就导致 Redis 无法及时处理请求,性能就会降低。

清理内存碎片由那个线程中执行的?

  Redis 是基于事件驱动的,Timer事件和I/O事件会注册到主线程当中,其中内存碎片整理Timer也是在主线程当中执行的。

内存相关命令

内存相关的命令简单介绍。
执行以下命令查询那些内存命令:

> MEMORY HELP
MEMORY DOCTOR                        - 输出内存问题报告
MEMORY USAGE <key> [SAMPLES <count>] - 估计key的内存使用情况
MEMORY STATS                         - 显示内存使用情况详细信息
MEMORY PURGE                         - 要求分配器释放内存
MEMORY MALLOC-STATS                  - 显示分配器内部统计信息

MEMORY DOCTOR

MEMORY DOCTOR 会列出 Redis 服务器遇到的不同类型的内存相关问题,并提供相应的解决建议.

> MEMORY DOCTOR
Sam, I detected a few issues in this Redis instance memory implants:

 * Peak memory: In the past this instance used more than 150% the memory that is currently using. The allocator is normally not able to release memory after a peak, so you can expect to see a big fragmentation ratio, however this is actually harmless and is only due to the memory peak, and if the Redis instance Resident Set Size (RSS) is currently bigger than expected, the memory will be used as soon as you fill the Redis instance with more data. If the memory peak was only occasional and you want to try to reclaim memory, please try the MEMORY PURGE command, otherwise the only other option is to shutdown and restart the instance.

 * Many scripts: There seem to be many cached scripts in this instance (more than 42). This may be because scripts are generated and `EVAL`ed, instead of being parameterized (with KEYS and ARGV), `SCRIPT LOAD`ed and `EVALSHA`ed. Unless `SCRIPT FLUSH` is called periodically, the scripts' caches may end up consuming most of your memory.

I'm here to keep you safe, Sam. I want to help you.

Peak memory(峰值内存): 过去,此实例使用的内存超过当前使用的内存的 150%。分配器通常无法在峰值后释放内存,因此您可以期望看到较大的碎片比率,但这实际上是无害的,只是由于内存峰值,如果 Redis 实例驻留集大小 (RSS) 当前大于预期,则一旦您用更多数据填充 Redis 实例,内存就会被使用。如果内存峰值只是偶尔出现,并且您想尝试回收内存,请尝试 MEMORY PURGE命令,否则唯一的其他选项是关闭并重新启动实例。

Many scripts(过多脚本):此实例中似乎有许多缓存脚本(超过 42 个)。这可能是因为脚本是生成和“EVAL”的,而不是参数化(使用KEYS和ARGV),“脚本加载”和“EVALSHA”。除非定期调用“脚本刷新”,否则脚本的缓存最终可能会消耗大部分内存。

MEMORY USAGE

MEMORY USAGE 给出一个key和它值在RAM中占用的字节数。

> set testk 1
OK
> memory usage testk
51

MEMORY STATS

MEMORY STATS 查看服务器的内存使用情况。

> memory stats
peak.allocated #redis启动以来,allocator分配的内存峰值,单位字节;同INFO的used_memory_peak
2604763296
total.allocated #allocator 当前分配的内存总字节数;同 INFO命令used_memeory
595052416
startup.allocated #Redis启动完成消耗的内存字节数;同INFO的used_memory_startup
11403768
replication.backlog #Redis复制积压缓存区内存字节数;同INFO的repl_backlog_size
0
clients.slaves #所有副本节点内存消耗总字节数(查询输出缓冲区,连接内存消耗)
0
clients.normal #Redis所有常规客户端内存消耗总字节数(查询输出缓冲区,连接内存消耗)
207724178
aof.buffer #当前和重写AOF缓冲区内存消耗总字节数;同 INFO命令aof_buffer_length和aof_rewrite_buffer_length之和
0
lua.caches
30536
db.0
overhead.hashtable.main #每个数据库中元数据占用的额外内存字节数。(redis的db就是一张hash表,首先就是这张hash表使用的内存,每一个key-value对都有一个dictEntry来记录他们的关系,元信息便包含该db中所有dictEntry使用的内存, redis使用redisObject来描述value所对应的不同数据类型(string、list、hash、set、zset),那么redisObject占用的空间也计算在元信息中。
10823332
overhead.hashtable.expires #用于存储key的过期时间耗费的内存资源。
865120
db.1
overhead.hashtable.main
275384
overhead.hashtable.expires
77552
....
overhead.total #Redis 额外内存消耗总字节数,例如:startup.allocated, replication.backlog, clients.slaves, clients.normal, aof.buffer 以及管理keyspace使用的内部数据接口消耗的内存字节数 同INFO的used_memory_overhead
233196498
keys.count #整个redis实例key的个数
183900
keys.bytes-per-key #每个key平均字节数
3173
dataset.bytes #Redis 实例中数据占用的总字节数,计算方法total.allocated减去overhead.total
361855918
dataset.percentage #Redis 数据消耗内存占总内存的百分比
61.998931884765625
peak.percentage #当前内存消耗占峰值内存消耗的百分比
22.844778060913086
fragmentation #同 INFO的 mem_fragmentation_ratio
0.96442300081253052

MEMORY PURGE

前面内存碎片处理方法中已介绍。

MEMORY MALLOC-STATS

MEMORY MALLOC-STATS 提供内存分配情况的内部统计报表,该命令目前仅实现了jemalloc作为内存分配器的内存统计,对其他分配器暂不支持。

> MEMORY MALLOC-STATS
___ Begin jemalloc statistics ___
Version: "5.1.0-0-g61efbda7098de6fe64c362d309824864308c36d4"
Build-time option settings
  config.cache_oblivious: true
  config.debug: false
  config.fill: true
  config.lazy_lock: false
  config.malloc_conf: ""
  config.prof: false
  config.prof_libgcc: false
  config.prof_libunwind: false
  config.stats: true
  config.utrace: false
  config.xmalloc: false
Run-time option settings
  opt.abort: false
  opt.abort_conf: false
  opt.retain: true
  opt.dss: "secondary"
  opt.narenas: 384
  opt.percpu_arena: "disabled"
  opt.metadata_thp: "disabled"
  opt.background_thread: false (background_thread: false)
  opt.dirty_decay_ms: 10000 (arenas.dirty_decay_ms: 10000)
  opt.muzzy_decay_ms: 10000 (arenas.muzzy_decay_ms: 10000)
  opt.junk: "false"
  opt.zero: false
  opt.tcache: true
  opt.lg_tcache_max: 15
  opt.thp: "default"
  opt.stats_print: false
  opt.stats_print_opts: ""
Arenas: 384
Quantum size: 8
Page size: 4096
Maximum thread-cached size class: 32768
Number of bin size classes: 39
Number of thread-cache bin size classes: 44
Number of large size classes: 196
Allocated: 598452992, active: 692617216, metadata: 57253832 (n_thp 0), resident: 750653440, mapped: 764928000, retained: 2612011008
...