土法炼钢兴趣小组的算法知识备份

【Redis / 缓存内核】maxmemory 策略:近似 LRU/LFU 与 MEMORY DOCTOR

文章导航

分类入口
databasestorage
标签入口
#redis#maxmemory#eviction#lru#lfu#memory-doctor#evict.c#7.4

目录

第 7 篇 讲的是已过期 key 如何被清扫;本篇讲仍有效 key 在内存触顶时如何被踢出。maxmemorymaxmemory-policy 是缓存角色 Redis 的核心旋钮,但源码实现的不是操作系统课本里的真 LRU,而是用固定内存的采样 + 淘汰池近似——理解这一层,才能解释「明明开了 LRU 却淘汰了热 key」类故障。

本文锚定 Redis OSS 7.4.x evict.cobject.cMEMORY DOCTOR),并对照站内 页面置换算法Buffer Pool 中的 LRU/CLOCK 讨论。

本文是「Redis / 缓存内核」系列第 8 篇(共 16 篇)。→ 系列目录

版本锚定:Redis OSS 7.4 / 8.x;默认 maxmemory-samples 为 5,LFU_INIT_VAL 为 5。


一、问题:内存上限下的在线驱逐

zmalloc_used_memory() 超过 maxmemory(且非 noeviction),写路径在 processCommand 里调用 performEvictions() 尝试腾出 mem_tofree。这是典型的 demand paging 式在线决策:只见过去访问,不见未来——与 Bélády OPT 不可实现性同一抽象(详见 页面置换 第一节)。

Redis 与 OS 页置换的关键差异:

维度 OS 页置换 Redis maxmemory
元数据 页表 + 硬件引用位 redisObject.lru 字段复用
精确 LRU 成本 链表 + 锁竞争 单线程仍不愿 O(N)
策略族 LRU/CLOCK/ARC… 8 种固定 policy + 采样
驱逐对象 有 TTL 与无 TTL 混合 volatile-* vs allkeys-* 显式分支

二、策略矩阵:volatile × allkeys

config.c 注册 8 种 policy(MAXMEMORY_*):

策略 候选集合 选择依据
volatile-lru expires 表中的 key 近似 LRU(idle 最大)
volatile-lfu 同上 近似 LFU(频率最低)
volatile-ttl 同上 expire_at 最早
volatile-random 同上 随机
allkeys-lru keys 近似 LRU
allkeys-lfu 同上 近似 LFU
allkeys-random 同上 随机
noeviction 不驱逐;写命令可能拒绝

volatile-* 只从 db->expires 采样——无 TTL 的 key 永不作为淘汰候选,内存触顶时可能直接 OOM 拒绝写入。纯缓存场景若忘记给 key 设 TTL,volatile-lru 等于「只删有过期时间的 subset」,常踩坑。

allkeys-*db->keys 采样,把 Redis 当整个数据集的上限容器;与 Buffer Pool 驱逐脏页不同,Redis 驱逐的是应用可见的 key,且会发 evicted 通知并写 AOF/复制。


三、近似 LRU:24 位时钟与淘汰池

3.1 LRU 时钟

真 LRU 需维护全序访问链表。Redis 在 redisObject 里用 24 位 lru 字段存上次访问的压缩时钟

#define LRU_CLOCK_RESOLUTION 1000  /* 毫秒;1000 表示秒级粒度 */

