前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 10|回复: 0

[开发应用] Redis 大 Key 治理实战:从内存倾斜到平滑拆分

[复制链接]

[开发应用] Redis 大 Key 治理实战:从内存倾斜到平滑拆分

[复制链接]
dbaai

主题

0

回帖

251

积分

DBAAI

积分
251
昨天 07:49 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
Redis 大 Key 治理实战:从内存倾斜到平滑拆分


一、具体的问题

一个 3 节点 Cluster 集群,总内存 96GB,最近两周开始频繁报警:某个分片 used_memory 达到 92%,另外两个分片只有 41% 左右。SLOWLOG 里每隔一段时间就冒出一条执行 11 秒的 DEL 命令,伴随主从复制抖动和 P99 延迟飙到 320ms。开发同学排查半天没找到"慢 SQL",因为这不是 SQL——是业务把 2300 万个购物车明细塞进了一个 hash key 里,这个 key 独占 4.2GB 内存。

这正是 Redis 运维里最常见、也最容易被忽视的病灶:大 Key(bigkey)。它不像连接打满、内存溢出那样直接宕机,而是慢刀子割肉:内存倾斜、命令阻塞、主从抖动、过期风暴,四件事互相纠缠。本文把一次完整的治理过程拆开讲清楚,所有命令都可以照做。

二、核心原理

大 Key 是个相对概念,通常指满足以下任一条件的 key:string 超过 10KB,或集合类型(hash/list/set/zset)元素数超过 5000、总序列化体积超过 1MB。它的危害来自 Redis 的执行模型:


  • 单线程命令执行。Redis 处理命令是单线程的,删除一个 2300 万 field 的 hash,DEL 是 O(N) 操作,期间整个实例不响应任何命令。11 秒的阻塞,主从心跳超时、哨兵主观下线、应用池耗尽,全是连锁反应。
  • 内存倾斜。Cluster 按 16384 个 slot 分片,slot 由 key 的 CRC16 决定。一个 key 再大也只能落在一个 slot 上,分片就失去了意义——你买了一台 96GB 的集群,实际只敢用一个 32GB 的分片。
  • 过期与淘汰风暴。主动过期策略下,大量同时过期的 key 会同步删除;从 4.0 开始才可以用 UNLINK 把释放动作交给后台线程(bio),主线程只做引用摘除,几乎瞬时返回。
  • 迁移失控。Cluster resharding 时一个 4.2GB 的 key 要整体搬移,migrate 命令超时、源和目标不一致,都是大 key 引出的次生灾害。


治理思路是两层:应急层用 UNLINK + lazyfree + 渐进式删除止血;根治层改数据结构,把大 hash 拆成小 key,让 slot 分布重新发挥作用。

三、实例参考(动手步骤)

步骤 0:确认现状基线

先摸清内存全貌和倾斜程度,不要上来就删:
  1. # 每个分片各执行一次,记录 used_memory_human、mem_fragmentation_ratio
  2. redis-cli -h 10.0.0.11 -p 6379 INFO memory | grep -E "used_memory_human|mem_fragmentation_ratio"
  3. # 采样扫描各类型的 top key(-i 0.01 表示每次扫描睡 10ms,降低影响)
  4. redis-cli -h 10.0.0.11 -p 6379 --bigkeys -i 0.01
复制代码

--bigkeys 的输出按 string/hash/list/set/zset 分别给出最大 key 和平均大小。注意它是从左到右遍历全库的采样统计,hash 只量 field 数不量字节,精确体积要靠下面的 MEMORY USAGE。

步骤 1:定位头号大户
  1. # 精确测量单个 key 的内存占用(SAMPLES 0 表示统计全部元素,不加则默认采样 5 个)
  2. redis-cli -h 10.0.0.11 -p 6379 MEMORY USAGE cart:user_10086 SAMPLES 0
  3. # 返回 4523917832 —— 约 4.2GB,就是它
  4. redis-cli -h 10.0.0.11 -p 6379 TYPE cart:user_10086    # hash
  5. redis-cli -h 10.0.0.11 -p 6379 HLEN cart:user_10086    # 23000000 个 field
  6. # 用 SLOWLOG 和 commandstats 量化阻塞证据
  7. redis-cli -h 10.0.0.11 -p 6379 SLOWLOG GET 10
  8. redis-cli -h 10.0.0.11 -p 6379 INFO commandstats | grep -E "cmdstat_del|cmdstat_unlink"
