3091 字
约 10 分钟
22
S12-A08 · 缓存容量预算:内存该给多少、淘汰怎么配

"给缓存加了 8G,两周后数据库的慢查询反而多了。"这句话在容量复盘里反复出现。机制不玄:内存变大 → 装下更多 key → 命中率上升 → 数据库压力下降,这条链只在工作集装得下时成立。装不下时,多出来的内存只延长了长尾 key 的停留时间,而它们各自被读一次;同时淘汰高频触发,缓存自己的延迟先涨。

容量预算只回答三个数:内存给多少、淘汰怎么配、何时该扩。三个数各有失效边界,混在一起调就会得到"加了内存没变快、减了内存立刻崩"。


1. 容量的四个约束

约束 决定什么 越界后的表现
实例内存上限(maxmemory / 容器 limit) 逻辑上能装多少 写入报 OOM command not allowed,或进程被 cgroup kill
命中率拐点 装到多少才开始有用 曲线走平,加内存的边际收益接近零
淘汰成本 每次写入的额外开销 延迟毛刺、CPU 升高、淘汰与主线程争抢
副本与持久化开销 物理内存 = 逻辑内存 × 倍数 fork 失败、AOF 重写反复重试、主从同时淘汰

四个约束里只有一个能被"加内存"消掉,第二个是数学,第三个是 CPU,第四个是物理账。

# redis.conf —— 上限先减掉持久化与副本的预留
maxmemory 6gb
maxmemory-policy allkeys-lfu
maxmemory-samples 10

按 Redis 7.x 核对:maxmemory 只约束数据集与部分开销,不含 fork 的 COW 页、复制缓冲与 AOF 重写缓冲。

2. 命中率与容量不是线性

访问分布是重尾(近似 Zipf),前百分之一量级的 key 吃掉大部分请求,命中率随容量呈凹函数:先陡后平。所谓"再多也补不齐",指尾部 key 的访问间隔远长于它在内存里的存活时间——装进去,读一次,被淘汰,等于没装。

判据不是"命中率多少算好",而是命中率对容量的导数:连续两周加 5% 内存、涨幅都低于 1 个百分点,说明已过拐点,问题在失效策略或访问模式。

3. 工作集估算的三种方法

方法 输入 输出 主要误差来源
热数据统计 --hotkeys、OBJECT FREQ 高频 key 清单与占比 需 LFU 类策略,冷启动期统计不可信
访问分布曲线 代理层/慢日志按 key 聚合 覆盖 90% 请求所需 key 数 日志采样率与时间窗选择
按对象大小反推 日均活跃 key 数 × 平均 value 大小 字节级工作集 忽略 per-key 开销,通常低估
$ redis-cli -h HOST -p PORT --hotkeys          # 需 LFU 策略,扫描本身有成本
$ redis-cli -h HOST -p PORT --bigkeys          # 找大 key,不是找热 key
$ redis-cli -h HOST -p PORT --memkeys --memkeys-samples 5000

三法粗精度不同:分布曲线定位量级,热 key 清单校准,大小反推换算内存。

4. 对象内存的真实占用

一条看着 200 字节的 JSON,在 Redis 里可能占 400 字节以上,差额来自结构性开销。

开销项 量级 说明
key 本身 与 key 长度相关 命名越长越贵,前缀复用可省一部分
SDS 头与 robj 十几到几十字节 短/长字符串的编码分支不同
dictEntry + 哈希表 每 key 数十字节 装载因子影响桶数组大小
过期字典 带 TTL 的 key 多一份 entry 全量 TTL 场景不可忽略
分配器取整 典型放大约 10%–30% jemalloc size class 向上取整
编码转换 阈值跨越时突增 小 hash 用 listpack,超阈值转 hashtable
$ redis-cli MEMORY USAGE user:profile:10086          # 单 key 实测字节
$ redis-cli OBJECT ENCODING user:profile:10086      # 是否已发生编码转换
$ redis-cli DEBUG OBJECT user:profile:10086         # 编码与长度细节,生产慎用

MEMORY USAGE 带采样参数时是估算值。预算时乘 key 总数再留 15%–25% 结构性余量,比拿 value 长度乘个数靠谱。

5. 淘汰策略的适用面与失效形态

策略 适用数据 失效形态
noeviction 缓存不可丢、写失败可接受 写满后写入报错,业务须有降级分支
allkeys-lru 通用读多写少 扫描型访问污染:全量遍历把冷 key 全标热
volatile-lru 冷热混合、部分 key 无 TTL 可淘汰集合太小时退化成 noeviction
allkeys-lfu 热点稳定、计数有意义 新热点冷启动饿死,需 lfu-decay-time 兜底
volatile-ttl 短命数据为主 长 TTL 的大对象先被清,可能清掉贵的那个
W-TinyLFU(本地侧) 高命中率要求 突发新热点有准入延迟,窗口内命不中

扫描污染的识别:keyspace_hits 骤降、evicted_keys 陡增、数据库 QPS 上升而请求总量没变。它不是容量不足。

# 用 LFU 必须同时配衰减,否则历史计数永久累积
maxmemory-policy allkeys-lfu
lfu-log-factor 10
lfu-decay-time 1

6. TTL 与容量的取舍

TTL 设定 命中率 陈旧窗口 淘汰压力 适用
很短(分钟级) 低 小 低 价格、库存、权限等强时效
中等(小时级) 中 可控 中 列表页、聚合结果
很长(天级) 高 大 低 静态字典、配置快照
无限 + 主动失效 最高 取决于失效通道 最低 有可靠广播失效时

