ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis 内存优化与性能调优:编码转换、内存碎片与慢查询的定位链路

Redis 内存优化与性能调优:编码转换、内存碎片与慢查询的定位链路 Redis 内存优化与性能调优编码转换、内存碎片与慢查询的定位链路1. 从一次线上告警说起假设你维护一个订单系统某天凌晨收到告警Redis 实例内存使用率 92%同时接口平均响应时间从 8ms 涨到了 60ms。值班同事的第一反应通常是下面几种把maxmemory调大让实例先“撑过去”重启实例重置 used_memory怀疑是流量上涨直接扩机器。这些动作可能在短时间内让监控恢复绿线但一两周后同样的问题会再次出现。原因在于它们都跳过了最关键的判断内存到底被谁占用了是真实业务数据增长还是编码方式不合理、碎片率过高、过期 Key 没被及时清理慢又是哪一种慢是单条命令本身耗时还是排队、网络、主从同步造成的本文不打算把 Redis 调优讲成一份参数清单而是先给出一条可以复述的定位链路。你可以把它理解成一次体检先看整体内存形状再判断数据结构是否用了低效编码然后检查内存碎片是否过高最后用慢查询日志锁定具体命令。整条链路串起来才是一个可复用的工程方法。为了便于后续展开先记住一个最小模型客户端命令 | v 命令执行线程 -- 读写 Redis 对象 -- 编码可能发生转换 | | | v | 内存分配 / 释放 | | v v 慢查询日志 -- 耗时超阈值 内存碎片率变化 | | v v 定位具体命令 触发淘汰策略2. 先建立整体框架一次写入会经过哪些环节Redis 的内存问题很少是单一原因导致的。它更像一个链条一次写入命令进入后先执行命令逻辑然后决定用哪种内部结构保存数据随着元素增多编码可能从紧凑结构转换为通用结构内存占用随之上升元素删除或过期后释放的内存不一定能立刻被复用于是产生碎片当内存达到上限淘汰策略开始工作如果某条命令因为数据量过大而变慢它就会被 slowlog 记录下来。把整体拆成四部分更容易理解对象与编码层Redis 为了兼顾内存和性能对同一种逻辑类型提供了多种底层编码例如列表在小数据量时用紧凑列表变大后转为快速链表。内存分配与回收层内存由分配器管理频繁增删会造成碎片碎片率过高会浪费物理内存。淘汰与过期层达到maxmemory后触发淘汰过期 Key 通过惰性删除和定期删除结合清理。可观测层INFO、MEMORY USAGE、SLOWLOG、LATENCY等命令帮助我们把现象对应到原因。这四个部分并非独立存在。编码转换会直接影响内存占用和碎片产生碎片率又会影响淘汰触发时机而淘汰和过期清理如果执行不当又可能造成延迟尖刺并进入慢查询日志。理解它们之间的连接比单独记住某个参数更重要。3. 编码转换为什么同样 100 个元素内存差好几倍3.1 一句话模型可以先把它理解成Redis 对每种数据类型都准备了两套甚至多套“收纳盒”小数据用紧凑盒省内存大数据用性能盒操作更快。超过阈值时Redis 自动把数据从紧凑盒搬到性能盒这个过程叫编码转换。3.2 触发条件与常见编码以常见的列表和哈希为例数据类型小数据编码大数据编码常见转换触发条件Listlistpack早期为 ziplistquicklist元素个数或单个元素长度超过阈值Hashlistpack早期为 ziplisthashtable字段数或字段值长度超过阈值Setintsethashtable元素非整数或数量超阈值ZSetlistpack早期为 ziplistskiplist元素数或成员长度超阈值阈值由配置项控制例如hash-max-listpack-entries、hash-max-listpack-value、list-max-listpack-size、set-max-intset-entries、zset-max-listpack-entries。不同 Redis 版本默认值和名称略有差异生产环境必须以当前版本redis.conf为准。3.3 一个可复现的实验下面用redis-cli做一个最小实验观察哈希从紧凑编码转为通用编码后内存的变化。前置条件是本地或测试环境有一个可执行redis-cli的 Redis 实例版本为 7.x。# 步骤一写入少量字段查看编码和内存redis-cli DEL demo:hash redis-cli HSET demo:hash f1 v1 f2 v2 redis-cli OBJECT ENCODING demo:hash redis-cli MEMORY USAGE demo:hash# 预期输出示例# listpack# (integer) 96# 步骤二写入大量字段再次查看foriin$(seq1500);doredis-cli HSET demo:hashfield_$ivalue_$i/dev/nulldoneredis-cli OBJECT ENCODING demo:hash redis-cli MEMORY USAGE demo:hash# 预期输出示例# hashtable# (integer) 32768关键步骤解释OBJECT ENCODING用来查看当前编码MEMORY USAGE用来查看单个 Key 占用内存。第一次输出listpack说明数据量小、仍然使用紧凑结构第二次输出hashtable说明已经发生转换。内存从几十字节涨到几万字节不完全是数据本身变多还包括哈希表结构、指针和预分配带来的额外开销。适用场景这个实验适合在测试环境验证业务 Key 的编码是否符合预期尤其适合字段数量接近阈值的场景。容易改错的地方是直接用MEMORY USAGE比较不同版本可能得到不同结果因为采样方式和版本实现会变化另外转换是单向的一旦变成hashtable即使随后删掉大部分字段编码通常也不会自动退回listpack。3.4 设计取舍紧凑编码省内存但每次修改都可能涉及整块内存搬移时间复杂度更接近 O(N)通用编码操作快但内存开销大。Redis 用阈值来平衡这两者。工程上真正要做的是判断你的业务数据是否长期停留在“接近阈值但未超过”的位置。如果一个 Hash 有 400 个字段恰好没触发 500 的默认阈值但字段值较长内存已经不小这时可以考虑拆分成多个 Key或者显式调整阈值并做压测。4. 内存碎片used_memory 和 RSS 为什么会差那么多4.1 碎片是怎么产生的内存碎片可以理解成一间仓库里堆满了大小不一的箱子很多小空隙没法再放进新的大箱子。Redis 也类似不断创建和删除不同大小的对象时分配器释放的内存块可能无法被后续请求复用于是操作系统看到的进程 RSS 远大于 Redis 自己统计的 used_memory。判断碎片程度主要看两个指标used_memoryRedis 分配器统计的已用内存used_memory_rss操作系统视角的常驻内存碎片率近似为used_memory_rss / used_memory。通常碎片率在 1.0 到 1.5 之间可以接受持续高于 1.5 且业务有明显增删波动时就需要关注。4.2 一次诊断流程示例下面是一个完整的诊断流程输入是一台内存持续高于预期的 Redis 实例步骤是读取信息、计算碎片率、查看分配器状态结果用于决定是否开启主动碎片整理。# 步骤一读取内存相关指标redis-cli INFO memory|grep-Eused_memory:|used_memory_rss:|mem_fragmentation_ratio|mem_allocator# 预期输出示例# used_memory:2147483648# used_memory_rss:3758096384# mem_fragmentation_ratio:1.75# mem_allocator:jemalloc-5.2.1# 步骤二查看碎片整理是否可用及当前状态redis-cli CONFIG GET activedefrag redis-cli MEMORY STATS|grep-Efragmentation|allocator# 步骤三评估后开启主动碎片整理生产需谨慎建议在低峰期验证redis-cli CONFIG SET activedefragyesredis-cli CONFIG SET active-defrag-ignore-bytes 100mb redis-cli CONFIG SET active-defrag-threshold-lower10关键步骤解释第一步算出碎片率约 1.75说明 RSS 比已用内存高出很多第二步确认当前没有开启主动碎片整理第三步调整参数让 Redis 在碎片超过一定比例时后台整理。active-defrag-ignore-bytes表示碎片浪费低于该值时忽略active-defrag-threshold-lower表示碎片率超过该百分比才开始整理。可观察结果整理期间used_memory_rss会逐步下降mem_fragmentation_ratio回落。边界和常见错误主动碎片整理会消耗 CPU主线程和子线程都可能受影响不建议在高负载实例上直接大范围开启另外重启实例也能消除碎片但会造成缓存雪崩风险需要配合预热方案。4.3 碎片与淘汰的关系这里最容易误解的是碎片率高不等于淘汰会提前触发。淘汰判断基于maxmemory和used_memory而不是 RSS。也就是说碎片可能让进程被操作系统 OOM Kill却不触发 Redis 自身的淘汰。生产上如果只盯着碎片率却不看操作系统内存会出现“Redis 自己觉得还有空间系统已经把进程杀掉”的情况。因此内存告警应该同时覆盖 used_memory、used_memory_rss 和操作系统剩余内存。5. 内存淘汰达到上限后谁先被牺牲当used_memory超过maxmemoryRedis 会按照maxmemory-policy的策略淘汰 Key。常见策略如下策略淘汰范围是否可能淘汰未过期 Key适用场景noeviction不淘汰写入报错不淘汰不允许丢数据的场景allkeys-lru所有 Key会纯缓存允许按最近使用淘汰allkeys-lfu所有 Key会热点差异明显希望按频率淘汰volatile-lru设置了过期时间的 Key不会混合存储仅缓存部分可淘汰volatile-lfu设置了过期时间的 Key不会有过期时间的缓存热点明显allkeys-random所有 Key会对淘汰顺序不敏感volatile-random设置了过期时间的 Key不会简单缓存场景volatile-ttl设置了过期时间的 Key不会希望优先淘汰快过期的 Key5.1 淘汰不是精确算法LRU 和 LFU 在 Redis 中都是近似实现。Redis 不会维护完整的访问链表而是通过采样若干个 Key从中选择淘汰对象。采样数量由maxmemory-samples控制默认值通常是 5调大更接近精确但更耗 CPU。5.2 一次淘汰行为的观察下面在测试环境复现一次淘汰前置条件是把实例限制到很小的maxmemory并设置策略为allkeys-lru。# 步骤一设置小内存和淘汰策略仅测试环境redis-cli CONFIG SET maxmemory 20mb redis-cli CONFIG SET maxmemory-policy allkeys-lru# 步骤二持续写入直到触发淘汰foriin$(seq120000);doredis-cli SETkey:$ivalue-$i/dev/nulldone# 步骤三查看淘汰统计和内存redis-cli INFO stats|grepevicted_keys redis-cli INFO memory|grepused_memory:# 预期输出示例# evicted_keys:5421# used_memory:20961480关键步骤解释evicted_keys持续增长说明淘汰已经发生used_memory稳定在maxmemory附近。适用场景用于验证策略是否生效、观察淘汰速度。容易改错的地方如果策略设为volatile-*但 Key 没有设置过期时间那么淘汰集合为空写入会直接报 OOM 错误而不是淘汰数据。5.3 淘汰与过期的关系过期 Key 的清理有两种方式惰性删除在访问时判断是否过期定期删除由后台周期性抽样清理。淘汰则是在内存达到上限时被动触发。两者都可能造成延迟但原因不同。如果大量 Key 设置了相同的过期时间会在同一时刻集中过期可能引起延迟尖刺更稳妥的做法是在过期时间上加随机抖动。6. 慢查询把“变慢”拆成可定位的命令6.1 slowlog 记录的是什么Redis 的慢查询日志记录的是命令执行本身的耗时不包括网络往返和排队时间。相关配置有两个slowlog-log-slower-than表示超过多少微秒记录slowlog-max-len表示最多保留多少条。查看命令为SLOWLOG GET、SLOWLOG RESET、SLOWLOG LEN。命令作用注意事项SLOWLOG GET n查看最近 n 条慢查询返回包含命令参数注意敏感信息SLOWLOG LEN查看当前慢查询条数长期增长说明持续有慢命令SLOWLOG RESET清空慢查询日志排障前先记录再清空LATENCY RESET清空延迟事件用于配合延迟监控LATENCY HISTORY查看延迟历史可定位延迟尖刺类型6.2 一次完整的慢查询定位下面的流程模拟从发现变慢到定位具体命令输入是一个响应时间上涨的实例步骤是先看慢查询再分析命令涉及的数据规模。# 步骤一配置慢查询阈值例如超过 10ms 记录10000 微秒redis-cli CONFIG SET slowlog-log-slower-than10000redis-cli CONFIG SET slowlog-max-len256# 步骤二查看慢查询redis-cli SLOWLOG GET5# 预期输出示例节选# 1) 1) (integer) 14# 2) (integer) 1712345678# 3) (integer) 45231# 4) 1) HGETALL# 2) big:hash:order# 5) 127.0.0.1:52341# 6) # 步骤三确认该 Key 的规模和编码redis-cli HLEN big:hash:order redis-cli OBJECT ENCODING big:hash:order redis-cli MEMORY USAGE big:hash:order# 预期输出示例# (integer) 500000# hashtable# (integer) 48234496关键步骤解释第一条慢查询显示HGETALL耗时约 45msKey 名为big:hash:order第三步确认它有 50 万个字段、占用约 46MB属于典型大 Key。HGETALL会一次性返回所有字段数据量大时不仅慢还会占用网络带宽和客户端内存。处理方向业务上改用HGET按需读取、HSCAN分批遍历或者把大 Hash 拆分成多个小 Key。适用场景所有出现响应时间尖刺的实例。边界SLOWLOG不记录网络延迟和执行前的排队时间如果慢查询日志为空但接口仍然慢需要排查连接池、网络、主从同步或客户端序列化。6.3 大 Key 的排查代码下面给出一个用 Java 客户端扫描大 Key 的完整示例。前置环境是 JDK 8 及以上、Maven 项目引入 Jedis 客户端、本地可连接 Redis。目标是按类型抽样估算 Key 规模而不是在生产环境全量遍历。importredis.clients.jedis.Jedis;importredis.clients.jedis.ScanParams;importredis.clients.jedis.ScanResult;importjava.util.List;publicclassBigKeyScanner{publicstaticvoidmain(String[]args){JedisjedisnewJedis(127.0.0.1,6379);try{StringcursorScanParams.SCAN_POINTER_START;ScanParamsparamsnewScanParams().count(100);longchecked0;longbigKeyCount0;do{ScanResultStringresultjedis.scan(cursor,params);ListStringkeysresult.getResult();for(Stringkey:keys){checked;Stringtypejedis.type(key);longsizeestimateSize(jedis,key,type);if(size10_000){bigKeyCount;System.out.println([BIG KEY] keykey, typetype, sizesize);}}cursorresult.getCursor();}while(!ScanParams.SCAN_POINTER_START.equals(cursor));System.out.println(checkedchecked, bigKeyCountbigKeyCount);}catch(Exceptione){System.err.println(scan failed: e.getMessage());}finally{jedis.close();}}privatestaticlongestimateSize(Jedisjedis,Stringkey,Stringtype){switch(type){casestring:returnjedis.strlen(key);casehash:returnjedis.hlen(key);caselist:returnjedis.llen(key);caseset:returnjedis.scard(key);casezset:returnjedis.zcard(key);default:return0;}}}关键步骤解释使用SCAN而不是KEYS避免阻塞主线程每次只取 100 个 Key逐个按类型统计元素数量超过 10000 就打印。预期输出会列出大 Key 及其类型和规模最后打印扫描总数和大 Key 数量。适用场景测试环境或低峰期对指定实例做抽样检查。容易改错的地方SCAN只保证遍历期间存在的元素最终被返回不保证不重复所以统计是近似值生产环境不要用KEYS *替代它。7. 性能调优从参数到业务结构7.1 调优的优先级Redis 性能调优不是先改配置而是先确认瓶颈位置。可以按照下面的顺序判断命令层面是否存在 O(N) 大命令、大 Key、频繁全量遍历连接层面客户端连接池是否过小或过大是否存在连接泄漏内存层面编码是否合理、碎片是否过高、淘汰是否频繁持久化层面是否开启了 AOF 频繁刷盘或 RDB 频繁快照部署层面主从、哨兵、Cluster 是否存在跨机房高延迟或频繁故障转移。7.2 管道与批量操作如果业务需要连续写入大量小 Key逐条命令会产生大量网络往返。可以使用管道批量发送但要控制每次批量的大小避免单次响应过大。下面是一个完整示例前置环境与上一节相同。importredis.clients.jedis.Jedis;importredis.clients.jedis.Pipeline;publicclassPipelineDemo{publicstaticvoidmain(String[]args){JedisjedisnewJedis(127.0.0.1,6379);try{Pipelinepipelinejedis.pipelined();for(inti0;i1000;i){pipeline.set(batch:key:i,value-i);}pipeline.sync();System.out.println(pipeline done, samplejedis.get(batch:key:999));}catch(Exceptione){System.err.println(pipeline failed: e.getMessage());}finally{jedis.close();}}}关键步骤解释pipelined()返回管道对象连续调用set只是把命令放入本地缓冲sync()才真正发送并读取响应。预期输出会打印value-999。适用场景批量写入、批量删除等不需要逐条读取结果的场景。边界管道只节省网络往返不减少服务端命令执行成本单次管道过大可能导致客户端内存上涨或服务端输出缓冲超限。7.3 用 Lua 减少往返的边界有些场景需要在服务端原子执行多条命令可以用 Lua 脚本。但脚本本身如果遍历大集合同样会阻塞主线程。下面是一个简单示例目标是原子地读取并更新计数。importredis.clients.jedis.Jedis;publicclassLuaCounter{privatestaticfinalStringSCRIPTlocal current redis.call(GET, KEYS[1]) if current false then current 0 end current tonumber(current) tonumber(ARGV[1]) redis.call(SET, KEYS[1], current) return current;publicstaticvoidmain(String[]args){JedisjedisnewJedis(127.0.0.1,6379);try{Objectresultjedis.eval(SCRIPT,1,counter:order,5);System.out.println(counterresult);}catch(Exceptione){System.err.println(lua failed: e.getMessage());}finally{jedis.close();}}}关键步骤解释脚本在服务端原子执行避免客户端读取和写回之间被其他命令插入。预期输出为counter5重复执行会递增。适用场景计数、限流、简单的原子读改写。边界脚本执行时间过长会阻塞其他命令lua-time-limit只控制脚本超时后的行为不会自动杀死脚本不要在脚本里做大范围遍历。8. 常见误区误区一used_memory 低就说明内存没问题。操作系统看的是 RSS碎片过高时可能被 OOM Kill。误区二重启能解决内存问题。重启只能清空碎片和过期数据如果编码和 Key 结构不合理问题会很快复发还可能造成缓存雪崩。误区三设置 volatile-lru 就一定能淘汰。如果 Key 都没有过期时间淘汰集合为空写入会报错。误区四slowlog 没记录就说明没有慢查询。slowlog 只记录命令执行耗时不覆盖网络、排队和客户端处理。误区五编码转换后删除元素就能恢复内存。大多数编码转换是单向的删除元素不一定让编码退回紧凑结构。误区六KEYS 用于排查大 Key。KEYS会阻塞主线程生产环境应使用SCAN。9. 生产实践建议给所有缓存 Key 设置带随机抖动的过期时间避免集中过期。对 Hash、List、Set、ZSet 的业务规模设上限接近编码阈值时提前拆分或压测。监控同时覆盖 used_memory、used_memory_rss、evicted_keys、expired_keys 和 slowlog 长度。大 Key 和高频 Key 定期抽检写入链路中避免一次性返回大集合。主动碎片整理只在评估 CPU 余量后开启优先在低峰期验证。淘汰策略要与业务数据性质匹配纯缓存用 allkeys-lru 或 allkeys-lfu混合存储用 volatile-lru 并确保有过期时间。10. 排障清单与面试复盘10.1 排障清单收到 Redis 内存或延迟告警 | v 1. INFO memory看 used_memory、used_memory_rss、碎片率 | v 2. INFO stats看 evicted_keys、expired_keys 是否异常增长 | v 3. SLOWLOG GET确认是否有慢命令记录参数和时间 | v 4. 对慢命令涉及的 Key 执行 OBJECT ENCODING、MEMORY USAGE、HLEN/LLEN 等 | v 5. 判断是编码不合理、大 Key、碎片过高还是淘汰策略不匹配 | v 6. 制定改动拆分 Key、调整阈值、开启整理、更换淘汰策略或优化业务访问 | v 7. 低峰期验证并持续观察10.2 面试与复盘问题为什么小 Hash 用 listpack 而大 Hash 用 hashtable转换后为什么不能自动转回used_memory_rss 远大于 used_memory 时可能有哪些原因如何验证allkeys-lru 和 volatile-lru 在 Key 都没有过期时间时分别会发生什么slowlog 记录不到某个慢请求可能是哪些环节造成的大 Key 的常见识别方式和拆分思路有哪些为什么管道不一定能提升服务端处理能力11. 总结回到开头的告警场景正确的处理顺序不是先调参数而是先建立定位链路用INFO memory看内存形状用OBJECT ENCODING和MEMORY USAGE判断编码和大 Key用碎片率判断内存是否被浪费用SLOWLOG找到具体慢命令最后再决定是拆分 Key、调整编码阈值、开启碎片整理还是更换淘汰策略。现象优先排查方向典型动作内存高但 used_memory 不高碎片率检查 RSS评估主动碎片整理内存高且 evicted_keys 增长淘汰策略与数据量调整策略或扩容检查过期时间延迟尖刺且 slowlog 有记录大 Key 或 O(N) 命令拆分 Key改用分批命令延迟尖刺但 slowlog 为空网络、连接池、持久化检查客户端、AOF、RDB、主从延迟编码从紧凑变为通用业务数据规模控制元素数量必要时拆分把这张表当作日常排障的入口比记住一堆参数更有用。12. 参考资料Redis 官方文档Memory Optimizationhttps://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/Redis 官方文档Keyspace 与过期、淘汰策略说明https://redis.io/docs/latest/develop/reference/eviction/Redis 官方文档SLOWLOG 命令https://redis.io/docs/latest/commands/slowlog/Redis 官方文档MEMORY USAGE 命令https://redis.io/docs/latest/commands/memory-usage/Redis 官方文档OBJECT ENCODING 命令https://redis.io/docs/latest/commands/object-encoding/Redis 官方文档INFO 命令https://redis.io/docs/latest/commands/info/Jedis 项目文档https://github.com/redis/jedis
返回列表