前事不忘,后事之师,不忘国耻!

 找回密码
 用户注册
搜索
查看: 25|回复: 0

[开发应用] Redis 内存淘汰治理实战:从 OOM command not allowed 到 TTL 覆盖与 LFU 双线收敛

[复制链接]
发表于 昨天 05:51 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
一、具体的问题


凌晨两点多,缓存集群里一台主节点开始批量报错,客户端日志里刷的全是 OOM command not allowed when used memory > 'maxmemory'。值班同学第一反应是内存不够了,把 maxmemory 从 4G 拉到 6G,报错消停了二十分钟又回来;再往上加,机器内存余量已经不到 2G,谁也不敢动。更奇怪的是监控面板上 evicted_keys 一直是 0——按理说内存顶格了,总该有键被淘汰出去,结果一个都没有。命中率从 98% 掉到 87%,读请求成片穿透到数据库,DB 连接池跟着报警。

复查配置,CONFIG GET maxmemory-policy 返回的是 volatile-lru,不是默认的 noeviction——策略明明配了,为什么零淘汰?把键空间采样一遍,2 万个键里 43% 的 TTL 是 -1,也就是永不过期。volatile-lru 的候选集合里,根本看不见这批键。

三个现象凑在一起:内存顶格、零淘汰、写不进去。这不是容量问题,是淘汰机制压根没在工作。另外提醒一句集群场景:每个分片独立执行这套机制,maxmemory 是单节点粒度,只拉高报错那一台的限额,救不了全集群。

二、核心原理


先把账本看清楚。maxmemory 管住的是 used_memory(数据加内部结构),复制缓冲、AOF 缓冲这些并不计入淘汰判定,INFO memory 里的 mem_not_counted_for_evict 就是这部分开销。所以 RSS 高于 maxmemory 是常态,官方建议给主从复制留出余量,原因在于淘汰动作本身也要写进复制缓冲,计数进去会形成"越淘汰越占内存"的反馈回路。

到顶之后的行为,完全由 maxmemory-policy 决定:noeviction(默认值)对写请求直接报 OOM;allkeys-\ 在全库键里挑受害者;volatile-\ 只在带 TTL 的键里挑。分水岭在最后这个:官方文档明确写着,如果库里没有任何键设置过期时间,volatile-xxx 系列的行为等同 noeviction。本例一半的键没有 TTL,等于把淘汰策略的手绑住了——这就是"零淘汰加写报错"的病根。

还有两个容易混的点。过期删除和内存淘汰是两套机制:expired_keys 走惰性加定期删除,节奏由 hz 控制;evicted_keys 走写入路径上的腾位置。LRU 本身是近似实现,maxmemory-samples 默认采 5 个键挑一个,采样越多越接近真 LRU。LFU(4.0 起可用)用对数计数器加衰减(lfu-log-factor、lfu-decay-time)识别"最近不常用",比 LRU 更抗偶发扫描污染,但衰减太快会把低频关键键误杀。

选型不是拍脑袋:全库都是缓存副本、可以安全重建的,用 allkeys 系;同一实例里混着缓存和持久键的,用 volatile 系并保证 TTL 覆盖,或者干脆拆成两个实例。本例最终选 allkeys-lfu,正是因为补完 TTL 后全库都是可重建的缓存副本,且访问热度高度集中,LFU 能把冷键更快请出去。淘汰的触发时机也值得说清:每次写入都做一次内存检查,超限就按策略淘汰到限额以下;单条命令写入大量数据(比如一次大集合求交集存成新键)可能瞬时超限一大截,这是设计允许的,不是失控。

三、实例参考(动手步骤)


步骤 0:确认现状基线
  1. redis-cli -h 10.0.3.21 -p 6379 INFO memory
  2. redis-cli -h 10.0.3.21 -p 6379 CONFIG GET maxmemory
  3. redis-cli -h 10.0.3.21 -p 6379 CONFIG GET maxmemory-policy
  4. redis-cli -h 10.0.3.21 -p 6379 INFO stats
复制代码

重点看五个数:used_memory 离 maxmemory 多近;mem_not_counted_for_evict 占多大(复制缓冲的余量);mem_fragmentation_ratio(长期超 1.5 另案治理);evicted_keys 是否为 0;keyspace_hits 与 keyspace_misses 算出命中率。注意 CONFIG GET 一次传多个参数要 7.0 以上才支持,老版本分开敲。

步骤 1:先量 TTL 覆盖率,再动任何配置
  1. redis-cli --scan | head -20000 > /tmp/keys.txt
  2. while read k; do redis-cli TTL "$k"; done < /tmp/keys.txt | sort | uniq -c | sort -rn
复制代码

本例结果:-1(永不过期)占 43%,-2(键已不存在)占 6%,其余分布在 300 到 86400 秒之间。这一步必须在从库或低峰跑:--scan 是游标式扫描不阻塞主线程,千万别用 KEYS 全量拉。

步骤 2:分批补 TTL(治本)

对无 TTL 的键按业务语义补过期:查询缓存类 EXPIRE 3600,session 类 86400,配置类不补(原因见坑 5)。用脚本 SCAN 循环逐键 EXPIRE,限速每秒 500 键左右,边跑边盯延迟:
  1. while read k; do redis-cli EXPIRE "$k" 3600; sleep 0.002; done < /tmp/no_ttl_keys.txt
复制代码

补完再跑一次步骤 1 的采样确认覆盖率,本例从 57% 补到 99.6%。为什么限速要说这么细:EXPIRE 虽然轻量,2 万个键高峰期全速跑照样挤占主线程;本例实际 40 分钟跑完,期间 SET P99 无明显变化,值得。

