- Redis这个内存杀手坑惨了我,排查三小时发现是过期策略搞的鬼*
引言:一个深夜告警引发的"血案"
凌晨2:15,手机突然响起刺耳的告警铃声------线上Redis集群内存使用率突破95%阈值。作为值班工程师的我瞬间清醒,快速登录监控系统查看,发现一个诡异的现象:明明业务量处于低谷期,但Redis内存占用却居高不下,甚至还在缓慢增长。
接下来的三小时排查过程,犹如侦探破案般抽丝剥茧,最终发现"凶手"竟是Redis的过期键删除策略。这个看似简单的机制,在特定场景下竟能引发严重的内存问题。本文将详细还原整个排查过程,深入分析Redis的过期策略机制,并给出防范方案。
第一部分:问题现象与初步排查
1.1 异常现象描述
监控系统显示以下关键指标异常:
- 内存使用率:95.3%且持续上升
- key总量:约420万
- 连接数:正常范围
- QPS:仅为平日峰值的1/5
最奇怪的是info memory中的expired_keys指标显示过去24小时只有不到1万key过期,而这与业务特性严重不符------我们的业务场景每天应该有百万级key自动过期。
1.2 排查路线图
按照标准排查流程,我依次检查了:
- 内存分析 :
redis-cli --bigkeys未发现异常大key - 数据类型 :
type命令抽样显示95%为string类型 - TTL分布 :
ttl命令抽样显示大量key已过期(TTL=-1) - 淘汰策略 :
config get maxmemory-policy显示为volatile-lru
此时发现第一个矛盾点:大量已过期key未被清理,但淘汰策略理论上会处理这些key。
第二部分:Redis过期机制深度解析
2.1 过期键的两种删除策略
Redis采用惰性删除+定期删除的组合策略:
- 1. 惰性删除(被动删除)*
- 触发时机:访问key时检查是否过期
- 优点:CPU友好
- 缺点:内存不友好,不访问的key永远不释放
- 2. 定期删除(主动删除)*
python
# 伪代码表示定期删除流程
def activeExpireCycle():
for db in redisServer.db:
expired = 0
for key in random.sample(db.dict, 20): # 随机采样
if is_expired(key):
delete_key(key)
expired +=1
if expired > 25%: # 每次最多删除25%样本
break
- 默认每秒运行10次(每100ms一次)
- 每次随机检查20个key,删除其中已过期的
- 如果超过25%的key过期,会继续检查直到低于25%
2.3 关键配置参数
- hz:默认10,控制定期删除频率
- maxmemory-samples:LRU淘汰时的采样数量(默认5)
- 动态调整机制:当发现过期key比例高时,会自动提高删除频率
第三部分:问题根因分析
3.1 完美风暴的形成
结合我们的业务场景,问题根源逐渐清晰:
- 批量创建特性:夜间批量作业会集中写入300万临时key,TTL=2小时
- 访问模式:这些key后续几乎不会被访问
- 删除瓶颈 :
- 惰性删除几乎不生效(无访问)
- 定期删除每秒最多删除200key(10次×20key)
- 需要清理300万key → 理论最少需要4.17小时
3.2 数学验证
计算实际清理速度:
bash
200 key/s × 3600s = 720,000 key/hour
3,000,000 / 720,000 ≈ 4.17小时
这与我们观察到内存4-5小时才完全释放的现象完全吻合。
第四部分:解决方案与优化实践
4.1 短期应急方案
-
手动触发清理 :
bash# 扫描所有db并强制清理 for i in {0..15}; do redis-cli -n $i scan 0 count 100000 | xargs -L 1 redis-cli -n $i ttl | grep -v "-1" done -
临时调整hz参数 :
bashredis-cli config set hz 100 # 提高10倍清理频率
4.2 长期解决方案
-
拆分业务key:
- 高频临时key使用独立Redis实例
- 设置更小的maxmemory确保及时触发淘汰
-
调整写入模式:
python# 原写法(集中创建) for item in items: r.set(f"temp:{item.id}", data, ex=7200) # 优化写法(均匀过期) base_ttl = 7200 spread_ttl = random.randint(-600, 600) # ±10分钟随机波动 for item in items: r.set(f"temp:{item.id}", data, ex=base_ttl + spread_ttl) -
监控增强:
bash# 监控过期key堆积 redis-cli info stats | grep expired_stale_perc
4.3 配置优化建议
redis
# redis.conf关键配置
hz 50 # 提高基线频率
maxmemory-policy allkeys-lru # 确保内存不足时能清理所有key
active-expire-effort 2 # 更积极的过期清理(Redis 7.0+)
第五部分:经验总结与最佳实践
-
关键认知:
- Redis过期删除不是实时的
- 批量相同TTL的key是高风险模式
- 监控不能只看内存使用率,还需关注
expired_stale_perc等指标
-
设计原则:
- 避免集中过期:通过随机抖动分散TTL
- 冷热数据分离:不同生命周期的key使用不同实例
- 压力测试:模拟批量过期场景验证清理速度
-
高级技巧:
c/* Redis源码中的自适应算法 */ if (server.stat_expired_stale_perc > 25) { server.active_expire_effort = MIN(server.active_expire_effort+1, 10); } else { server.active_expire_effort = MAX(server.active_expire_effort-1, 1); }理解这种自适应机制有助于合理设置effort参数。
结语:从故障中学到的
这次事故让我深刻体会到:即使像Redis这样成熟的组件,在特定使用模式下也会表现出意料之外的行为。作为工程师,我们不仅要会使用工具,更需要理解其内部机制和适用边界。
最终我们通过三个层面的改进彻底解决了问题:
- 架构层面:分离临时数据到专用集群
- 代码层面:为批量key添加TTL随机抖动
- 配置层面:调优hz和active-expire-effort参数
希望本文的排查思路和解决方案能给遇到类似问题的同行带来启发。记住,在分布式系统中,没有"魔法"------每个异常现象背后都有其技术原理,只有深入理解这些原理,才能真正掌握系统。