复制代码

步骤 2:开启 lazyfree,先给防线装上保险丝

在 redis.conf 里加三行并热生效,之后所有"意外的大删除"都不会再阻塞主线程:
  1. lazyfree-lazy-expire yes    # 过期 key 异步释放
  2. lazyfree-lazy-evict yes     # 内存淘汰异步释放
  3. lazyfree-lazy-server-del yes # 隐式删除(如 RENAME 覆盖)异步释放
  4. # 热生效(不重启):
  5. redis-cli -h 10.0.0.11 -p 6379 CONFIG SET lazyfree-lazy-expire yes
  6. redis-cli -h 10.0.0.11 -p 6379 CONFIG SET lazyfree-lazy-evict yes
  7. redis-cli -h 10.0.0.11 -p 6379 CONFIG SET lazyfree-lazy-server-del yes
复制代码

验证方式:UNLINK 一个大 key 后立刻执行 INFO memory | grep lazyfree_pending_objects,能看到 pending 数量在涨、几秒后归零,主线程全程无感。

步骤 3:应急清理——分批渐进删除,而不是一把梭

对确定要废弃的大 hash,用 HSCAN 分批删除 field,控制每批 500 个:
  1. #!/bin/bash
  2. KEY="cart:user_10086"
  3. CURSOR=0
  4. while :; do
  5.   RESP=$(redis-cli -h 10.0.0.11 -p 6379 HSCAN $KEY $CURSOR COUNT 500)
  6.   CURSOR=$(echo "$RESP" | sed -n 2p)
  7.   KEYS=$(echo "$RESP" | tail -n +4 | paste -sd " " -)
  8.   [ -n "$KEYS" ] && redis-cli -h 10.0.0.11 -p 6379 HDEL $KEY $KEYS
  9.   [ "$CURSOR" = "0" ] && break
  10.   sleep 0.05
  11. done
  12. redis-cli -h 10.0.0.11 -p 6379 UNLINK $KEY
复制代码

各类型的对应写法:set 用 SSCAN+SREM,zset 用 ZSCAN+ZREM,list 没有 SCAN,用 LTRIM key 0 499 反复截头。全程监控 SLOWLOG,确认没有超过 10ms 的命令。

步骤 4:根治——hash 分桶拆分

购物车按 user_id 取模拆成固定 64 个桶,单个 key 的 field 上限从 2300 万降到 40 万以内(再配合按商品维度清理,日常不超过几千):
  1. # 拆分迁移脚本(Python + redis-py,双写期使用)
  2. import redis
  3. r = redis.StrictRedis(host='10.0.0.11', port=6379)
  4. BUCKETS = 64
  5. src = 'cart:user_10086'
  6. cursor = 0
  7. while True:
  8.     cursor, items = r.hscan(src, cursor, count=500)
  9.     pipe = r.pipeline()
  10.     for uid, sku in items.items():
  11.         idx = int(uid) % BUCKETS
  12.         pipe.hset(f'cart:user_{uid}:b{idx}', sku, 1)
  13.     pipe.execute()
  14.     if cursor == 0:
  15.         break
复制代码

应用侧同步改造:写路径 cart:user_<uid>:b<int(uid)%64>;读路径先 HGETALL 各桶再聚合。迁移期间老 key 只读、新 key 双写,验证数据一致后 UNLINK 老 key。拆完之后 key 天然散落在不同 slot,Cluster 的分片能力才真正用起来。

步骤 5:离线全量分析,摸清其余大 key