步骤 3:在线切策略并落盘
  1. CONFIG SET maxmemory-policy volatile-lru
  2. CONFIG SET maxmemory-policy allkeys-lfu
  3. CONFIG REWRITE
复制代码

TTL 覆盖前用 volatile-lru 过渡(此时它已经是真淘汰了),覆盖达标后切 allkeys-lfu 作为最终策略。LFU 参数先不动(lfu-log-factor 10、lfu-decay-time 1 的默认值适合大多数缓存负载),观察一个完整周期再调。CONFIG SET 是运行时生效、重启即失,必须 CONFIG REWRITE 写回 redis.conf 或同步改配置文件。注意 REWRITE 会把当前全部运行时配置一起写回配置文件,变更窗口里别叠加其他未确认的 CONFIG SET,避免把临时调整一并固化进磁盘。

步骤 4:观察淘汰是否"活了"
  1. redis-cli --stat
复制代码

健康的信号:evicted_keys 从 0 涨到与写入节奏匹配的量级,used_memory 稳定在 maxmemory 之下,OOM 报错归零。淘汰从 0 变成正常值不是变坏了,而是机制恢复了——这一点要提前跟值班同事对齐,免得又有人拿着"淘汰暴涨"来报障。

步骤 5(可选):碎片治理

mem_fragmentation_ratio 长期高于 1.5 且 used_memory_peak 回落后 RSS 不降,开 activedefrag(前提是 jemalloc 编译支持,用 CONFIG GET activedefrag 试探),或安排低峰重启。本例碎片 1.62 降到 1.07,靠的是 defrag 加补 TTL 后的写入分布更均匀,没走重启。

治理前后对比

指标治理前治理后
OOM 报错3.6 万次/天0
evicted_keys0(机制失效)4.1 万次/天(正常淘汰)
TTL 覆盖率57%99.6%
缓存命中率87.3%99.2%
mem_fragmentation_ratio1.621.07
used_memory / maxmemory4.0G / 4.0G 顶格3.4G / 4.0G 有余量
SET P99 延迟41ms2.3ms
穿透到库的读请求12.6 万次/天0.9 万次/天
冷数据占用1.4G0.3G


四、实操检查清单



  • maxmemory 不为 0:64 位默认无限制,会一路吃到被系统 OOM killer 杀进程。
  • maxmemory-policy 与负载匹配;volatile-\* 上线前必须先确认 TTL 覆盖率,否则等于 noeviction。
  • TTL 覆盖率每周用 --scan 抽样统计一次,-1 占比超过 5% 即触发补 TTL 排查。
  • INFO memory 里盯 mem_not_counted_for_evict,给复制缓冲留足余量,别把 maxmemory 顶到物理内存极限。
  • mem_fragmentation_ratio 连续一周超 1.5 触发碎片治理流程。
  • evicted_keys 与 expired_keys 分开建监控项,混在一个告警阈值里必误判。
  • 任何 CONFIG SET 之后必须 CONFIG REWRITE 或同步改 redis.conf,写进变更 SOP。
  • 改淘汰策略前先在从库演练一晚,看淘汰量级与业务表现再上主库。
  • OOM 告警文案带上当时的 maxmemory 值,一眼分清是容量问题还是策略问题。
  • maxmemory-samples 保持默认 5,淘汰抖动明显再升到 10。
  • LFU 上线后按天抽查低频关键键(配置、字典类)是否被误杀。
  • 生产环境禁用 KEYS,采样一律 --scan。


几个容易踩的坑



  • volatile-\ 不是"温和版 allkeys-\":候选集合为空时它就是 noeviction,配了等于没配。这是本次事故的病根,也是官方文档里一句常被忽略的话。
  • maxmemory 只管 used_memory:复制缓冲、AOF 缓冲、输出缓冲不计入淘汰判定,RSS 超 maxmemory 不是 bug;但反过来说,把 maxmemory 顶到物理内存 100%,等于给复制留零余量,主从同步一抖就换出。
  • CONFIG SET 重启即失:只在线改不落盘,某次例行重启后策略悄悄回退,事故复发时排查一整晚。落盘是必须动作,不是可选项。
  • 淘汰发生在写入路径:批量灌数(全量重建缓存)会在瞬间触发密集淘汰,制造延迟尖刺;错峰灌入或加大 maxmemory-samples 缓解。
  • allkeys-lfu 会淘汰"没有过期语义"的键:配置表、路由字典这类不该被动的键,要么单独实例隔离,要么回到 volatile-\* 并保证 TTL 覆盖,二选一,没有第三条路。
  • LFU 衰减误杀:lfu-decay-time 调太小,周期性低频键(周报、月结)会被当冷数据淘汰;动衰减参数前先想清楚业务访问周期。
  • 淘汰只发生在主库,动作以 DEL 传播到从库:主库 OOM 时从库看着一切正常,从库监控不能替代主库。
  • 过期和淘汰是两个机制:expired_keys 涨说明 TTL 设计在正常工作,evicted_keys 涨说明容量在打架,语义完全不同。
  • 补 TTL 别一把梭:EXPIRE 是写命令,高峰期全速跑会推高延迟,限速分批是常识不是可选项。
  • 灰度验证要留观察窗:切 LFU 后第一周,每天对比 evicted_keys 与命中率两条曲线,命中率掉 2 个点以上先回滚 allkeys-lru,再查衰减参数。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-11-10 00:03 , Processed in 0.022034 second(s), 10 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表