长 TTL 把容量压力换成陈旧压力,短 TTL 把命中率换成数据库 QPS。加抖动是避免同批 key 集体过期,幅度按可接受的陈旧窗口给。

7. 缓存与持久化的内存叠加

场景 峰值来源 典型量级 处理
RDB fork 写入速率 × fork 时长 的 COW 页 与写入量同阶 预留内存或错峰触发
AOF 重写 重写缓冲 + fork COW 与重写期写入量同阶 调高 auto-aof-rewrite-percentage
主从全量同步 复制缓冲区膨胀 与同步时长相关 限速、避开高峰
容器 limit 上述峰值未计入 limit 达 limit 即被 kill limit = 数据 + 峰值 + 余量
$ redis-cli INFO memory | grep -E 'used_memory:|used_memory_rss|mem_fragmentation_ratio'
$ redis-cli INFO persistence | grep -E 'rdb_last_bgsave_status|aof_rewrite_in_progress'
$ redis-cli INFO stats | grep -E 'evicted_keys|expired_keys|keyspace_hits|keyspace_misses'

used_memory_rss / used_memory 长期高于 1.5 说明碎片吃掉了一部分预算。

8. 分片后的总容量与热点不均

总可用容量 ≈ 分片数 × 单分片可支配内存 × (1 − 碎片与副本预留)
真实约束   = min(各分片剩余内存),不是平均值
分片方式 不均来源 观测口径
客户端哈希 哈希函数与 key 分布 各实例 used_memory 极差
Cluster slot slot 分配不均、大 key 落单 slot 各节点 used_memory 与 cluster slots
代理层 路由倾斜、连接池不均 代理侧分片命中统计

平均值最容易骗人:10 个分片里 9 个用 40%、1 个用 95%,平均 45.5% 看着安全,实际已到顶。预算按极差做。

9. 击穿、穿透与容量无关

这三个词常被归因于"缓存不够大",实际是失效瞬间的并发问题:热点 key 过期那刻请求同时回源。手段是逻辑过期、单飞合并、预热与热点本地兜底。

判据:数据库 QPS 尖峰与淘汰/过期时间点对齐,问题在失效路径;与请求量对齐且命中率平稳,问题才在容量。

10. 扩容的时机信号

信号 阈值形态 含义
命中率平台 连续 14 天波动小于 1 个百分点 已过拐点,扩容无收益
淘汰速率 淘汰数 / 写入数 持续高于 10% 容量长期不足
扩容后数据库 QPS 不降反升 与扩容量同向 淘汰把请求打回了数据库
单分片内存极差 最热分片超过 85% 该分片决定整体上限
延迟毛刺周期 与 rdb_last_bgsave 对齐 是持久化,不是容量

11. 成本核算

年成本 ≈ 内存单价(GB·月) × 逻辑容量 × 副本倍数 × 12 + 运维与迁移工时
收益   ≈ 被挡下的数据库 QPS × 单 QPS 边际成本 × 12
结论   = 收益 > 成本 才扩;否则改策略、改 TTL、改数据模型
选择 有效收益 隐藏成本
加内存 命中率上升(过拐点后接近零) 单价 × 副本倍数,主从双份
改淘汰策略 同样内存下命中率上升 需重跑热 key 统计,短期抖动
缩短 TTL 陈旧窗口变小 数据库 QPS 上升
拆大 key / 改结构 单位内存装更多有效数据 改造与双写期复杂度

副本倍数最容易被漏:主从各一份等于单价翻倍。"省下的 QPS"只在数据库确为瓶颈时才算收益。

12. 排错

现象 常见原因 处理
加内存后命中率不变 已过拐点,或分布本身平坦 停扩,改失效策略与数据模型
加内存后数据库更慢 扫描污染把热 key 挤出 换 LFU 或加本地层,隔离批量任务
内存没到 maxmemory 却开始淘汰 分片不均,单分片先到顶 按最热分片看,重分 slot
fork 时延飙升甚至失败 COW 峰值超出物理余量 预留、错峰、调大重写阈值
命中率高但 P99 差 淘汰与大 key 操作争抢主线程 拆大 key、异步删除、加副本分担读

13. 边界

场景 为什么加内存不解决 该做什么
数据总量小于内存 本来就能全量装下,瓶颈在别处 查慢查询、序列化、网络
访问分布平坦 没有可缓存的热点,命中率天然低 换预计算或物化视图
命中率本就不高 回源比例由业务决定,容量不是变量 改接口聚合粒度
写多读少 缓存只增加一致性问题 直接优化写入路径
淘汰已占 CPU 显著比例 扩容压不住写入侧的淘汰频率 降写入、改批量、换分片

一句话判据:先确认瓶颈在"装不下"还是"留不住",两种账单不一样。


14. 自测

  1. 命中率连续两周不随容量变化,说明曲线处在哪一段?下一步看什么指标?
  2. 一次全量遍历为什么能污染 LRU?用哪两个计数器确认它发生了?
  3. 同一条 200 字节 JSON,为什么 MEMORY USAGE 明显更大?差额主要来自哪几项?
  4. maxmemory 为什么不包含 fork 的 COW 页?这个差额在容器里会造成什么后果?
  5. 十个分片平均使用率 45%,为什么仍可能已到上限?
  6. 数据库 QPS 尖峰与缓存过期时间点对齐,问题该归到容量还是失效路径?
S12-A08 · 缓存容量预算:内存该给多少、淘汰怎么配
https://www.clxhxhhr.top/posts/6208/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。