Redis 内存碎片率 1.6 要不要重启?别急着动,我的判断流程和 activedefrag 参数清单
先说结论:mem_fragmentation_ratio > 1.5 只是个报警信号,不是重启指令。我见过 1.8 但延迟 P99 还在 3ms 以内的实例,也见过 1.2 但 evicted_keys 每小时涨 2 万的实例。前者乱重启反而把可用性搞崩,后者才是真着火。Redis 版本不同,碎片表现差很多,4.0 以前基本只能重启,6.0 以后有 jemalloc-bg-thread、activedefrag、lazyfree 可以组合拳。下面的命令和参数我都按 7.2.4 和 6.2.14 实测过,云 Redis 可能屏蔽部分 CONFIG SET,以控制台参数为准。
一、先别猜,把 INFO MEMORY 和 MEMORY STATS 拉出来
我一般先跑两条:redis-cli -h 127.0.0.1 -p 6379 info memory 和 redis-cli -h 127.0.0.1 -p 6379 memory stats。重点不是只看 mem_fragmentation_ratio,而是把 used_memory、used_memory_rss、mem_fragmentation_bytes、allocator_frag_ratio、rss_overhead_ratio 一起看。比如 used_memory=18.2G、used_memory_rss=29.1G、mem_fragmentation_ratio=1.60,同时 allocator_frag_ratio=1.12、rss_overhead_ratio=1.01,那说明 jemalloc 内部还好,RSS 高更多是分配器预留和操作系统页。要是 allocator_frag_ratio=1.45,那才是 jemalloc 层面的碎片,activedefrag 更可能有效。mem_fragmentation_ratio < 1 反而危险,通常意味着部分内存被 swap 出去了,赶紧看 vmstat 1 的 si/so。
还有几个字段别漏:evicted_keys、expired_keys、keyspace_hits、keyspace_misses、latest_fork_usec、total_forks。如果 evicted_keys 持续增加,说明已经触碰 maxmemory,这时候碎片率高会提前触发淘汰。如果 latest_fork_usec 从 20000 涨到 800000,说明 fork 变慢,可能跟内存页表和碎片有关。INFO COMMANDSTATS 可以看 del、expire、unlink 调用量。SLOWLOG GET 128 看有没有 DEL 大 key 导致的慢查询。LATENCY LATEST 看事件。redis-cli --bigkeys 和 redis-cli --memkeys 采样大 key,但生产环境别在高峰期跑,--memkeys 会 MEMORY USAGE 每个 key,QPS 高时很伤。
二、activedefrag 不是万能药,参数要按实例调
Redis 4.0 引入 activedefrag,默认是 no。开启:redis-cli config set activedefrag yes。但只开这个不够,我常用这套参数,7.2.4 上验证过:active-defrag-ignore-bytes 100mb、active-defrag-threshold-lower 10、active-defrag-threshold-upper 100、active-defrag-cycle-min 1、active-defrag-cycle-max 25、active-defrag-max-scan-fields 1000。含义是碎片超过 100MB 才启动,碎片率低于 10% 不整理,高于 100% 用最大 25% CPU 周期整理,每次扫描最多 1000 个字段。如果实例 QPS 只有 2k,可以把 cycle-max 拉到 40;如果 QPS 8k、P99 敏感,就压到 10。开了以后看 INFO MEMORY 里的 active_defrag_running,1 表示正在跑。还可以看 allocator_frag_ratio 是否从 1.4 往 1.2 降。
但有两个坑。第一,activedefrag 主要整理 jemalloc 的内存页,对 used_memory 和 used_memory_rss 的差值不一定马上降,我遇到过跑了 6 小时才从 1.61 降到 1.33。第二,如果 Redis 编译时用的不是 jemalloc,比如 MALLOC=libc,activedefrag 效果会差很多。用 redis-cli info server | grep mem_allocator 看,如果是 libc,别指望它。还有 jemalloc-bg-thread,Redis 6.0 后默认 yes,但有些云厂商会关。config get jemalloc-bg-thread 如果是 no,可以试 config set jemalloc-bg-thread yes,它让 jemalloc 后台线程回收脏页,对碎片率有 5%-15% 的改善,不是立竿见影。
三、lazyfree 和淘汰策略,比碎片率更该先查
很多碎片其实是删除和过期造成的。Redis 4.0 有 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del、lazyfree-lazy-user-del、lazyfree-lazy-user-flush。我负责的一个推荐服务,每天 0 点批量过期 300 万 key,expired_keys 每秒 3 万,lazyfree-lazy-expire 是 no,主线程同步释放内存,导致 used_memory_rss 不降、碎片率从 1.25 冲到 1.72。改成 yes 后,第二天同一时间碎片率只到 1.38。命令是 config set lazyfree-lazy-expire yes,写入 redis.conf 才持久。如果业务用 DEL 删大 key,改成 UNLINK,它是异步删除,记得 lazyfree-lazy-user-del 在 7.0 后才有类似行为,老版本还是 UNLINK 更稳。
淘汰策略也要看。maxmemory-policy 用 allkeys-lru 还是 volatile-lru,对碎片影响不一样。allkeys-lru 会淘汰所有 key,可能把热 key 淘汰掉,导致缓存命中率下降,回源压力大。volatile-lru 只淘汰设置了 expire 的 key,如果大量 key 没 TTL,可能直接 OOM。我一般用 allkeys-lfu 配合 maxmemory-samples 10,7.2 上命中率比 allkeys-lru 高 3%-7%,但 LFU 计数器会占内存,lfu-log-factor 10、lfu-decay-time 1 要调。maxmemory 别设成物理内存的 100%,留 20%-30% 给 RDB fork、AOF 重写、复制缓冲区。32G 实例我通常设 maxmemory 24gb,repl-backlog-size 64mb,client-output-buffer-limit replica 256mb 64mb 60。这些参数和碎片率一起看,才能判断要不要动。
四、真要重启,别直接 kill,用主从切换或迁移
如果 activedefrag 跑了 12 小时,allocator_frag_ratio 还在 1.5 以上,evicted_keys 每分钟增加,P99 从 5ms 涨到 30ms,那就别硬扛。重启方案有三种,对比一下:直接重启单节点,简单但停机 30 秒到 5 分钟,取决于 RDB 大小和加载速度;主从切换,先重启从节点,再 REPLICAOF 提升,几乎不停机,但需要至少一主一从,且从节点内存要够;用 Redis Shake 或 MIGRATE 迁到新实例,适合跨版本升级,但 MIGRATE 会阻塞,大 key 可能卡死,Redis Shake 支持批量、限速,我一般设 pipeline=100、qps=20000、replace=true。云 Redis 通常有“主从切换”按钮,切换前确认从节点 master_link_status:up、master_last_io_seconds_ago 小于 10、slave_repl_offset 与主节点差值小于 1MB。
我的独立观点是:碎片率不是 KPI,内存成本和延迟才是。如果多花 8G 内存能换来 P99 稳定,我宁愿先扩内存,而不是赌一次重启。因为重启后如果 maxmemory 和 lazyfree 没调,碎片率还会回来。我的处理顺序是:先看 evicted_keys 和 latency,再开 activedefrag 和 jemalloc-bg-thread,然后调 lazyfree 和淘汰策略,最后才是扩内存或主从切换重启。每次改一个参数,观察 30 分钟,记录 mem_fragmentation_ratio、allocator_frag_ratio、used_memory_rss、P99。别一次改五个,出了问题不知道是谁的锅。Redis 不是玄学,参数背后都是内存分配和操作系统的账。