Redis内存突然从几个G飙到十几个G告警一条接一条这是每个用Redis的人都怕遇到的事。我接手过的线上故障里内存暴增的占比相当高而且每次原因都不太一样有时候是bigkey一次性塞进去有时候是客户端输出缓冲区被打爆还有时候是明明删了一堆key内存却不降反升。这篇文章就把我这些年排查Redis内存暴增的思路完整写出来从底层内存组成、info memory指标到bigkey、缓冲区、碎片等各个层面逐一拆解。如果你正在用Redis做缓存或存储不管是刚入门还是已经踩过不少坑这份排查思路都能直接拿去用至少能让你在下次告警来的时候不慌。1. 先搞清楚Redis内存由哪些部分组成排查才不盲目很多人一听“Redis内存暴增”第一反应就是“数据量涨了”然后开始对着key列表发呆。但在实际排查中数据量增长往往只是其中一种可能而且很多时候根本就不是主因。Redis的内存占用从来都不只是key-value本身搞清楚它到底由哪些部分组成后面的排查才有方向。1.1 Redis内存不是只有数据还有这些容易忽略的开销Redis的内存可以粗略分成五块每一块都有可能在特定场景下突然暴涨。第一块是数据内存也就是key和value本身占用的空间。这里要注意Redis存的不是裸字符串而是有SDS简单动态字符串、dictEntry、对象头、过期字典等额外元数据的管理开销。一个很小的key-value在Redis底层可能要多占几十字节的元数据。所以尽量不要存几百万个key特别短的键值对元数据占比会高得吓人。第二块是客户端缓冲区。每个连接到Redis的客户端都有输入缓冲区和输出缓冲区。输入缓冲一般是几十KB级别风险不大但输出缓冲区可以非常大。比如某个客户端执行了一个返回几十万条数据的KEYS命令、一个大成员数量的SMEMBERS命令或者慢消费的PUB/SUB订阅者结果数据积压在输出缓冲区里内存会瞬间被顶起来。第三块是复制积压缓冲区也就是从节点断线重连时用来增量同步的repl-backlog。这个缓冲区默认只有1MB但如果主节点在从节点断线期间写入量非常大积压数据超过配置大小反而会触发全量同步全量同步本身也会带来额外的内存消耗。第四块是AOF相关缓冲区。开启AOF持久化后写命令会先进入AOF缓冲区再落盘执行AOF重写时还会额外产生一个AOF重写缓冲区。如果写入量很大这部分内存也可能成为暴增的推手。第五块是内存碎片。这部分最容易被忽略因为碎片不是业务数据但确实占用着进程实际拿到的内存。频繁写入、删除、修改不同大小的valuejemalloc分配器会留下很多无法使用也无法归还的内存空洞积少成多之后used_memory_rss就会明显高于used_memory。把这几块都放进脑子里再看“内存突然暴增”就不会只是死盯数据和key量了。1.2 “突然暴增”和“缓慢上涨”的排查方向完全不同这是我在实际排查中体会最深的一点。内存“缓慢上涨”和“突然暴增”背后的机制完全不同方向搞错了排查效率会低很多。缓慢上涨常见原因就那么几个业务数据持续增长、过期key回收速度跟不上新增速度、内存碎片慢慢累积、某些缓存key没有设置TTL导致只增不减。而突然暴增往往是某个时间点发生了“瞬时事件”。比如一个定时任务一次性把几十万条数据写入Redis一个接口故障导致缓存穿透大量请求回源数据库后把结果又写回Redis一个客户端连接数突然翻了几倍一个monitor命令的输出缓冲区瞬间被打爆一次主从全量同步在同一时刻发生。排查的第一步其实不是看数据而是先判断这次暴涨属于“渐进式积累的爆发”还是“瞬时事件的冲击”。判断方法很简单看监控曲线在暴涨前有没有一个明显的持续爬升期。如果是水平直线突然抬升优先往瞬时事件方向找如果是一路向上然后突然加速就要同时考虑增长和瞬时叠加的因素。2. 第一波排查看监控大盘和info memory输出定位增长源做了这么多年故障排查我给自己定了一条规矩任何内存问题都先看两样东西一是监控图二是info memory输出。这两样能快速把问题缩小到一个具体方向之后再深入细节。2.1 先确认到底是“突然”还是“其实一直在涨”拿到告警别急着上服务器。先打开监控大盘拉出最近12小时甚至24小时的used_memory曲线看清楚涨势是什么样的。如果曲线是从某个时间点开始直线拉升那基本可以断定是瞬时事件。这时候去对齐时间点那个时刻有没有发版、有没有定时任务、有没有业务活动、有没有人手动执行过什么命令。很多时候问题就是一次发布引入了一个不合理的命令或者某个定时任务开始执行。如果曲线是缓慢爬坡最后才触达告警阈值那就要往数据增长和回收机制上查。实际排查中我会把TimeStored、info stats里的total_commands_processed也拉出来看。如果总命令数在同时段暴涨说明是请求量突增导致的如果命令数没有明显变化但内存涨了那大概率是某个命令本身携带的数据量变大了比如从数据库拉取并写入的结果集变大。2.2 info memory的几行关键输出一眼看出增长来源登录Redis实例后第一件事就是执行redis-cli info memory。别嫌输出长关键就那几行。redis-cli -h 127.0.0.1 -p 6379 info memory输出里重点看这些字段字段作用used_memoryRedis分配器分配的总内存基本等于当前所有数据加开销used_memory_rssRedis进程向操作系统申请的内存包含碎片used_memory_peak历史峰值判断当前是否已经创过新高mem_fragmentation_ratio碎片率用used_memory_rss除以used_memorymem_allocator分配器类型一般是jemallocmaxmemory_policy内存淘汰策略这个影响内存满了以后的行为我的习惯是先看used_memory和used_memory_rss的关系。如果两者相差不大说明内存确实都用在数据和开销上了如果rss比used_memory高出一大截那碎片问题优先考虑。然后看maxmemory_policy如果当前策略是noeviction内存到上限后不会淘汰任何key写入直接报错但内存本身不会自己降下来。顺便看一眼used_memory_peak如果当前值还没超过peak说明曾经也到这个量级可能不是第一次发生只是之前没告警。2.3 used_memory和used_memory_rss的差值暗藏玄机这个差值很多人不重视但它真的能直接决定排查方向。正常情况下used_memory_rss会略大于used_memory因为进程本身需要一点内存加上少量碎片碎片率在1到1.5之间都算合理。如果碎片率大于1.5甚至2以上说明大量内存被碎片吃掉了。这种情况最典型的特征是你看到业务数据量不大但RSS高得离谱top里Redis进程内存占用巨大而used_memory本身并不高。这时候不要去找什么bigkey了方向应该是碎片清理。如果碎片率小于1也就是说RSS小于used_memory这个现象更危险说明Redis部分内存被操作系统换出到交换分区swap了。一旦发生swapRedis性能会急剧恶化因为磁盘IO比内存慢几个数量级。这种情况常见于整机物理内存不足系统开始swap。搞清楚这两层关系再往下排查就不会出现“数据明明不大却总报内存告警”这种认知偏差。3. 数据层面排查bigkey、过期key和数据结构选择如果info memory显示used_memory本身就在涨RSS和它趋势一致那么问题基本就在数据层面。这一层最常见的三个元凶分别是bigkey、过期key堆积和数据结构选型不合理。3.1 bigkey是内存暴增的头号嫌疑人bigkey就是单个key的value特别大或者集合类key的成员数量特别多。比如一个String类型的value有几十MB一个Hash有上百万个field一个List存了上千万个元素。这种key平时就在那里但只要被一次性写入或者读取内存就会瞬间跳一个台阶。快速排查工具是Redis自带的--bigkeys参数redis-cli -p 6379 --bigkeys它会用scan方式遍历全库按数据类型分别统计出最大的key。但要注意这个命令有几个局限。第一它是抽样统计不是全量精确统计遍历过程中每类key采样有限第二它只能统计最外层的key有多大如果一个大key实际上是Hash结构内部某个field特别大它是看不出来的第三它只输出最大和平均不会给你完整的大小分布。所以这个命令适合快速找嫌疑不适合做精确分析。要精确定位还得用debug object或者写脚本扫描。比如用debug object看单个key的序列化长度redis-cli -p 6379 debug object user:session:10001返回里的serializedlength字段就是序列化后的长度单位字节。注意这个是序列化长度和实际内存占用有一定差距但作为相对比较足够了。真正做全库扫描我都是写个小脚本用scan游标遍历所有key对每个key执行debug object然后按serializedlength从大到小排序输出。这样能拿到全库的精确分布。后面第6部分我会给出一份可以直接用的脚本。3.2 大量过期key堆积为什么没有及时被回收另一个非常隐蔽的坑是过期key没有及时被回收。Redis删除过期key有两种方式惰性删除和定期删除。惰性删除就是访问到这个key时才去检查是否过期过期就直接删掉定期删除是后台每100ms随机抽取一批设置了过期时间的key检查并删除过期key。这两个机制配合正常情况下没问题。但极端情况下会出问题。比如你一次性给几百万个key设置了5分钟后过期这些key同时过期定期删除的扫描速度跟不上大量过期key就会滞留在内存里等下一次扫描或访问才真正释放。表现出来就是明明设置了TTL内存却降不下来。排查时看两个指标。一是info stats里的expired_keys看累计过期key数量有没有异常增加二是info keyspace看带过期TTL的key数量。有一种更常见的情况大量key根本没有设置TTL。业务代码写的时候图方便SET key value而不加EXPIRE结果这些key成了永久key。日积月累内存自然只涨不降。这种问题用scan脚本扫一遍找出所有没有TTL的key即可一般都能发现罪魁。3.3 数据结构选型埋下的内存膨胀隐患数据结构选型不对是内存暴增的慢性毒药而且平时不太容易被发现。很多团队习惯把所有缓存都存成JSON字符串一个用户信息对象、一个订单详情对象全部序列化成JSON塞进String。这本身没问题但如果这个对象频繁更新某些字段每次更新都要反序列化、修改、再序列化整个大字符串都要重写内存效率极差。同样一份用户信息用Hash存每个字段独立存储更新一个字段只改一个field内存占用通常比JSON字符串小很多尤其当字段值短的时候。但Hash也不是万能的。Redis对不同数量的field有两种编码方式数量少时用listpack内存紧凑数量超过hash-max-listpack-entries配置后会自动转成hashtable内存占用会明显增大。所以如果业务上hash的field特别多内存增长会超出预期。集合类key也一样。List、Set、ZSet在元素数量少的时候有紧凑编码超过阈值就转换。如果存储的是大量小元素内存开销会比想象中大。排查这类问题可以用redis-memory-analyzer这类工具分析内存分布或者干脆把相同业务的数据在测试环境用不同结构存一遍对比最后的内存占用差距往往能有几倍。4. 缓冲区与客户端层面的隐患排查数据层面查完了没发现明显问题那就得把视线转移到连接和缓冲区上。很多“突发”内存暴涨真正的原因在客户端而不是在Redis本身。4.1 输出缓冲区暴涨monitor、subscribe和慢查询的锅Redis输出缓冲区说白了就是Redis要发给客户端的数据先暂存在这里。正常情况下客户端消费很快缓冲区一直处于低水位。但如果客户端消费能力跟不上或者根本就没在消费这里就会积压。最典型的坑就是monitor命令。monitor会把Redis收到的所有命令实时推送给你如果某个同事开着一个monitor终端挂着不动Redis端会不断往这个客户端的输出缓冲区塞数据。redis-cli的默认行为是按行输出如果终端不消费或者消费慢缓冲区就会一直涨。我见过一次线上事故就是一个monitor进程开着没关输出缓冲区涨到几个GB。PUB/SUB也一样。如果订阅者没有及时消费消息或者把慢客户端混进订阅组消息会一直积压在输出缓冲区里。排查方法很简单执行redis-cli client list看每个客户端的omem字段这个字段表示输出缓冲区当前占用的内存字节数。正常情况下是0或者几百字节如果某个客户端omem高达几十MB甚至几百MB基本就是它了。找到之后直接CLIENT KILL杀掉异常连接内存马上就会降下来。然后去检查是不是有monitor挂起、订阅客户端是否存在消费阻塞。防患于未然建议在redis.conf里设置client-output-buffer-limitclient-output-buffer-limit normal 0 0 0 client-output-buffer-limit slave 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60normal默认是0不限制但针对自己的业务可以做约束。尤其pubsub一定要限制慢消费导致的积压是灾难级的。4.2 复制积压缓冲区与AOF重写缓冲如果Redis是主从架构还有两个隐藏的内存消耗点。一个是复制积压缓冲区。当从节点断开连接主节点会把它断线期间的写命令积压到repl-backlog里用于从节点重连后的增量同步。repl-backlog默认1MB太小的话从节点断线稍微久一点积压就被覆盖主从只能全量同步。而全量同步时主节点要生成RDB快照发送给从节点如果数据集很大这个过程中内存会明显上涨。另一个是AOF重写缓冲。执行BGREWRITEAOF时Redis会fork子进程来重写AOF文件重写期间新写入的命令会同时写入AOF重写缓冲区。如果写入量很大这个缓冲区也可能涨到不可忽视的大小。排查时看info replication和info persistence字段关注aof_rewrite_in_progress、aof_rewrite_buffer_length以及repl_backlog_active、total_net_repl_input_bytes。如果恰好是在AOF重写或者主从同步期间爆的内存元凶基本就锁定在这了。4.3 客户端连接数暴增导致内存被吃满还有个容易被低估的因素连接本身也占内存。每个客户端连接在Redis内都要维护一套状态包括输入缓冲区、输出缓冲区、连接对象等一般几KB到几十KB不等。单看不多但如果是几千、几万个连接同时涌进来内存就不可小觑。连接数暴增的常见原因是连接池配置不合理。比如应用侧配置的连接池最大数设置过大一有流量抖动就疯狂创建连接或者连接池没有设置空闲回收连接一直占着不放还有应用连接超时后无限重试导致Redis端堆积大量连接请求。排查时看info clients里的connected_clients如果从几百涨到几千上万方向就明确了。然后对client list按连接来源做一个简单统计看是不是某台应用服务器的IP连接数异常就能回溯到具体是哪个服务在疯狂建连。我一般会建议把maxclients设置为一个合理的上限同时在应用侧限制连接池大小并配置空闲连接回收。Redis这边被连接数打挂大多数时候不是Redis不行而是应用侧连接池没配好。5. 内存碎片与分配器为什么删了key内存却不降这一节我单独拿出来讲因为太多人在这一步原地打转。明明业务数据已经删了很多Redis的used_memory也降了但看top命令Redis进程内存还是占着十几个G。这种“内存黑洞”现象十有八九是碎片问题。5.1 碎片率怎么算、多少算异常碎片率就是info memory里的mem_fragmentation_ratio计算方式是mem_fragmentation_ratio used_memory_rss / used_memory这个值代表进程实际占用的物理内存和Redis分配器认为自己在用的内存两者之间的倍数关系。判断标准按我多年的运维经验1到1.5之间正常范围碎片率略高一点没关系。1.5到2之间碎片偏多需要关注如果长期处于这个区间说明写入删除模式不够稳定。大于2严重碎片化内存被白白浪费掉一大半得处理。小于1异常危险说明内存被swap性能会急剧恶化。举个例子used_memory只有5GB但used_memory_rss有12GB碎片率2.4。这意味着有7GB的内存被碎片占用真实数据只占5GB。这种情况下就算业务数据删了一半进程内存可能丝毫不动。5.2 碎片是怎么产生的jemalloc和频繁修改的关系Redis默认的内存分配器是jemalloc。jemalloc的特点是分配内存时会按大小分级管理类似于把内存分成很多固定大小的格子然后按最接近实际大小的格子分配。这个机制本身是为了减少碎片、提升性能但它有一个特点当你释放一个256字节的内存块这个内存块会回到jemalloc的256字节池子里而不是立刻还给操作系统。如果后面分配的需求都是128字节那256字节池子里的空块就永远用不上变成“永久碎片”。产生碎片的典型操作模式是反复写入和删除不同大小的value或者对大key做频繁的字段增删。比如一个旧key原来是50KB被改成1KBjemalloc里50KB的空洞就剩下了1KB的新数据放到1KB的格子里。如果这种操作大量发生碎片会以肉眼可见的速度累积。也就是说碎片的本质不是Redis泄露了内存而是内存被分配器“内部消化”了业务数据释放了但这些空洞不归操作系统管。5.3 什么时候该做memory purge确认碎片严重后处理手段主要有三种。第一种是执行memory purge命令redis-cli -p 6379 memory purge这个命令会向jemalloc申请整理内存把空余的page释放回操作系统。注意它只对jemalloc分配器有效而且执行时会有一定的CPU开销不建议在高负载时刻频繁执行。第二种是开启自动碎片整理。4.4版本后的Redis支持activedefrag配置让Redis自动在后台做碎片整理。config set activedefrag yes config set active-defrag-ignore-bytes 100mb config set active-defrag-threshold-lower 10 config set active-defrag-threshold-upper 100但这种自动整理也有代价它是通过复制现有数据到一个新内存位置然后释放旧位置实现的运行时CPU和内存都会有一定起伏。业务高峰时段慎用。第三种是直接重启Redis实例。重启后进程重新申请内存碎片全部清零这是最彻底的方法。但需要先做好持久化和数据恢复准备一般用在维护窗口期执行。注意如果用了AOF和RDB持久化重启后能恢复数据如果是纯缓存场景数据丢了也无所谓那就随便重启。我个人的建议是碎片率超过1.8且确认不是业务数据导致的大内存占用就可以安排一次维护窗口重启解决一劳永逸。6. 一次内存暴增的完整排查复盘从告警到定位再到处理前面讲了很多理论这一节拿一个实际案例完整走一遍流程。这个案例不算特殊但很典型基本覆盖了上面讲到的所有排查点。6.1 拿到告警后的标准动作某天早上9点45分监控告警某Redis实例used_memory从5GB涨到15GB触发高水位告警。我当时的处理顺序是这样的。第一步先看监控曲线。内存从9点32分开始直线拉升9点40分左右到达15GB然后趋于平稳。是典型的瞬时冲击不是缓慢上涨。第二步登录实例执行info memory。结果used_memory 15GBused_memory_rss 15.2GBmem_fragmentation_ratio约1.01说明碎片不是主因内存确实是实打实被数据占用了。第三步看info keyspace。db0的keys数量是180万和告警前差不多没有暴增。这就很有意思了key数量没涨内存却涨了10GB说明要么是现有的key变大了要么是某个大key一次性写入后被删掉但内存被别的因素占住。第四步执行redis-cli --bigkeys快速扫描。结果显示一个名叫report:monthly:stats:202406的Hash key元素数量高达381万个serializedlength约4.5GB。这个key在10分钟之前还不存在是一次定时任务写入的。第五步确认生成逻辑每天早上9点半有个报表服务跑定时任务把上个月的统计数据一次性写入Redis以Hash结构存储。今天的脏数据由于上游关联表数据膨胀生成的field数量比平时多了几十倍。6.2 用scan脚本扫描全库key的size分布当时的处理并不算完因为要确认除了这个4.5GB的大key还有没有其他隐藏的大家伙。于是用scan脚本全库扫了一遍。这里给出一个我常用的Python扫描脚本可以统计所有key的serializedlength并排序输出import redis r redis.Redis(host127.0.0.1, port6379, db0) result [] cursor 0 while True: cursor, keys r.scan(cursorcursor, count1000) for key in keys: try: size r.debug_object(key)[serializedlength] result.append((key.decode(), size)) except Exception: pass if cursor 0: break result.sort(keylambda x: x[1], reverseTrue) for key, size in result[:30]: print(f{size / 1024 / 1024:.2f} MB\t{key})这个脚本核心是scan游标遍历避免用KEYS命令阻塞主线程。debug_object就是执行debug object拿到每个key的serializedlength。最终排序输出最大的30个key。扫描结果确认除了那个4.5GB的Hash之外其他key都正常最大也就几十MB。6.3 处理方案与后续预防定位到元凶后处理很快。第一步删除临时大key。由于这个key是一次性统计结果没有其他服务在实时读取直接异步删除redis-cli -p 6379 unlink report:monthly:stats:202406用UNLINK而不是DEL是因为DEL是同步阻塞操作一个大key可能要阻塞主线程几百毫秒甚至更久UNLINK则是后台异步释放内存不阻塞。第二步内存立即释放。删除后再看info memoryused_memory回到了5GB左右。但RSS需要一定时间才完全归还给操作系统这是正常现象。第三步优化业务逻辑。和报表服务团队沟通后把统计结果改成了持久化存储Redis只保存最近几次的统计摘要每个key限制在1000个field以内超过就分片存储。同时给这类key设置过期时间哪怕任务异常也不会长期占用。第四步加监控。给Redis实例增加bigkey扫描的定时任务每周扫描一次超过50MB的key直接告警。并且针对key数量、内存增长速度、连接数这几个指标配置了多维度的告警规则防止再出现“没有明显增长却突然爆内存”的盲区。7. 常见问题速查表症状、原因与排查方向对照排查做多了你会发现很多场景是可以快速归类的。我把这些年遇到的内存暴增情况整理成一张速查表遇到问题时先对号入座能省下大量盲目探索的时间。症状特征常见原因优先排查点解决方向内存直线拉升命令数同步暴增瞬时写入量大缓存穿透或批量任务total_commands_processed、keyspace限流、回源优化、任务拆分key数量基本不变内存却涨现有key被更新变大或写入大value--bigkeys、scan脚本定位并拆分bigkey设置了TTL但内存不降过期key回收不及时大量同时过期expired_keys、keyspace ttl统计过期时间加随机抖动调大扫描频率删除大量key后RSS不降内存碎片严重mem_fragmentation_ratiomemory purge、activedefrag、重启连接数突然翻倍连接池配置不合理连接风暴info clients、client list限制maxclients、修复连接池某个客户端omem巨大monitor挂起、pub/sub慢消费client list的omem字段CLIENT KILL、设置output-buffer-limit主从同步期间内存暴涨全量同步、复制积压缓冲info replication、aof_rewrite调整repl-backlog、错峰同步used_memory_rss明显小于used_memory发生swap物理内存不足系统free、内存监控扩容物理内存、排查其他进程占用这张表我在做交流分享时经常贴出来它最大的作用是快速建立“症状到原因”的映射关系。实际排查时先看症状匹配哪一行再针对那一行做深入验证基本都能在半小时内定位问题。最后再分享一个小经验。我建议每个Redis实例从第一天起就加好内存监控不只是简单的used_memory指标还要同时监控碎片率、key数量、连接数、过期key数量、bigkey扫描结果。很多事故是可以提前预防的比如碎片率在1.5以上持续一周就安排一次重启评估bigkey扫描发现超过100MB的key就主动评估拆分方案。内存暴增这个问题真正做到事后快速定位是基本功但能做到事前提前拦截才是更值得追求的境界。