华为云DCS-Redis大key与热key治理

举报
yd_221506059 发表于 2026/10/18 14:40:37 2026/10/18
【摘要】 内存告警、延迟尖刺、一月一次主从切换,第一反应是扩容,结果从 3 主 3 从加到 4 主 4 从,延迟内存都没改善。本文把大 key 与热 key 分开讨论:前者是空间与阻塞问题,DEL 一个 500 万元素的 Hash 能阻塞单线程实例数秒;后者是单核问题,一个 key 只落一个 slot,加节点无效。文中给出 lazyfree 配置、大 key 拆分与本地缓存的取舍,以及定位方法。

华为云 DCS Redis 大 key 与热 key 治理:加了两个节点,集群反而更慢

内存告警、延迟尖刺、偶尔一次主从切换。第一反应是扩容,从 3 主 3 从扩到 4 主 4 从,结果延迟没降、内存没降,还多付了一台机器的钱。问题不在容量,在于两类被混为一谈的 key——大 key 是空间与阻塞问题,热 key 是单核问题,药方完全不同,用错方向会白花预算。

一、现象:扩容失效

集群规格 3 主 3 从,单节点 16 GB,总内存 48 GB。告警与异常表现:

观察项 现象
内存使用率 单节点 82%~88%,长期高位
延迟 P99 平时 2 ms,偶发 200~800 ms 尖刺
尖刺时间点 与定时任务、报表导出时间高度重合
主从切换 一个月内发生 1 次,切换期间请求大量失败
扩容后 延迟、内存均无改善

最后一条是关键:扩容无效,说明瓶颈不是"总量不够",而是"某几个点特别重"。

二、先分清两种病:大 key 与热 key

这两个词经常混着说,但它们是两个正交的问题:

维度 大 key 热 key
本质 单个 key 的数据量过大 单个 key 的访问频率过高
消耗什么 内存 + 单次操作耗时 单个 CPU 核心
症状 内存不均、偶发阻塞、网络带宽打满 延迟尖刺、QPS 上不去
扩容有用吗 有用(分摊内存) 完全没用(一个 key 只在一个 slot)
主要手段 拆分 key、异步删除、限制读取量 本地缓存、key 拆分、读写分离

扩容对热 key 完全无效,这是本次踩的第一个坑。

三、大 key:DEL 为什么会阻塞整个实例

Redis 是单线程处理命令的。删除一个大 key 时,需要逐个释放其内部所有元素的内存——这个释放过程是同步的,在这期间整个实例不处理任何其他命令。

一个存了 500 万元素的 Hash,DEL 可能阻塞数秒。而线上用的是同步删除:

DEL big_hash        ← 阻塞,期间所有请求排队

Redis 4.0 之后提供了异步删除,把释放动作交给后台线程:

UNLINK big_hash     ← 立即返回,后台线程慢慢释放

同时还有几个 lazyfree 相关配置,控制哪些场景走异步:

lazyfree-lazy-eviction yes     # 淘汰时异步释放
lazyfree-lazy-expire yes       # 过期时异步释放
lazyfree-lazy-server-del yes   # RENAME 等隐式删除时异步
lazyfree-lazy-user-del yes     # 用户执行 DEL 时也异步(等效 UNLINK)

注意 lazyfree-lazy-expire yes 这条尤其重要:如果大量 key 设置在同一时刻过期,过期删除会在那个时间点集中爆发,形成周期性尖刺——这和本文"尖刺与定时任务重合"的现象完全吻合。

大 key 还有两个隐性成本

① 读取时的网络与内存翻倍。 HGETALL 一个 500 万元素的 Hash,返回的数据要完整序列化、网络传输、再在客户端反序列化。服务端内存峰值和客户端内存峰值同时翻倍,这个成本在压测里往往测不出来(压测数据量小)。

② 集群内存分布不均。 Redis Cluster 按 slot 分片,一个大 key 只能落在一个节点上。如果几个大 key 恰好落在同一个节点,该节点内存远高于其他节点,而其他节点还有大量空闲——总内存看着够,单节点先 OOM。

四、热 key:为什么加节点完全无效

Redis Cluster 的 key 分布规则是:

slot = CRC16(key) mod 16384

一个 key 只对应一个 slot,一个 slot 只在一个主节点上。所以:

无论加多少节点,同一个 key 的读写永远落在同一台机器上。

而 Redis 处理命令是单线程的。一个 QPS 10 万的热 key,会把这个节点的单核彻底打满,其他节点再空闲也帮不上忙。这就是扩容后延迟毫无改善的原因。

热 key 的三种解法,按侵入性排序:

1. 本地缓存(首选)

在客户端加一层进程内缓存,短 TTL,挡掉绝大部分读请求:

// 伪代码:本地缓存 + 短过期,兜住热 key
private final Cache<String, String> localCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(500, TimeUnit.MILLISECONDS)   // 关键:短 TTL
        .build();

public String getHotValue(String key) {
    return localCache.get(key, k -> redis.get(k));
}

代价是一致性:500 ms 内可能读到旧值。所以本地缓存只适合"允许短暂不一致"的场景(商品详情、配置项、榜单),不适合余额、库存这类强一致数据。