unsigned int getLRUClock(void) {
    return (mstime() / LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
}

读/写命中时 LRU_CLOCK() 写入 o->lruestimateObjectIdleTime() 用当前时钟减 o->lru 得 idle 毫秒——这是 CLOCK 族思路:不移动链表,只打时间戳(对照 页面置换 CLOCK 节)。

1000/hz <= LRU_CLOCK_RESOLUTION 时,主循环在 serverCron 预计算 server.lruclock,避免每次访问调 mstime()——精度与 hz 绑定,高 hz 提高时钟分辨率。

3.2 采样与 eviction pool

performEvictions() 在 LRU/LFU/TTL 类策略下调用 evictionPoolPopulate():每次从候选 dict 随机抽 maxmemory-samples 个 key(默认 5,可配 1–64),把 idle 最大的若干名填入固定大小 淘汰池 EvictionPoolLRU[EVPOOL_SIZE](16 槽)。

/* evict.c 文件头注释(节选) */
/* Every time there is a key to expire, we sample N keys (usually ~5)
 * to populate a pool of M keys (EVPOOL_SIZE = 16).
 * After the pool is populated, the best key in the pool is expired. */

然后从池尾(idle 最大)取候选,若 key 仍存在则 dbGenericDelete()。池内可能残留已删 key 的「幽灵」条目,下次再填充。

与真 LRU 的差距:一次驱逐决策只看 \(N\) 个随机样本中的最优,全局热 key 若不在样本中就可能被误踢——扫描型 workload(全表遍历)下近似 LRU 表现差,与 Megiddo & Modha 在 ARC 论文(IBM, 2003)中指出的「纯 LRU 对频率与最近性失衡」是不同维度的问题,但现象类似:单次顺序扫描可冲掉整个淘汰池视角下的「冷」数据

3.3 LFU 编码

volatile-lfu / allkeys-lfu 复用 lru 字段,高 16 位为衰减时间,低 8 位为对数计数器(LFU_INIT_VAL = 5 起步)。LFUDecrAndReturn() 做时间衰减,LFULogIncr() 在访问时亚线性增频——用 8 位近似 Morris 计数思想,避免 per-key 高频计数器内存。

运维可调 lfu-log-factorlfu-decay-timeOBJECT FREQ 读当前计数)。LFU 对周期性热点(如每分钟刷新的报表 key)通常优于秒级 LRU 时钟,但参数敏感,无 universal 最优(与 ARC 自适应论点形成对照,Redis 未实现 ARC)。

3.4 volatile-ttl 与 random

volatile-ttlexpires 采样后选 剩余 TTL 最短 的 key(evictionTimeProc),适合「优先保留长 TTL 会话」的业务,但不保证保留访问频率高的 key。

random 策略一次随机选一个候选 dict 槽中的 key,无池;实现最便宜,行为最难预测,仅适合均匀访问或调试。

flowchart TD
  ov["memory > maxmemory"]
  pe["performEvictions()"]
  sample["evictionPoolPopulate<br/>N = maxmemory-samples"]
  pool["EvictionPool[16]<br/>max idle / min freq"]
  del["dbGenericDelete + lazyfree?"]
  ov --> pe --> sample --> pool --> del

四、performEvictions 与延迟

performEvictions() 循环驱逐直到 mem_freed >= mem_tofree 或无法继续。每次删除走 dbGenericDelete(..., server.lazyfree_lazy_eviction, DB_FLAG_KEY_EVICTED),大对象可异步释放(第 7 篇)。

注意:

noevictionperformEvictions 返回 EVICT_FAIL,写命令得到 OOM 类错误,读命令仍可服务——这是「缓存」与「存储」角色分界:宁可拒写也不默默丢有效数据(除非业务接受)。


五、MEMORY DOCTOR:概念化信号

MEMORY DOCTOR 调用 getMemoryDoctorReport()object.c),基于 getMemoryOverheadData()规则化启发诊断,不是 AI 分析。核心判定阈值(概念摘要):

信号 条件(概念) 含义
empty 分配总量 < 5MB 实例几乎为空
big_peak peak / used > 1.5 峰值远高于当前,可能有大批 key 已驱逐或重启
high_frag 碎片率 > 1.4 且 > 10MB 分配器碎片,考虑 activedefrag
high_alloc_frag allocator 层碎片 jemalloc 类问题
high_proc_rss 进程 RSS 相对分配偏高 可能是 COW、复制缓冲、lazyfree 积压
big_client_buf 平均每客户端 > 200KB 输出缓冲 慢客户端或大响应堆积
big_slave_buf 从节点总缓冲 > 10MB 复制滞后
many_scripts 缓存脚本 > 1000 EVAL 脚本过多

