一次生产 Redis 内存打满的排查、清理和复盘

发布时间:2026/11/6 5:15:16
一次生产 Redis 内存打满的排查、清理和复盘 前言今天收到一条监控报警生产环境的主 Redis 内存使用率直接飙到了99.98%而且监控大盘上的Evictions逐出 Key 数量开始不断往上涨。这个指标一旦报警其实挺危险的因为我们这台实例配的淘汰策略是allkeys-lru。也就是说在内存完全打满的情况下Redis 为了能继续写入新数据会开始按 LRU 策略随机淘汰旧 Key。而这台 Redis 上同时还跑着用户的 Session、接口限流计数器、以及一些业务基础缓存要是任由它这么无差别逐出核心业务数据被误杀只是时间问题。既然确认了 Redis 确实满了第一步肯定不是慌着乱删而是要先把情况摸清楚分析内存占用分布 - 找出大头 - 制定安全清理方案 - 根治隐患。记录一下这次排查过程、按空闲时间清理冷数据的脚本以及事后复盘出来的几个架构问题。配图占位CloudWatch / Datadog 监控大盘上 Redis 内存飙到 100% 且 Evictions 开始递增的图一、线上不能盲猜全库 Key 内存分布分析面对一个被打满的生产 Redis第一大忌就是凭感觉去删 key第二大忌是图省事直接敲KEYS *或者FLUSHDB前者会导致单线程直接卡死后者会把线上业务全干崩。必须先搞清楚到底是谁在吃内存。我写了个轻量级的 Python 脚本通过SCAN游标分批遍历全库采样统计每个前缀的 Key 数量和预估内存占用。当时跑出来的全库内存分布大概是这样的关键业务前缀已做脱敏Key 前缀脱敏Key 数量数量占比单 Key 均值预估占用内存业务性质prod:search_emb:*58.5 万22.8%14.5 KB~8.07 GB (87.5%)搜索 Query 向量缓存prod:img_url:*106.3 万41.4%537 B~544 MB (5.9%)图片预签名 URL 缓存service_go:prod:*18.8 万7.3%1.95 KB~350 MB (3.8%)微服务加速缓存prod:card_cache:*3.8 万1.5%2.03 KB~72.7 MB (0.8%)运营卡片缓存prod:session:*42.8 万16.7%136 B~55.5 MB (0.6%)用户 Session 状态prod:qclass:*17.1 万6.7%211 B~34.4 MB (0.4%)分类打标缓存prod:user:*/LIMITS:*~3 万1.2%100~400 B~15 MB用户信息、限流等核心业务看到这个表问题就很清楚了整个实例可用内存9.2 GB左右prod:search_emb:*这一个前缀就占了8.07 GB87.5%。Session、限流这些核心业务加起来只占很小一部分是被这一个前缀挤掉的。用于分析全库分布的非阻塞脚本如下importredis rredis.Redis(hostyour-redis-host,port6379,socket_timeout10)cursor0stats{}whileTrue:cursor,keysr.scan(cursorcursor,count5000)forkinkeys:k_strk.decode(utf-8,errorsreplace)partsk_str.split(:)prefixparts[0]:parts[1]iflen(parts)2elseparts[0]ifprefixnotinstats:stats[prefix]{count:0,sample_bytes:0,sample_cnt:0}stats[prefix][count]1ifstats[prefix][sample_cnt]50:mr.memory_usage(k)ifm:stats[prefix][sample_bytes]m stats[prefix][sample_cnt]1ifcursor0:breaktotal_keyssum(s[count]forsinstats.values())print(fTotal keys:{total_keys})forp,sinsorted(stats.items(),keylambdax:x[1][count]*(x[1][sample_bytes]/max(1,x[1][sample_cnt])),reverseTrue):avg_ms[sample_bytes]/max(1,s[sample_cnt])est_mb(s[count]*avg_m)/(1024*1024)print(f{p:30}|{s[count]:8}|{avg_m:8.1f}B |{est_mb:10.2f}MB)二、翻看代码这个大头能不能清理定位到前缀后立刻去代码库里翻对应的实现。发现这是在做语义搜索时为了加速对用户搜索词Query调用大模型LLM计算出的 1024 维 Embedding 向量。代码里的逻辑是标准的Read-Through旁路读穿模式用户输入搜索词先去 Redis 查这个 Query 的 embedding 在不在如果命中Cache Hit直接拿存好的向量去向量库做检索如果未命中Cache Miss调用大模型 API 生成 1024 维向量然后写回 Redis并设置了一个3 天ex3*86400的 TTL。这就解释了为什么会有 58 万个 key在最近几天的搜索量下用户搜索的词各不相同3 天的积累硬生生堆出了 8GB 的向量数据。能不能清怎么清从业务逻辑上看它本身就是个加速缓存清掉不会引起任何数据不一致或业务报错——因为只要缓存没有代码就会自动去 LLM 回源重新生成。但能不能全部删掉不行。如果一口气把 58 万个向量全抹掉线上正在跑着搜索请求瞬间全量 Cache Miss 会导致所有并发搜索全部涌向底层的 LLM 模型 API瞬间把下游的大模型接口打出限流或者超时反而引发更大的次生故障。所以最好的解法是既要把内存降下来又不能全部清空优先清理那些最冷门、很久没被访问的数据保留热点缓存。三、精细化清理按命中率Idle Time精准清理最冷 50%那么问题来了Redis 里怎么判断哪些向量缓存是“很久没被命中过的冷数据”答案是利用 Redis 原生的OBJECT IDLETIME key。在allkeys-lru模式下Redis 会给每个 key 记录一个 LRU 时钟。通过OBJECT IDLETIME可以精确查出这个 key 距离上一次被读取/写入已经过去了多少秒这个查询只读时钟完全不会改变 key 的访问时间。我们先用 Pipeline 跑了一下全量 58.5 万个向量 key 的空闲时间分布空闲时间距上次命中Key 数量占比决策 2 天48小时以上无人访问14.9 万25.59%准备清理极冷数据1 ~ 2 天24~48小时无人访问14.4 万24.72%准备清理冷数据12 ~ 24 小时11.8 万20.28%保留温数据6 ~ 12 小时6.9 万11.82%保留活跃数据1 ~ 6 小时8.0 万13.73%保留高频热数据 1 小时2.2 万3.86%保留核心热数据数据很清楚空闲时间 24 小时以上一天以上没人搜过的词占了 50.3%29.4 万个。这 29.4 万个 key 大概率是一些冷门长尾搜索词留着除了吃内存毫无收益。而剩下的 49.7% 都是在过去 24 小时内有命中记录的热门词。执行安全异步清理为了保证对生产业务 0 影响我们使用SCAN游标 Pipeline批量判断 UNLINK后台线程异步释放内存不阻塞主线程每批 1000 个批次间休眠 10msimportredisimporttime rredis.Redis(hostyour-redis-host,port6379,socket_timeout10)# 阈值24 小时以上未被命中的冷数据IDLE_THRESHOLD86400cursor0total_scanned0total_unlinked0batch_to_delete[]print(f清理前内存:{r.info(memory).get(used_memory_human)})whileTrue:cursor,keysr.scan(cursorcursor,matchprod:search_emb:*,count2000)ifkeys:piper.pipeline(transactionFalse)forkinkeys:pipe.object(idletime,k)idlespipe.execute()fork,itinzip(keys,idles):total_scanned1ifitisnotNoneanditIDLE_THRESHOLD:batch_to_delete.append(k)# 每凑齐 1000 个调用一次异步 UNLINKiflen(batch_to_delete)1000:r.unlink(*batch_to_delete)total_unlinkedlen(batch_to_delete)batch_to_delete[]time.sleep(0.01)# 短暂停顿避免主线程抖动ifcursor0:breakifbatch_to_delete:r.unlink(*batch_to_delete)total_unlinkedlen(batch_to_delete)time.sleep(2)print(f清理完成! 共扫描{total_scanned}个异步清理冷 Key{total_unlinked}个)print(f清理后内存:{r.info(memory).get(used_memory_human)})清理效果脚本跑了大约 20 秒扫描 585,152 个 Key精准删除了294,350 个冷 Key占比 50.30%Redis 内存从9.19 GB 降到 5.22 GB释放了4.0 GB内存水位从99.98% 降回 56.5%Evictions归零Session 等核心业务的报警解除下游 LLM 接口没有出现流量冲击。配图占位清理完成后内存使用率降回 56% 且业务恢复平稳的监控大盘图四、事后复盘暴露出哪些架构问题虽然问题在十几分钟内解决了但这次内存打满暴露了架构上的几个问题1. 序列化方式太浪费JSON 字符串 vs 二进制 pack去看了代码才发现这个 1024 维的 float 向量写入 Redis 时直接调了json.dumps(vector)。一个 1024 维的浮点数组转成 JSON 字符串后每个浮点数带一大堆小数位加上逗号括号单个 key 占了14.5 KB ~ 21 KB但实际上1024 个float32用 Python 的struct.pack(1024f, *vector)压成二进制只有固定的4096 字节4 KB哪怕为了兼容做个 Base64 也就 5.5 KB。如果一开始就用二进制 pack同样的 key 数量内存直接能砍掉62%原本 8GB 的数据直接能降到 3GB 以下。2. 大体积加速缓存与核心业务共用实例未做物理隔离这台 Redis 实例目前混放了两类东西既承担着搜索向量、URL 预签名等大体积、允许丢失、可重新计算的旁路加速缓存又承担着 Session、用户信息、接口 Rate Limit 限流等小体积、低延迟、绝不能丢的核心业务。一旦某个旁路缓存逻辑上线或者流量暴涨就会立刻侵占整个实例的内存导致核心业务的 key 被 LRU 驱逐。这两类应该物理拆开搜索/算法等高消耗缓存和核心业务 Redis分到不同实例。3. 实例规格本身偏小当前这台主实例选型是cache.r4.large总物理内存 12GB扣除 25% 预留内存后 Redis 可用只有 9.2GB。相比之下我们的推荐系统等重度计算服务的 Redis 实例普遍都用到了 64GB 规格。随着搜索和 AI 功能用得越来越多9.2GB 已经跟不上了后续要升配或者迁移。总结线上排查高危组件的故障节奏往往是先用监控指标快速定性 - 用只读/低开销手段定位根因 - 权衡业务风险制定最小代价的止血方案 - 最后从代码和架构层面堵死漏洞。这次按 LRU 空闲时间切一刀把内存压回了安全线也尽量保住了命中率。更重要的是复盘出了序列化、实例隔离和容量规划这几个要补的地方。原文地址https://www.someget.cn/middleware/2026/08/21/redis-out-of-memory-cleanup-and-reflection.html

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询