核心内容摘要
樱花树下的秘密:校花扒开让我樱花女子视频里的唯美真相
序幕周五下午的惊魂时刻那是周五下午 4 点离下班还有一个小时我正准备提交最后一行代码。
突然运维群里的报警机器人疯了。
[CRITICAL] Redis Cluster-02 Memory Usage 90% (
1GB /
5GB)[CRITICAL] Redis Cluster-02 OOM Killer invoked我心里“咯噔”一下。
这台 Redis 实例平时主要做用户 Session 和一些简单的配置缓存常年稳定在200M左右。
怎么可能在短短两小时内飙升到8G这不是简单的流量突增这是一次典型的内存“泄露”或者说是业务层面的逻辑泄露。
如果不立刻解决整个用户登录系统将全面瘫痪。
本文将带你还原这场惊心动魄的排查过程从现象分析、工具使用到源码定位揭示那个差点毁了我们周末的“罪魁祸首”。
案发现场第一轮常规体检连上 VPN通过堡垒机登录生产环境。
我的第一反应是是不是内存碎片
1 排除内存碎片Redis 的内存占用不仅仅是数据大小还包括内存分配器的开销。
如果碎片率过高操作系统看到的内存占用RSS会远大于 Redis 实际存储的数据used_memory。
我迅速敲下命令redis-cli -h
192.
x.x -p6379INFO memory返回的关键指标如下# Memoryused_memory:8152940210# 约
15 GB (Redis 认为它存的数据量)used_memory_rss:8421005120# 约
42 GB (操作系统实际分配的量)mem_fragmentation_ratio:
03分析mem_fragmentation_ratio只有
03说明内存碎片极少通常
5 才需要担心碎片问题。
used_memory实打实地达到了
15 GB。
结论这不是操作系统的问题是真的有数据把 Redis 撑爆了。
2 排除客户端缓冲区积压有时候如果消费端处理太慢Redis 的输出缓冲区Output Buffer会积压大量数据。
redis-cli -h
192.
x.x -p6379CLIENT LIST我检查了omem(Output Buffer Memory) 一列发现所有连接的占用都为 0 或很小。
结论排除客户端堆积问题。
抽丝剥茧寻找那个胖子既然是数据问题那么只有两种可能海量小 Key突然写入了千万级的微小 Key。
超级大 Key某一个或几个 Key 像黑洞一样在吞噬内存。
1 尝试一dbsize里的猫腻我先看了一下 Key 的总数
127.
0.
1:6379dbsize(integer)15203疑点出现了只有
5 万个 Key如果
5 万个 Key 占用了 8G 内存平均每个 Key 大约500KB。
这在 Redis 里绝对属于“巨型”数据了。
平时的 Session Key 只有几 KB。
这强烈暗示问题不在于 Key 的数量而在于 Key 的体积。
2 尝试二--bigkeys的扫描Redis 自带的--bigkeys参数是排查神器它会扫描整个 keyspace找到每种数据类型中最大的那个 Key。
redis-cli -h
192.
x.x -p6379--bigkeys终端飞速滚动最后输出了摘要... [Summary] Sampled 15203 keys in the keyspace! Total key length in bytes is 420123 (avg len
27.
Biggest string found user:session:9921 has 1024 bytes Biggest list found system:notification:queue has 25012 items Biggest hash found config:app:settings has 5 fields ...困惑最大的 String 只有 1KB。
最大的 List 只有
5 万个元素即便每个元素 1KB也才 25MB。
这就奇怪了--bigkeys竟然没扫出来排查盲区--bigkeys是基于采样的虽然它扫描所有 Key但如果是巨大的 Set 或 Hash它只统计元素个数不一定能精准反映内存占用。
或者这个 Key 可能刚刚过期了或者是在某些隐藏的 DB 索引里不不对。
内存依然是 8G。
说明 Key 还在。
深度取证RDB 离线分析法在线上执行DEBUG OBJECT或者遍历所有 Key 是极其危险的会阻塞主线程。
为了不影响业务虽然已经快挂了我决定采用离线分析。
1 导出 RDB利用 Redis 的 BGSAVE 功能注意如果内存快满了BGSAVE 的fork操作可能会导致 OOM需确认sysctl vm.overcommit_memory设置为 1。
redis-cli bgsave等待 dump.rdb 生成后我将其下载到本地分析服务器。
2 使用rdb-tools验尸这是一个 Python 编写的强大工具能将 RDB 解析成 CSV 或 JSON。
pipinstallrdbtools# 解析 RDB按内存大小排序取前 3 名rdb -c memory dump.rdb --bytes128-l3几分钟的漫长等待后屏幕上跳出了结果。
看到结果的那一刻我差点一口老血喷出来。
database,type,key,size_in_bytes,encoding,num_elements,len_largest_element 0,list,global:error:log:
,8102394812,quicklist,45020120,512 0,string,user:session:102,4096,raw,1024,1024 0,hash,app:config,2048,ziplist,10,200凶手找到了Key 名称global:error:log:
类型List大小
1 GB元素个数4500 万个一个用来记录“全局错误日志”的 List竟然在一天之内膨胀到了 8G
案情还原一行代码的蝴蝶效应拿到 Key 名字后我迅速在代码库中全局搜索。
终于在一个角落的GlobalExceptionHandler.java全局异常处理类里发现了这段逻辑// 伪代码示例publicvoidhandleException(Exceptione){StringlogKeyglobal:error:log:LocalDate.now().toString();StringerrorMsge.getMessage()StackTraceUtils.toString(e);// 记录错误日志到 Redis方便后台查看redisTemplate.opsForList().rightPush(logKey,errorMsg);}致命缺陷分析无限追加使用RPUSH写入 Redis List原本是想做一个简单的日志队列。
无 TTL没有设置过期时间无长度限制没有执行LTRIM这意味着 List 可以无限增长。
触发条件平时系统稳定错误很少这个 Key 也就几 MB。
但今天下午某个第三方 API 服务挂了导致系统内部抛出了海量的ConnectionTimeoutException。
死循环每次异常都触发handleException写入 Redis如果 Redis 慢了或者满了可能引发新的异常再次写入…这就是 200M 飙升到 8G 的真相一次外部服务的抖动配合一段没有边界限制的代码制造了一个吞噬内存的黑洞。
紧急救援如何安全删除 8G 的大 Key找到了凶手现在的任务是“排雷”。
绝对不能做的事直接执行DEL global:error:log:
。
为什么Redis 是单线程的。
删除一个 8G 的大 Key涉及 4500 万次内存释放操作。
这会导致 Redis 主线程阻塞几十秒甚至几分钟。
在这期间所有应用请求都会超时导致真正的服务雪崩。
1 正确方案UNLINK (Lazy Free)如果你的 Redis 版本
0请务必使用UNLINK命令。
UNLINK global:error:log:
原理UNLINK只是把这个 Key 从元数据Keyspace中摘除告诉 Redis “这个 Key 不可见了”。
真正的内存释放操作会交给后台线程Bio Thread异步执行不会阻塞主线程。
2 兼容方案Redis
0 以下如果还在用老版本只能用脚本一点点地删Scan LTRIM/LPOP。
# Python 渐进式删除脚本示例defsafe_delete_list(redis_conn,key,batch_size
:whileredis_conn.llen(key)0:# 每次只修剪 10000 个元素redis_conn.ltrim(key,batch_size,-
time.sleep(
0.
# 甚至可以睡一会让出 CPUredis_conn.delete(key)执行完UNLINK后INFO memory监控图表瞬间出现了一个漂亮的断崖式下跌。
内存回到了 200M。
6.
总结与反思如何避免下一次悲剧虽然事故解决了但这次教训必须深刻吸取。
作为架构师我们需要从规范层面堵住漏洞。
1 开发规范三原则所有 Key 必须有过期时间这是 Redis 使用的第一铁律。
特别是对于日志、缓存类数据必须设置 TTLTime To Live。
redisTemplate.expire(logKey,1,TimeUnit.DAYS);容器类数据结构必须有容量限制使用 List, Set, Hash, ZSet 时必须考虑“最大包含多少元素”。
redisTemplate.opsForList().rightPush(logKey,msg);// 每次 push 后保留最近 1000 条redisTemplate.opsForList().trim(logKey,-1000,-
;禁止在 Redis 存放巨型对象如果需要存储日志请使用 ELK (Elasticsearch, Logstash, Kibana) 或文件系统。
Redis 是内存数据库寸土寸金不是垃圾桶。
2 运维监控兜底**设置maxmemory-policy**生产环境必须设置内存淘汰策略。
例如volatile-lru或allkeys-lru。
如果设置了策略当内存满时Redis 会自动踢掉旧数据而不是直接 OOM 宕机虽然这可能会丢日志但保住了服务。
大 Key 预警利用云厂商的 Redis 分析服务或者自建脚本定期扫描在低峰期一旦发现超过 50MB 的 Key立即报警。
流量监控监控 Redis 的output_buffer和input_buffer以及网络流量。
异常的流量增长往往是大 Key 的前兆。
结语技术债总是要还的通常还是在周五下午。
那个不起眼的List因为缺乏边界控制在特定条件下变成了吞噬系统的怪兽。
这次排查经历告诉我们敬畏每一行代码特别是那些看似无害的日志逻辑。
希望这篇文章能放入你的“避坑指南”里。
如果你也遇到过类似的 Redis 奇葩问题欢迎在评论区留言分享互动环节 (Hook)你遇到过最离谱的 Redis 故障是什么开发把 100MB 的 HTML 存进了 String循环里忘了关 Redis 连接导致连接数耗尽或者是和我一样被日志撑爆了内存在评论区晒出你的“炸库”经历点赞最高的我下期专门写一篇关于 Redis 性能优化的硬核代码解析别忘了关注我