与淘汰的关系high_frag / big_peak 不直接说明 LRU 选错,但可区分「policy 误踢热 key」与「内存碎片/lazyfree 未归还」。驱逐本身会制造大量释放块,加剧碎片——长期高驱逐率应同时看 MEMORY PURGEactivedefrag 与 key 设计(大 key、无 TTL)。

本站不粘贴本机 MEMORY DOCTOR 输出;读者可在实例上执行 MEMORY DOCTOR 对照上表理解字段。


六、学术谱系与争论

谱系:Bélády OPT(1966)→ LRU 链表(教科书)→ CLOCK 近似(Corbató 等)→ Redis 采样池(evict.c 注释自 2014 起演进)→ LFU 对数计数(Redis 4.0+)→ ARC/2Q(Megiddo & Modha 2003;Redis 实现)。

争论

  1. 近似 LRU 是否「够好」:Redis 官方文档明确写的是 approximated LRU。对 heavy-tailed 热点,LFU 或业务侧 TTL 常更稳;对扫描,应靠 allkeys-lfu 或客户端避免冷数据大面积进入。
  2. volatile vs allkeys 语义volatile-* 隐含「无 TTL 数据更宝贵」;若业务相反,应 allkeys-* 并给常驻 key 显式长 TTL 或 PERSIST
  3. 采样数 N 的权衡maxmemory-samples 增大提高近似质量,但单次 performEvictions CPU 上升——与 PostgreSQL Buffer Pool 的 clock sweep 扫全表不同,Redis 刻意限制为常数级采样。

开放问题:Redis 是否会引入自适应策略(ARC 类)至今无主线承诺;模块与多 DB 下淘汰池跨 slot 公平性在 Cluster 场景依赖 kvstore 分片,极端倾斜 slot 的驱逐公平性需结合 第 13 篇 理解。


七、常见误解

7.1 「开了 LRU 就等于操作系统 LRU」

不对。Redis 是 N 随机样本 + 16 槽池 + 秒级时钟,不维护全局 LRU 序。热 key 不在样本中仍可能被踢。

7.2 「淘汰和过期是一回事」

不对。过期看 expires 时间戳;淘汰看 maxmemory-policy 与访问元数据。过期 key 应先被 第 7 篇 路径删除;若仍超内存,才驱逐未过期 key。

7.3 「volatile-lru 会删所有 key」

只删设了 TTL 的 key。无 TTL 的 key 在 volatile-* 下受保护,可能导致 write OOM。

7.4 「LFU 不需要预热」

新 key 以 LFU_INIT_VAL 起步,需若干次访问才能积累频率;极短生命周期的 key 可能被 LFU 误判为冷数据。


八、小结

三句话小结

  1. 八种 policy 先分 volatile/allkeys 候选集,再在 LRU/LFU/TTL/random 间选 victim。
  2. 近似 LRU/LFUredisObject.lru 时钟/计数 + 每次约 5 个随机样本填入 16 槽淘汰池,不是全局链表。
  3. MEMORY DOCTOR 用碎片、峰值、客户端/复制缓冲等启发规则指路,不替代对 maxmemory-policy 与 workload 的分析。

上一篇TTL 与过期

下一篇RDB 快照


参考资料

  1. Redis OSS 7.4 源码:src/evict.cperformEvictionsevictionPoolPopulateestimateObjectIdleTime)、src/object.cgetMemoryDoctorReport)、src/server.hMAXMEMORY_*LRU_CLOCK_RESOLUTIONLFU_INIT_VAL)。
  2. Redis Documentation v7.x, Key evictionmaxmemory-sampleslfu-log-factor
  3. Megiddo & Modha, ARC: A Self-Tuning, Low Overhead Replacement Cache, USENIX FAST 2003(对照自适应替换;Redis 未实现 ARC)。
  4. 页面置换算法Buffer Pool
  5. 系列 index第 7 篇 TTL

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-13 · database / storage

【Redis / 缓存内核】混合持久化与崩溃语义

说明 Redis 7.4 默认开启的 RDB preamble + INCR AOF 如何组合恢复路径,结合 fsyncgate 与数据完整性给出 appendfsync 与混合模式的配置决策树及耐久争论边界。


By .