资讯动态

Redis慢查询与BigKeys排查:构建五层监控体系的实战经验

发布时间:2026/9/26 3:36:03 来源:尧图企业网站定制
线上数据库集群里Redis 一直是大家眼中“最不可能出问题”的一环。直到一次商品详情接口的偶发超时把 BigKeys 和慢查询这两个词彻底带进了我们的缓存监控体系。这篇文章不准备讲太多空泛理论我想把一个真实问题的排查过程以及最后沉淀下来的整套监控框架完整拆给你看。如果你手头维护着几个 Redis 实例或者在给公司搭建缓存告警体系这篇文章应该能帮你省掉不少弯路——尤其是那些“健康指标全绿、业务却在超时”的诡异场景。后面涉及的命令我都标了执行环境你可以直接复制到自己的机器上验证。1. 一次“偶发超时”带出的监控盲区1.1 故障现场健康指标全绿业务却在超时事情发生在某个大促前的压测阶段。业务方反馈商品详情页接口每天晚上八点左右会出现少量超时Redis 客户端报command timed out after 2000ms但过几分钟自己又恢复了。我第一反应是打开云监控看 Redis 实例状态CPU 使用率 12%QPS 两万左右且曲线平稳内存使用率 70%缓存命中率 93%。单看这几项这就是一个非常健康的实例。但业务方的超时日志是真实存在的不会撒谎。于是我们把网络也排查了一遍网络组同事检查了交换机、丢包率、带宽水位结论是物理链路没有问题。那问题就只剩一个方向Redis 自己变慢了但慢的时机非常隐蔽没有体现在平均指标里。1.2 常规巡检为什么抓不住“变慢”的 Redis这个案例暴露了一个典型问题监控体系只覆盖了资源层没有覆盖命令层和数据层。资源层的 CPU、内存、网络指标回答的是“这台机器忙不忙”。但 Redis 是单线程事件循环模型所有命令在服务端是排成一队逐个执行的。一条命令执行 500ms会让后面成千上万个请求一起排队等待从业务视角看就是“Redis 卡了”可此时机器的 CPU 可能只有 10%。平均指标还有一个天然缺陷它会掩盖长尾。2 万 QPS 里混着 20 个 500ms 的慢请求平均值几乎看不出来。只有把视角放到命令级别去看具体是哪条命令耗时异常才能发现问题。1.3 监控体系全景从资源层到命令层的五层模型这次事故之后我把 Redis 监控拆成了五个层次每一层解决一类问题监控层关注点主要数据来源资源层CPU、内存、网络、磁盘云监控、系统监控实例层QPS、连接数、命中率、淘汰数、过期数INFO命令命令层单条命令耗时、慢查询、阻塞客户端SLOWLOG、LATENCY数据层key 规模、bigkey 分布、TTL 覆盖率SCAN、--bigkeys巡检主从层同步延迟、积压缓冲区、fork 耗时INFO replication大多数团队的第一套监控都停在“资源层 实例层”但真正导致线上事故的往往在下面三层。BigKeys 属于数据层慢查询属于命令层这篇文章的核心就是这两层怎么查、怎么治、怎么持续盯。2. BigKeys 定位先抓慢命令再挖大 Key2.1 多大的 key 算 bigkey阈值与业务场景业界对 bigkey 的判定没有一个绝对标准但有一个通行的经验区间String 类型value 大于10KB开始关注大于1MB必须治理集合类型Hash / List / Set / ZSet元素个数超过5000开始关注超过10000必须治理这个标准不是拍脑袋。Redis 单线程模型下一个 Hash 执行HGETALL取 1 万个字段耗时可能在几十毫秒到一百多毫秒不等一个 20MB 的 String 执行一次GET光序列化传输就得占住连接。在 QPS 过万的业务上这种耗时会让 p99 从几毫秒直接冲到几百毫秒。要注意阈值必须结合场景一个 2MB 的冷数据 key一年访问不了几次危害有限一个 500KB 但每秒被访问上百次的热 key才是更需要处理的对象。bigkey 巡检前最好先按访问频率把 key 分个类。2.2 redis-cli --bigkeys 的原理与正确姿势排查 bigkey 最常用的工具是redis-cli --bigkeys。它背后做的事很简单用SCAN游标遍历整个 keyspace对每个 key 执行TYPE再按类型用STRLEN、LLEN、SCARD、HLEN、ZCARD这些 O(1) 命令读取长度或元素个数最后按类型输出最大的若干 key。命令长这样redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1-i 0.1表示每个类型扫描完成后停顿 0.1 秒用来给实例“喘口气”。在线上执行时这个参数一定不要省。执行完会输出类似这样的统计-------- summary ------- Biggest string found mall:product:883213:shelf has 23872113 bytes Biggest hash found mall:sku:stock has 12000 fields Biggest set found user:online:20250603 has 2300 members这里有个容易被忽略的细节--bigkeys统计的是“字符串长度”和“集合元素个数”它反映的是结构大小不是内存占用。一个元素很多但每个元素都很短的 Hash和一个字段不多但单个 value 很大的 Hash在--bigkeys里的表现可能天差地别。所以它适合做快速筛选精确定位还得靠下面这两个命令。2.3 MEMORY USAGE 与 DEBUG OBJECT精确测量内存要精确知道一个 key 实际占多少内存用MEMORY USAGEredis-cli MEMORY USAGE mall:product:883213:shelf它会返回该 key 在当前数据结构下完整的内存占用包括对象头、指针、字典扩容等开销。但这个命令会对整个 key 做一次完整遍历来计算CPU 消耗不小不适合对线上大量 key 执行只适合针对--bigkeys筛出的少数候选 key 做确认。DEBUG OBJECT是另一个常用手段redis-cli DEBUG OBJECT mall:product:883213:shelf输出里的serializedlength只是序列化后的字节数不包括 Redis 对象头、哈希表桶、指针等内存。实际内存通常是这个值的 2 到 3 倍。所以看到serializedlength: 8901231时别以为这个 key 只占 8.9MB真实占用很可能在 20MB 上下。2.4 我们那次“先丢 KEY、再阻塞”的完整定位过程回到开头那次事故。我看完资源指标没发现问题直接登进实例查慢查询redis-cli SLOWLOG GET 20结果让我有点意外慢查询里大量出现KEYS mall:product:*单次执行耗时在 200ms 到 600ms 之间。代码里明显有人在高峰期执行 KEYS 全量扫描。再用 MONITOR 抓了 20 秒现场确认是某个缓存清理的定时任务在每次运行时会先KEYS匹配前缀再逐个删除。这就是第一个根因KEYS 命令在整个 keyspace 上做全表扫描。当时实例里有两三百万个 key一次 KEYS 就是几百毫秒的阻塞而且它排在命令队列前面后面所有读写请求都得等它跑完。这已经能解释“偶发超时”了。修复 KEYS 之后我以为事情结束了。但为了不留隐患我在低峰期执行了一次--bigkeys结果又扫出几个 20MB 以上的大 String就是前面提到的mall:product:883213:shelf。这个 key 存的是商品详情页的多端文案、图片列表、优惠策略的全量 JSON每次订单状态变更都会整体覆盖写入。它单个 key 的SET耗时其实不高甚至有业务缓存「读多写少、整体覆盖」的典型特征在网络传输和内存分配上的开销都被慢查询日志漏掉了。两个问题叠加才让那个接口在高峰期频繁踩到超时红线。这也说明慢查询和 bigkey 必须一起看它们互为补充单独看任何一个都有可能误判。3. 慢查询别让“日志条数”骗了你3.1 slowlog 机制与阈值调优Redis 的慢查询日志机制很轻每条命令在服务端执行结束后如果耗时超过配置的阈值就记录到内存队列。相关配置有两个# 阈值单位微秒默认 10000即 10ms CONFIG GET slowlog-log-slower-than # 队列长度默认 128 CONFIG GET slowlog-max-len默认的 10ms 阈值对绝大多数场景来说太宽松了。Redis 的单个命令正常应该在微秒级完成一条命令跑超过 2ms 就值得关注。生产环境我一般建议改成 2000 微秒CONFIG SET slowlog-log-slower-than 2000 CONFIG SET slowlog-max-len 1024注意CONFIG SET是动态修改改了之后记得同步到 Redis 配置文件否则重启就丢了。slowlog-max-len不要设太小默认 128 条几分钟就被冲掉了起不到追溯作用。查询慢查询的指令SLOWLOG GET 100 SLOWLOG LEN SLOWLOG RESET慢日志是一个先进先出的内存队列重启 Redis 就没了。要保留历史必须定期从SLOWLOG GET拉出来存到 ES、ClickHouse 或者普通日志文件里这也是监控体系里比较容易被忽略的一环。3.2 slowlog 看不到的两类慢排队、fork、swap慢查询日志有一个重要边界它只记录命令在 Redis 服务端“真正执行”的时间不包含网络往返时间也不包含命令在队列里等待前面命令执行的时间。这就导致两类典型问题在 slowlog 里完全隐身。第一类是排队等待。当某条慢命令比如 KEYS占住主线程 500ms 时后面所有命令实际上都被阻塞了但它们在 slowlog 里的执行耗时可能只有零点几毫秒。如果你只看慢查询条数会觉得“Redis 不慢啊”可客户端看到的延迟已经是秒级。第二类是 fork 和 swap 这类“进程级停顿”。Redis 做 RDB 持久化或者 AOF 重写时需要 fork 一个子进程fork 过程主线程要复制内存页表。实例内存越大fork 耗时越长。我曾经遇到过一个 20GB 内存的实例latest_fork_usec显示 fork 耗时超过 300ms这期间所有命令全部卡住但 slowlog 里一条记录都没有。查看方法INFO stats | grep latest_fork_usec内存 swap 也是同理。当物理内存不足key 被换到磁盘访问时会有毫秒甚至秒级的停顿CPU 和内存指标可能都看不出异常。所以完整的事件级监控一定要覆盖latest_fork_usec最好把INFO stats里的关键字段也采集进监控系统。3.3 KEYS 命令为何是头号杀手很多人觉得 KEYS 只是“查一下”危害被夸大了。说一个具体数字百万级 key 的实例上一次KEYS mall:*耗时通常在 300ms 以上。因为它要对整个 keyspace 做一次全量扫描无论匹配结果只有几个 key它都得遍历完所有 key 才能给你答案。Redis 没有前缀索引这回事。在测试环境 key 数量只有几百时KEYS 耗时 1ms 以内完全没感觉。一旦上了生产key 涨到几百万问题立刻暴露。开发同学在本地复现不了就会陷入“测试没问题、线上偶发超时”的困惑。替代方案是用SCAN游标分批取redis-cli --scan --pattern mall:product:* --count 1000SCAN同样是 O(N)但每次只返回一批 key然后让出事件循环不会长时间霸占主线程。它不能像 KEYS 那样保证一次拿到全量结果需要循环消费游标直到返回 0。业务代码里做缓存清理时优先用 SCAN 或者记录 key 清单不要把 KEYS 写进循环逻辑。3.4 slowlog 与 latency 监控要配合使用因为 slowlog 有上述盲区监控体系里必须补上另一个维度端到端的延迟测量。最简单的是直接用redis-cli --latencyredis-cli --latency -h 127.0.0.1 -p 6379它会持续输出当前到 Redis 的往返延迟统计包括 min、max、avg。这个值能反映网络链路和实例整体的处理能力但不能定位到具体命令。进阶用法是通过 Redis 内置的LATENCY事件框架监控底层事件比如fork、aof-write等。查询最新事件LATENCY LATEST LATENCY HISTORY fork我在实际排查中会把三张图放在一起看红线是客户端测到的接口延迟蓝线是 slowlog 里命令执行耗时绿线是latest_fork_usec。如果红线高、蓝线低、绿线也低那问题大概率在网络或客户端如果绿线有明显尖峰那就是 fork 问题如果蓝线高才轮到具体命令。4. BigKey 治理实战删、拆、压三步走4.1 直接删除UNLINK 与 lazy free 的取舍定位到 bigkey 之后最直接的诉求是把它清掉。但直接用DEL删除一个 20MB 的 String 或者一万个元素的 Hash代价是同步阻塞主线程删除期间所有命令都卡住。这在 Redis 4.0 之前的版本是线上事故的高发原因。Redis 4.0 引入了UNLINKUNLINK mall:product:883213:shelfUNLINK把 key 从命名空间里摘掉后真正的内存回收交给后台线程异步执行主线程几乎不受影响。生产环境删除大 key优先用 UNLINK。还有一个相关配置值得了解CONFIG SET lazyfree-lazy-user-del yes开启之后DEL命令也会走异步释放。但要注意异步释放不代表内存瞬间回收只是不阻塞主线程。如果实例本身内存告急释放期间内存占用还会持续一段时间别因为“异步”就放松对内存水位的监控。4.2 渐进式删除大 Hash 与大 List 的瘦身脚本不是所有 bigkey 都能直接删。有些 key 还在被业务读取直接删除会导致缓存穿透把请求打到数据库。这时候需要渐进式瘦身分批从集合中删除元素把 bigkey 变成一个普通大小的 key再让它自然淘汰。大 Hash 用HSCANHDEL分批删import redis r redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) key mall:sku:stock while True: cursor, fields r.hscan(key, cursor0, count100) if not fields: break r.hdel(key, *fields.keys()) print(cursor) if cursor 0: break大 List 可以用LTRIM反复截断key user:visit:history while r.llen(key) 0: r.ltrim(key, 0, 999)这里每次LTRIM key 0 999会保留前 1000 个元素把后面的全部丢掉循环执行直到列表清空。建议在业务低峰执行并且把每次删除的批次调小比如一次 100 个避免单次操作耗时太夸张。渐进式删除最大的价值是“删除期间仍然提供服务”。如果业务允许 key 直接消失果断用 UNLINK效率和代码复杂度都低得多。别为了追求优雅把简单问题复杂化。4.3 业务侧拆分与序列化压缩临时清理只是止血要想根治得让 bigkey 不再产生。我们当时对mall:product系的 key 做了两层改造。第一层是拆分。原来的 key 是一个商品的全部详情包含图片列表、多端文案、优惠策略、库存快照。现在按业务维度拆成多个 keymall:product:{id}:info、mall:product:{id}:images、mall:product:{id}:promotion。单个 key 的大小从 20MB 降到几百 KB读取按需获取写的时候也只更新变化的那块。第二层是压缩。对文本类 JSON 直接做 gzip20MB 的 JSON 压缩完经常只剩 2MB 左右。写入时压缩、读取时解压CPU 多了几个毫秒的开销但换来网络传输和内存占用的大幅下降对读多写少的大 body 场景非常划算。改动之前建议先做压测确认解压耗时不会拖垮接口。还有一点容易被忽略如果业务上允许给这类 key 设置合理的 TTL。永久 key 只增不减迟早会再次变成 bigkey。加了过期时间即使业务侧漏删Redis 也会在某个时间点帮我们清理掉。4.4 治理后的效果验证治理完成不是“改完代码就收工”一定要把效果量化出来。我当时做了三件事第一在高峰期重新看接口的延迟分布p99 从 120ms 降到了 8ms 左右第二查 slowlog确认 KEYS 和大 key 相关的命令从日志里消失第三连续三周每周跑一次--bigkeys巡检把新增的大 key 列表扫出来逐个确认改造是否覆盖。这里有一个关键经验bigkey 治理是持续过程不是一次性任务。只要业务在增长、代码在迭代新的 bigkey 就一定会出现。没有巡检机制兜底半年后大概率复发。5. 监控体系落地指标清单、工具选型与告警阈值5.1 采集层INFO、redis_exporter、自研巡检脚本监控体系的采集层建议采用“通用指标 专项巡检”的组合模式。通用指标用开源的redis_exporter接入 Prometheus。它通过周期执行INFO命令拿到实例基础数据再暴露成 Prometheus 指标。启动方式很简单./redis_exporter \ --redis.addrredis://10.0.0.1:6379 \ --redis.passwordxxx \ --web.listen-address:9121采集周期设置 15 到 30 秒一次即可。太频繁反而会给 Redis 增加无意义的 INFO 调用负担。专项巡检则必须自己做因为标准 exporter 不会定期跑--bigkeys。我们写了一个简单的定时脚本每周在业务低峰自动执行一次redis-cli --bigkeys把结果解析成 JSON 存到 Prometheus 的bigkey_count指标里Grafana 上做成一张表能看到每个实例最近一周有没有新增大 key。脚本核心逻辑只有两步调--bigkeys拿输出按Biggest xxx正则解析出 key 名和大小。5.2 核心指标与告警阈值清单这是我认为最重要的一张表推荐直接抄走指标告警阈值含义connected_clients / maxclients超过 80% 持续 5 分钟连接数即将耗尽used_memory / maxmemory超过 80%内存容量告急blocked_clients大于 0 且持续超过 1 分钟有命令在阻塞等待keyspace_misses / (keyspace_hits misses)超过 20%命中率异常下降evicted_keys速率持续大于 0内存淘汰策略被频繁触发expired_keys速率出现明显尖峰可能造成缓存雪崩latest_fork_usec超过 1 秒fork 阻塞风险master_link_down_since_seconds大于 0主从同步断开slowlog 新增条目数/小时超过 20命令级异常频繁出现bigkey 巡检结果出现超过阈值的 key数据层健康恶化阈值不一定要照搬需要结合自己的实例规格和业务属性调整但它们覆盖的方向是通用的资源层、实例层、命令层、数据层、主从层一层都不能少。5.3 慢查询与 bigkey 的定期巡检方案监控指标画在 Grafana 上只是第一步还要有主动巡检的机制。我们最终把巡检沉淀成了每周一次的固定动作输出一份 Redis 巡检报告包含四块内容第一全实例慢查询 Top 10。脚本从每个实例的 slowlog 中拉取最近七天的数据按执行次数和最大耗时排序重点看有没有新出现的命令类型。第二bigkey 清单。每周低峰跑--bigkeys和上周清单做 diff新出现的大 key 自动置顶抄送给相关业务负责人。第三内存和 key 规模趋势。实例的 key 总数、内存水位和一周前的对比提前发现“缓慢增长”型问题。第四主从健康和 fork 耗时。INFO replication的延迟状态以及latest_fork_usec的变化趋势。这套巡检不需要额外开发太多东西脚本加起来也就几百行。关键是让它稳定运行并且每次输出的报告有人看、有人跟。5.4 落地过程中常见的三个坑第一个坑是采集频率过高。有的团队为了数据平滑把 exporter 周期调到 5 秒甚至 1 秒结果压抑了 Redis 本身的性能指标看起来是“近实时”了但为了采集制造了额外负载。Redis 的 INFO 本身很轻但快手网络抖动、频繁建连也是成本15 秒是一个比较合理的折中。第二个坑是全量执行 MEMORY USAGE。有人觉得--bigkeys不够精确想用MEMORY USAGE全量跑一遍所有 key这种操作在百万 key 的实例上会让 CPU 直接飙升甚至拖垮主线程。正确做法是先用--bigkeys或DEBUG OBJECT的serializedlength做粗筛只对 Top 候选 key 精确测量。第三个坑是命中率告警一刀切。缓存命中率低不一定是坏事如果业务本身就是冷热交替的比如某些活动页只在特定时间段被大量访问miss 率高是正常现象。告警不要只看命中率绝对值要和数据库的慢查询、接口延迟联动判断——命中率下降的同时数据库负载升高才是真正需要告警的“缓存穿透/击穿”信号。踩过几次坑之后我个人的体会是Redis 监控体系的建设顺序永远是先解决已经发生的问题再建设报警最后才谈自动化。如果连 KEYS 还在代码里跑、bigkey 还在线上堆那再漂亮的监控大屏也只是事后复盘时的装饰品。先把大 key 清掉把慢查询阈值调准让团队能在一个相对健康的基线上观察数据后面的一切才谈得上意义。

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

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

免费获取报价 →
↑