TL;DR
- 场景: 在大规模分布式缓存中使用 EVCache,需要搞清楚底层 Memcached 的内存管理与过期淘汰行为
- 结论: EVCache 本质依赖 Memcached + Slab Allocation + 惰性过期 + 近似 LRU,性能好但也有内存碎片、淘汰抖动等工程风险
- 产出: 梳理 EVCache 底层 Memcached 架构、Slab Class/Item 结构、Lazy Expiration 与 LRU 淘汰逻辑
1. Memcached 架构
Memcached 是由 danga 公司开发的高性能分布式内存对象缓存系统,最初为 LiveJournal 设计。
核心特点:
- C/S 架构: 采用经典客户端-服务器模型,服务器端以守护进程方式运行,监听端口 11211
- 基于 libevent 的高性能事件处理: 使用 libevent 库实现高效的事件驱动模型
- 多线程支持: Memcached 是多线程的
工作流程:
- 应用首先检查 Memcached 中是否存在所需数据
- 若命中缓存,直接返回结果
- 若未命中,则查询数据库
- 将数据库结果存入 Memcached(设置合理过期时间)
- 返回数据给应用
2. Slab Allocation 内存管理机制
Memcached 采用 Slab Allocation(块分配)的内存管理机制来解决内存碎片问题。
核心原理:
-
内存分配基础
- 以固定大小的 Page 为单位进行内存分配,默认每个 Page 大小为 1MB
-
内存分割机制
- 将分配的内存分割成不同大小的 Chunk(块)
- Chunk 大小遵循增长因子(Growth Factor)规律,默认增长因子为 1.25
-
数据存储策略
- 当存储数据时,Memcached 会选择能容纳该数据的最小 Slab Class
3. 数据 Item 结构
Item 是存储的数据,以双向链表形式存储:
typedef struct _stritem {
struct _stritem *next; // LRU 双向链表中的后向指针
struct _stritem *prev; // LRU 双向链表中的前向指针
rel_time_t time; // 最近一次访问时间(用于 LRU)
rel_time_t exptime; // 过期时间
int nbytes; // value 区域的数据长度
unsigned short refcount; // 引用计数
uint8_t slabs_clsid; // 所属的 slab class ID
uint8_t nkey; // key 的长度
} item;
4. 过期机制
4.1 Lazy Expiration(惰性过期)
Memcached 采用惰性过期策略,不会主动扫描和清理过期数据,而是在每次执行 get 操作时才会检查数据是否过期。
优势:
- 节省系统资源:不需要额外的后台线程或定时任务
- 按需处理:只有在真正访问数据时才进行过期检查
4.2 LRU(最近最少使用)
当 Memcached 使用的内存超过配置的最大内存限制时,系统会触发 LRU 算法来回收内存空间。
淘汰执行步骤:
- 系统从对应 slab class 的数据项列表尾部开始逆向扫描
- 优先查找 refcount=0 的 item
- 如果找不到 refcount=0 的 item,则查找最近访问时间超过 3 小时的 item
5. 错误速查表
| 症状 | 根因定位 | 修复 |
|---|---|---|
| 命中率正常但延迟偶发飙高 | -m 配置过小或热点数据膨胀 | 提升 -m 配额或拆分业务缓存 |
| 某些 key 偶尔失踪 | LRU 淘汰优先回收长时间未访问的 item | 调整缓存粒度与访问模式 |
| 内存使用率看似不高,但无法再插入新数据 | Slab Allocation 导致”类满”但结构浪费 | 重新规划 key/value 大小 |
| 过期时间到达后,仍能看到大量疑似已过期 key | 使用 Lazy Expiration | 对关键 key 通过主动 get 触发清理 |
| CAS 更新失败比例高 | 热点 key 写竞争严重 | 对热点 key 做分桶/分片 |