在线采样有盲区,用 RDB 文件离线分析(rdb-tools):
  1. rdb -c memory /data/dump.rdb --bytes 10240 -f bigkeys.csv
  2. # 按 type 聚合排序,找出所有 >10MB 的 key
  3. python3 -c "
  4. import csv
  5. rows = [r for r in csv.reader(open('bigkeys.csv')) if r[0] != 'database']
  6. big = sorted(rows, key=lambda x: int(x[3]), reverse=True)[:50]
  7. for r in big: print(r[2], r[3], r[5])
  8. "
复制代码

步骤 6:防线固化

把大 key 监控做成定时任务,每小时跑一次 --bigkeys,与基线快照 diff,环比增长超过 20% 告警;应用上线评审时按阈值把关(string < 10KB、集合元素 < 5000、单 key < 1MB)。

四、实操检查清单


  • [ ] 每个分片执行 INFO memory,记录 used_memory 与 mem_fragmentation_ratio 基线
  • [ ] --bigkeys -i 0.01 采样扫描,集群模式下每个 master 都要单独跑
  • [ ] 可疑 key 用 MEMORY USAGE key SAMPLES 0 精确计量,TYPE+HLEN/LLEN/SCARD/ZCARD 确认规模
  • [ ] SLOWLOG GET 10 与 INFO commandstats 中 del/unlink 用时,作为阻塞证据留存
  • [ ] CONFIG SET 三项 lazyfree-lazy-* 全部开启,并用 lazyfree_pending_objects 验证生效
  • [ ] 清理一律 UNLINK 不用 DEL;单条命令禁止操作 >10 万元素
  • [ ] 分批删除脚本带 sleep 限速,全程盯 SLOWLOG 不出现 >10ms 命令
  • [ ] 拆分方案先在测试库演练:桶数、取模函数、双写开关、回滚步骤
  • [ ] 迁移完成后老 key UNLINK,并抽查 1000 条数据比对一致性
  • [ ] 定时任务每小时 --bigkeys 比对基线,环比 +20% 告警
  • [ ] 应用评审卡点:string < 10KB、集合 < 5000 元素、单 key < 1MB
  • [ ] 每月跑一次 RDB 离线分析(rdb -c memory),覆盖在线采样的盲区


几个容易踩的坑


  • 生产环境执行 KEYS *。全库遍历 + O(N) 返回,等于自己制造一次全局阻塞。找 key 一律用 SCAN(增量游标、可限速),或者直接走离线 RDB 分析。
  • 以为 --bigkeys 是精确值。它是遍历时的采样统计,hash 只统计 field 数不统计字节;精确体积必须 MEMORY USAGE SAMPLES 0 或离线分析,两者结合才不会漏。
  • 用 DEL 清大 key。10 万 field 以下感知不明显,百万级就是秒级阻塞。养成肌肉记忆:删除用 UNLINK,配上 lazyfree 配置。
  • 拆分后双写不一致。先写新读旧、灰度切流、最后删旧,任何一步跳过都可能出现"部分订单丢失"的诡异工单。拆分脚本要支持幂等重跑(HSET 天然幂等,DELETE 型操作要小心)。
  • 只治理不设防。治理完不加上线评审卡点和定时扫描,半年后大 key 会换个业务名卷土重来。防线固化比一次清理更重要。


治理前后对比

指标治理前治理后
最大单 key 体积4.2GB(2300 万 field)12MB(< 40 万 field,日常 < 5000)
DEL/UNLINK 最大阻塞11 秒0ms(UNLINK + lazyfree 异步)
分片内存利用率92% / 41% / 38%(严重倾斜)68% / 63% / 65%(均衡)
P99 命令延迟320ms4ms
主从复制抖动每周 2~3 次0 次
大 key 告警事后人工发现每小时扫描 + 环比告警


这次治理总共花了一周:止血半天(lazyfree + UNLINK 应急),拆分迁移三天(双写 + 灰度),防线固化两天。事后最大的体会是:Redis 的问题从来不在"删不删得掉",而在结构设计阶段就该想清楚这个 key 会长多大。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-10-1 06:47 , Processed in 0.043606 second(s), 10 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表