2. key 拆分(写扩散)

把 hot:item:1001 复制成 hot:item:1001:0 ~ hot:item:1001:9 十份,读的时候随机取一个:

int shard = ThreadLocalRandom.current().nextInt(10);
String realKey = "hot:item:1001:" + shard;

这十份会分布到不同 slot,从而分散到不同节点,真正用上了集群的多核能力。

代价是写扩散:更新时要写十份,且存在短暂不一致窗口。适合读远多于写的场景(比如热点商品),不适合写频繁的数据。

3. 读写分离

华为云 DCS 支持读写分离实例,把读请求分摊到从节点。这是成本最低的方案,但要注意主从复制是异步的,从节点数据可能有毫秒级延迟。

五、怎么把这两类 key 找出来

1. 扫大 key

绝对不要用 KEYS *——它是全量遍历,在百万级 key 的实例上会阻塞数秒,线上等同于一次故障。

正确工具是 --bigkeys,它内部用 SCAN 增量遍历,不阻塞:

redis-cli -h <host> -p 6379 --bigkeys

输出会按类型列出最大的 key。但它只看元素数量,不看实际内存。要精确判断内存占用,用:

# 对可疑 key 逐个查内存(Redis 4.0+)
redis-cli MEMORY USAGE big_hash

如果要批量评估,用 SCAN 游标遍历 + MEMORY USAGE 采样:

redis-cli --scan --pattern 'item:*' --count 100 \
  | while read k; do
      echo "$(redis-cli MEMORY USAGE "$k") $k"
    done | sort -rn | head -20

注意 SCAN 的语义:它是增量遍历,遍历期间新增/删除的 key 可能漏掉或重复,不保证完整快照。对排查来说够用,但别拿它当精确统计。

2. 找热 key

--hotkeys 依赖 LFU 计数器,必须先把淘汰策略设成 LFU 系列,否则命令直接报错:

maxmemory-policy allkeys-lfu     # 或 volatile-lfu

然后:

redis-cli --hotkeys

如果线上策略是 LRU 不能改,退化方案是看 SLOWLOG 和 INFO commandstats,或者用 MONITOR 短时间采样(MONITOR 会显著影响性能,只能开几秒,严禁长时间挂)。

六、淘汰策略:一个容易引发事故的默认值

maxmemory-policy 的选择直接影响"内存满时会发生什么":

策略 内存满时的行为 适用
noeviction 写入直接报错 缓存不能丢的场景(但会写失败)
allkeys-lru 淘汰最久未用的 key 纯缓存,最常用
allkeys-lfu 淘汰访问频率最低的 key 有明显热点,找热 key 需要它
volatile-lru/lfu 只在设置了过期时间的 key 里淘汰 混合用途
allkeys-random 随机淘汰 基本不用

两个坑:

  1. noeviction 在内存满时会返回 OOM 错误给客户端,业务表现为"写入突然全部失败",但监控上内存只是"到顶了",很容易误判成网络问题。
  2. 淘汰动作本身消耗 CPU。内存长期贴着上限跑,淘汰会持续触发,进一步拉高延迟——这也是为什么内存使用率 85% 就该处理,而不是等到 100%。

七、治理方案与优先级

优先级 动作 风险 见效
1 开启 lazyfree 系列配置(含 lazyfree-lazy-expire) 极低 立即消除删除/过期阻塞
2 大 key 拆分,按业务维度切分 中(需改代码) 1~2 周
3 热 key 加本地缓存或 key 拆分 中(需评估一致性) 1~2 周
4 调整 maxmemory-policy 为 LFU 低 立即
5 把 KEYS、HGETALL 大集合替换为 SCAN / HSCAN 分批 中(需改代码) 2~4 周
6 读写分离 / 扩容 低 立即(但可能无效)

第 1 项几乎零成本、零风险,应该立刻做。 而第 6 项要放在最后——因为前面已经证明,方向错了的话扩容纯属浪费。

八、效果

指标 治理前 治理后
延迟 P99 200~800 ms(尖刺) 4 ms
单节点内存使用率 82%~88% 55%~62%
内存分布偏差(最高/最低) 2.3 倍 1.1 倍
主从切换 1 次/月 0
节点数 4 主 4 从(扩容后仍慢) 回退到 3 主 3 从

最后一行值得注意:治理完成后,节点数比扩容前还少,性能和内存却都改善了。这说明当初的扩容是完全错误的方向。

九、小结

四句话:

  1. 先分清大 key 和热 key:前者是空间与阻塞问题,后者是单核问题,扩容只对前者有效。
  2. 删除大 key 必须异步,UNLINK 加 lazyfree 配置,否则一次 DEL 就能阻塞整个实例数秒。
  3. 热 key 加节点无效,因为一个 key 只落在一个 slot 上;要靠本地缓存、key 拆分或读写分离。
  4. 别用 KEYS 排查,用 --bigkeys 和 SCAN;--hotkeys 需要先把淘汰策略设为 LFU。

如果只记一句:Redis 变慢的时候,先问是"哪个 key 太大"还是"哪个 key 太热",这两个答案通向完全不同的修法。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。