资讯动态

Redis maxmemory打满导致OOM报错?排查思路与解决实战

发布时间:2026/10/3 6:51:39 来源:尧图企业网站定制
下午两点多群里突然有人喊“线上Redis报错了”紧接着运维截图贴上来一串醒目的英文OOM command not allowed when used memory maxmemory。这种报错我见过太多次了原因其实就一句话Redis的内存占用量达到了maxmemory配置的上限而当时设置的淘汰策略又没法把内存降下来于是所有写命令直接被拒绝执行。这篇文章就把这个经典问题完整梳理一遍maxmemory到底怎么算、内存被谁吃掉了、有哪些治本和治标的手段、以及我在实际救火过程中的完整排查路径。不管是刚接触Redis的新手还是已经被线上告警折磨过的老运维都能从里面找到能直接上手的方案。1. 先搞懂 maxmemory 到底是什么1.1 maxmemory 和“Redis 能用多少内存”不是一回事maxmemory是Redis配置里的一个硬性上限参数单位可以是gb、mb、kb也可以直接写字节数。当used_memory达到这个值之后Redis会根据maxmemory-policy配置的策略来决定下一步动作要么从现有key中挑选一些淘汰掉要么直接拒绝写入命令并返回OOM错误。很多人的第一个误区就在这里以为设置了maxmemoryRedis就只会占用这么多内存。实际上不是。maxmemory只是Redis内部数据内存的上限它管不到内存碎片、主从复制积压缓冲区、客户端输出缓冲区、AOF重写缓冲区这些额外开销。换句话说你给Redis设了8GB的maxmemory但它在操作系统里看到的常驻内存(RSS)可能已经到了10GB甚至更多这两者的差距就是上面说的那些额外部分在“吃”内存。用生活里的例子来理解maxmemory相当于宿舍的限电功率你设了1000W但不代表整个宿舍就只有1000W在跑。空调、饮水机这些“额外负载”并不计入限电额度内可一旦总功率过载照样会跳闸。1.2 内存为什么必须设上限有些刚接触Redis的同学会问既然maxmemory会导致写入失败那我把它设成0或者干脆不设行不行先澄清一个概念maxmemory 0在64位系统上表示不限制这也是默认状态。不限制意味着Redis可以一直吃内存直到操作系统的物理内存被耗尽。这时候问题就不只是Redis自己报错了而是操作系统OOM Killer会跳出来随机挑进程杀掉。更可怕的是如果内存耗尽导致系统疯狂使用swap整个服务器的响应速度都会断崖式下跌其他业务全部被拖下水。所以设maxmemory本质上是在“主动控制失效方式”与其被操作系统强行杀掉不如让Redis自己定义一个优雅的失败边界——要么淘汰不重要的数据要么明确告诉业务方“我装不下了”。这一点对生产环境来说非常关键。另外还要知道Redis的检查时机它不是在后台线程自动检查的而是在每次执行写命令之前同步检查。所以内存并不是“瞬间打满”的时候才报错而是写入方每次尝试写入时发现“已经满了”就会拒绝。这也解释了为什么OOM报错往往在业务高峰时段集中爆发——因为那个时间段写入请求最密集。1.3 怎么正确估算 maxmemory 的值估算实际上分两个层面一个是Redis内部数据的真实占用另一个是整个系统层面的内存预算。先说Redis内部。执行info memory会看到一个关键字段used_memory它表示Redis分配器实际分配出去的内存总量包括数据本身、key、过期字典、命令缓冲等。这个值才是和maxmemory比较的那个指标。再看系统层面。假设一台服务器有32GB物理内存上面只跑一个Redis第一版配置往往有人直接写maxmemory 30gb实际上这非常危险。操作系统本身、后台进程、页面缓存都需要内存再加上Redis还有RSS和used_memory之间的碎片差距设置成30GB很可能导致整机内存紧张反而触发swap。我一般建议的做法是先预留操作系统和基础运维组件的内存通常至少4GB如果还跑监控Agent、日志采集再往上加观察线上稳定时的mem_fragmentation_ratio如果长期在1.3以上说明碎片开销偏高要给maxmemory留出更多缓冲最终maxmemory建议控制在物理内存的70%到80%之间比如32GB内存的机器Redis的maxmemory先设20GB比较稳妥而不是贴着物理内存上限去卡。配置修改有两种方式写入redis.conf里的maxmemory 20gb或者在Redis命令行执行CONFIG SET maxmemory 20gb。前者重启后生效后者即时生效、但重启后会丢失。稳妥做法是两者都做先用CONFIG SET动态调整再把配置写回文件。提示设置maxmemory之前一定要先想清楚maxmemory-policy选什么。只设上限不选策略等于默认noeviction内存满了不仅不淘汰任何数据还会直接拒写。2. 排查内存到底被谁吃掉了2.1 用 info memory 一眼看穿内存构成遇到maxmemory打满第一步永远不是急着删除数据而是先搞清楚内存结构。redis-cli info memory会输出一串指标我重点看这几个字段含义我关注的原因used_memoryRedis分配器实际分配的内存总量和maxmemory直接对比的对象used_memory_rss向操作系统申请的内存比used_memory更能反映真实占用量used_memory_dataset数据实际占用的内存判断纯业务数据占了多少used_memory_overhead数据之外的开销过大说明连接数、meta信息异常mem_fragmentation_ratioused_memory_rss / used_memory大于1.5说明碎片严重maxmemory当前配置的最高上限确认是不是真的到了阈值maxmemory_policy当前淘汰策略确认报错原因的关键字段举个例子某个实例used_memory显示8GBused_memory_rss显示11GB说明有3GB的内存碎片开销。这时候就算你把maxmemory从8GB调到9GB表面上看有的缓存空间更多了但实际数据还是只能放8GB左右碎片会继续蚕食多余的内存。碎片过高的处理办法有两个方向Redis 4.0以上可以开启activedefrag yes让后台线程主动整理碎片或者干脆在主从切换、业务低峰期重启一次Redis让内存重新规整。前者适合长期运行、不想中断业务的场景后者适合碎片率实在高得离谱的极端情况。2.2 大 key 是头号嫌疑人在所有导致used_memory暴涨的因素里大key是出现频率最高的一个。所谓大key通常指单个key对应的value特别大比如一个list里塞了几百万个元素、一个hash里挂了上百万个字段、一个string直接存了几十MB。定位大key可以用两条命令。第一条是redis-cli --bigkeys它会遍历整个实例并按照string、list、set、zset、hash分类统计出最大的几个key。第二条是MEMORY USAGE key可以精确查看某个key及它的整个value占用了多少字节。这里要特别提醒一个坑redis-cli --bigkeys是全库扫描本质上就是遍历所有key线上实例如果数据量大、流量高执行期间可能引起阻塞。更好的习惯是在业务低峰期跑或者用--i 0.1参数控制每扫描100个key之后的休眠时间比如redis-cli --bigkeys --i 0.1能明显降低对线上服务的影响。另外不能只看key本身的大小还要看并发访问频率。一个value只有几KB、但每秒被访问几万次的key虽然不太吃内存却可能拖垮CPU。所以定位问题时要同时看“大”和“热”Redis 4.0之后有redis-cli --hotkeys命令但它要求淘汰策略必须是LFU不然跑不起来。2.3 容易被忽略的内存吞噬者内存打满后很多人把所有注意力都放在业务key上却忘了一些非常容易忽略的内存开销。第一个是客户端连接。每一条客户端连接在Redis服务端都有对应的输入缓冲区、输出缓冲区。比如某次线上事故排查时我通过CLIENT LIST发现实例上有几千个连接其中不少连接从建立开始就一直挂在那里输出缓冲区积压了大量数据。Redis默认的client-output-buffer-limit normal 0 0 0表示普通客户端不限制但如果连接异常缓冲区越积越大内存就是这么悄悄吃掉的。第二个是复制积压缓冲区。如果实例配置了主从复制那么repl-backlog-size这块内存是必须留出来的默认1MB主从网络不稳定的时候会临时增大。这个值虽然不大但在内存精确卡的场景下也要算进去。第三个是发布订阅和慢日志相关的临时内存。PUBSUB模式下的订阅者如果消费速度跟不上消息产生速度Redis就需要暂存消息积压到一定程度也会推高内存。排查时我习惯先看一眼info clients里的connected_clients和blocked_clients再结合info stats里的total_net_input_bytes和total_net_output_bytes判断是否有异常连接在大量拉取数据。很多时候把一堆僵死连接清掉内存立刻就能降下来一截。3. 解决四个方向把内存压回安全线3.1 方法一设置匹配业务的淘汰策略 maxmemory-policymaxmemory-policy是Redis提供的淘汰机制策略选对了OOM报错可以在秒级内消除选错了可能变成大面积误删数据。我先把八种策略列全再说怎么选。策略含义适用场景noeviction不淘汰任何key写命令直接报OOM存储重要数据不允许数据丢失allkeys-lru从所有key中按最近最少使用淘汰纯缓存场景数据可重建volatile-lru只从设置了过期时间的key中按LRU淘汰缓存为主但有少量永久keyallkeys-lfu从所有key中按访问频率最低淘汰访问频率差异极大的场景volatile-lfu只从设置了过期时间的key中按LFU淘汰带过期时间且频率差异大的数据allkeys-random从所有key中随机淘汰数据访问无规律淘汰谁影响都不大volatile-random只从设置了过期时间的key中随机淘汰带过期时间且可随机淘汰volatile-ttl优先淘汰剩余存活时间短的key时间敏感型缓存数据实际业务中90%以上的缓存场景选allkeys-lru就够了。它简单、直观、维护成本低Redis会用一个近似LRU算法淘汰最久没有被访问过的key。如果你对“最近少用”这个定义拿不准就用LFU的变体它在Redis 4.0之后才引入对访问频率的刻画更准确。最怕的是volatile-lru搭配“所有key都没设置过期时间”。这种情况下Redis在内存满了以后发现没有任何key是“可淘汰”的于是默默退化成noeviction继续报OOM。这个问题我见过太多次了很多团队以为“设了volatile-lru就高枕无忧”结果忘了给key加TTL内存照样被打满写操作照样被拒绝。修改策略的命令是redis-cli CONFIG SET maxmemory-policy allkeys-lru另外还有两个相关参数可以顺手调一下maxmemory-samples默认5表示LRU算法采样几个key做比较。采样数越大淘汰结果越接近精确LRU但CPU开销也越高。一般不建议超过10。maxmemory-clientsRedis 7.0新增的按客户端限制内存的参数如果线上客户端连接非常杂可以按连接维度设置内存上限。注意动态修改策略后要记得CONFIG REWRITE把配置写回redis.conf不然下次重启策略就回滚了。3.2 方法二给无过期时间的 key 补票淘汰策略只是“灭火”真正治本要给数据补上生命周期。我处理过不少实例里面的key有一大半是没设过期时间的缓存数据它们永远躺在内存里直到把maxmemory耗尽。检查方式很简单redis-cli --scan --pattern * | xargs -L 100 redis-cli ttl | grep -v ^-1$ | wc -l上面这条命令统计的是有TTL的key数量反过来就能算出没设TTL的比例。如果比例偏高就要跟业务方确认这些key真的需要永久保存还是当初写代码时忘记加EXPIRE了大部分情况都是“忘记加”。比如一些缓存工具类库默认不设置过期时间业务方调用时也没细看数据就这么源源不断地堆积了下来。针对这种情况、如果数据语义上允许过期最简单粗暴的方法是先给它们补一个合理的TTL让Redis自己慢慢淘汰。还有一种情况是key确实不能设置过期时间比如存储了用户的登录态或者订单状态过期会导致业务逻辑出错。这类永久key要单独拎出来放到固定的key前缀空间里然后选择volatile-lru或volatile-ttl策略确保淘汰只落在“可过期”的数据上永久数据不会被动到。3.3 方法三宁可扩容也别硬扛当maxmemory打满的根因是“数据量真的增长太快现有实例已经装不下”时调整淘汰策略只是缓兵之计治本方案是扩容。扩容有两个方向。一个是纵向扩容也就是调大maxmemory。但这有个前提服务器物理内存必须跟得上。我见过有人把maxmemory从8GB直接调到12GB结果实例所在机器总内存只有16GB系统其他进程直接OOM。扩容之前先看free -h确认机器剩余内存够不够。一个是横向扩容也就是上Redis Cluster集群。Redis Cluster通过分片把数据分散到多个节点上每个节点承载一部分数据理论上可以线性扩展容量。注意迁移过程中涉及reshard对主从复制的带宽和节点CPU都有影响最好在业务低峰期做。另外还有一个很多人忽略的方向调整持久化策略来降低内存峰值。比如RDB快照在BGSAVE时需要fork子进程而fork瞬间会用掉额外内存写时复制机制如果实例的数据量已经很大fork一次可能导致内存瞬间翻倍。这时候如果业务能接受可以调低RDB的自动触发频率或者干脆关闭RDB、只保留AOF。3.4 方法四数据结构层面省内存这是容易被忽视但效果极好的一种方式。Redis在不同数据量下会采用不同的内部编码合理配置这些编码参数能省下大量内存。几个典型参数hash-max-ziplist-entries默认128表示hash里字段数小于等于128时用ziplist编码hash-max-ziplist-value默认64表示hash里单个字段值小于等于64字节时用ziplistlist-max-ziplist-sizequicklist的压缩节点大小set-max-intset-entriesset全是整数且数量不超过512时用intset这些编码的前提是数据量小、字段数少当超过阈值时会自动转换为hashtable编码内存占用会显著升高。如果业务能接受微小的CPU开销去换取内存节省可以适当调高这些参数。还有一个常见方案大量字符串key有相同前缀或相同结构时考虑改用hash存储。比如原来存user:10001:name、user:10001:age、user:10001:email三个独立string可以改成user:10001这个hash里的三个字段。这样不仅节省了key本身的元数据开销还能利用hash的底层编码优势。大key的拆分也一样。一个包含百万元素的list或set建议按固定数量拆成多个小key这样既能避免单key过大导致淘汰和迁移困难也能降低单次删除时的阻塞风险。4. 实战一次线上 maxmemory 满的完整救火过程4.1 事故现场复盘之前接手过一套业务系统Redis用的是单机实例maxmemory设置为16GB数据主要是用户维度的缓存和一批消息队列结构。某个星期三下午业务方反馈首页接口突然大量超时紧接着监控告警弹出“Redis写命令失败”。登录Redis执行info memory关键结果如下used_memory: 17179869184 maxmemory: 17179869184 maxmemory_policy: noeviction mem_fragmentation_ratio: 1.12used_memory和maxmemory已经完全持平说明内存确实打满了。而maxmemory_policy是noeviction意思是Redis拒绝了所有写命令只保留读命令能力。这解释了为什么接口会大量超时——写缓存、更新队列的操作全部失败业务链路上抛出了一堆异常。4.2 按顺序排查和止血第一步是止血因为noeviction策略下业务已经严重受损。我直接动态修改了淘汰策略redis-cli CONFIG SET maxmemory-policy allkeys-lru这个操作即时生效Redis开始按照LRU算法淘汰那些长期没有被访问的key写命令立刻恢复。这里要说明一下我选择allkeys-lru而不是volatile-lru原因是这台实例上的缓存数据以未设置TTL的为主用volatile策略等于没用。第二步是找到内存大头。在确认写命令恢复后我趁业务请求量稍微回落的间隙执行了大key扫描redis-cli --bigkeys --i 0.1扫描结果里出现了一个非常扎眼的数据某个业务名下的list元素数量超过800万整体占用内存超过2GB。顺着key前缀找到对应的业务方确认这是一个历史遗留的消息队列消息生产方一直往里面写消费方逻辑出了bug导致消费速度跟不上队列无限积压。第三步是安全删除这个队列。这里强调一下删除这么大的key绝对不能直接用DELRedis的单线程模型会被阻塞好几秒。正确做法是用异步删除redis-cli UNLINK 大key名UNLINK会在后台释放内存主线程不会被阻塞。命令执行后used_memory没有立刻下降这是因为内存释放是异步完成的过了几十秒再看才发现降了好几个GB。第四步是恢复策略。内存降下来之后我从业务角度重新评估这类缓存数据虽然可以接受淘汰但淘汰范围最好不要扩大到永久数据。于是确认了所有重要数据都设置了足够的TTL之后把策略从临时的allkeys-lru调整回更精准的形态配置成适合业务的组合。4.3 事后复盘整个事故从爆发到恢复大约用了四十分钟其中大部分时间花在定位大key和确认业务归属上。事后我做的第一件事是给这台实例增加了内存监控告警阈值设在maxmemory的75%和90%两个档位。75%告警意味着“需要关注”90%告警意味着“必须马上处理”而不是等到100%才发现问题。第二件事是跟业务方确认了所有还在增长的队列类key给它们设置了最大长度限制并且在消费逻辑里做了流控。第三件事是建立了一个每周执行一次的大key巡检任务用脚本在低峰期自动跑--bigkeys并输出报告避免同类问题再次出现。5. 常见问题与避坑技巧5.1 高频问题速查表现象可能原因解决方法设置了allkeys-lru还是报OOM数据增长太快Redis淘汰速度跟不上写入速度扩容、减少写入频率、给数据加TTL设置了volatile-lru仍报OOM很多key没有设置过期时间无可淘汰对象检查TTL覆盖情况改为allkeys-lru或补TTL删除大key后内存没立刻降下来DEL是异步后台释放等待一段时间再观察不要重复删除动态修改配置后重启失效没有执行CONFIG REWRITE手工重写redis.conf或执行CONFIG REWRITE内存没到maxmemory但写命令报OOM开启了maxmemory-clients限制调整maxmemory-clients或客户端连接数碎片率长期偏高大量小key频繁增删、过期开启activedefrag或低峰期重启5.2 避坑清单真·经验先说几个我踩过或者见别人踩过的坑每一个都值得单独记住。第一条不要在线上随手执行FLUSHALL或FLUSHDB来释放内存。这两个命令会清空所有数据而且老版本在清空过程中会阻塞实例。即使真的要清也要先确认数据可重建再考虑用FLUSHDB ASYNC这种异步版本。第二条KEYS *命令在生产环境千万不要碰。它会遍历所有key并返回全部结果实例数据量一大瞬间阻塞几十秒甚至几分钟一点不夸张。定位问题要用SCAN或者redis-cli --scan它们可以分批迭代不会阻塞主线程。第三条设置maxmemory时一定要连带设置maxmemory-policy。默认的noeviction会在内存打满后直接拒写对缓存型业务来说这几乎等于线上事故。正确的做法是先在测试环境验证策略效果再到生产环境动态调整。第四条删除大key优先用UNLINK不要用DEL。Redis 4.0以后提供的异步删除接口能避免主线程长时间阻塞尤其对于list、set、zset这类可能包含百万个元素的key差异是几十毫秒和几十秒的区别。第五条调整maxmemory后要同步检查实例所在机器的物理内存。之前见过一个案例maxmemory调到20GB但机器物理内存只有24GB系统其他进程不到0.5GB可用操作系统开始频繁swapRedis响应时间直接从微秒级变成了秒级。maxmemory不是越高越好它只是数据内存的上限不代表整机上限。5.3 有没有办法预测 maxmemory 什么时候会打满内存打满不是突然发生的通常有一个增长过程。如果监控里记录了过去几周used_memory的变化曲线可以做一个简单的线性外推估算出预计打满的时间点。比如当前used_memory是12GBmaxmemory是16GB最近一个月平均每天增长150MB那么大约一个月后就会触顶。这类估算不需要复杂的算法在监控系统里配置一个“增长速度”指标就行。当连续几天的内存增量超过预期就说明业务数据量在变多要么是自然增长要么是出现了类似消息积压的异常。提前发现异常增长能在大故障发生前就介入处理。另外info memory里有一个mem_fragmentation_ratio指标如果它持续上升说明小key在大量创建和过期内存碎片逐渐累积。这种情况下即使used_memory没有接近maxmemoryRSS也已经很高了同样需要关注。最直接的做法是开启activedefrag让Redis在后台自动整理碎片。6. 写到最后的一点个人经验处理过几次maxmemory打满的现场之后我最大的感悟是maxmemory并不是一个“错误配置”而是Redis给业务划定的一条安全底线。它逼迫你在上线前就要想清楚一件事——如果内存满了到底应该牺牲谁。很多团队是在内存打满之后才临时把策略调成allkeys-lru这确实能让服务立刻恢复但代价可能是某些不该被淘汰的数据被悄无声息地清除掉了等业务方发现数据缺失时已经晚了一整天。这个风险远比OOM报错本身严重得多因为OOM是显式的、能被监控发现而误淘汰是隐形的事后极难排查。所以我现在接手任何一个Redis实例第一件事就是打开redis.conf看maxmemory和maxmemory-policy这两项配置再执行一次info memory确认碎片率最后跟业务方确认数据生命周期。这套动作看似简单却能提前排除掉绝大多数内存相关隐患。希望这篇文章里的排查思路、命令技巧和避坑经验也能帮你少踩几个坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