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 是多线程的

工作流程:

  1. 应用首先检查 Memcached 中是否存在所需数据
  2. 若命中缓存,直接返回结果
  3. 若未命中,则查询数据库
  4. 将数据库结果存入 Memcached(设置合理过期时间)
  5. 返回数据给应用

2. Slab Allocation 内存管理机制

Memcached 采用 Slab Allocation(块分配)的内存管理机制来解决内存碎片问题。

核心原理:

  1. 内存分配基础

    • 以固定大小的 Page 为单位进行内存分配,默认每个 Page 大小为 1MB
  2. 内存分割机制

    • 将分配的内存分割成不同大小的 Chunk(块)
    • Chunk 大小遵循增长因子(Growth Factor)规律,默认增长因子为 1.25
  3. 数据存储策略

    • 当存储数据时,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 算法来回收内存空间。

淘汰执行步骤:

  1. 系统从对应 slab class 的数据项列表尾部开始逆向扫描
  2. 优先查找 refcount=0 的 item
  3. 如果找不到 refcount=0 的 item,则查找最近访问时间超过 3 小时的 item

5. 错误速查表

症状根因定位修复
命中率正常但延迟偶发飙高-m 配置过小或热点数据膨胀提升 -m 配额或拆分业务缓存
某些 key 偶尔失踪LRU 淘汰优先回收长时间未访问的 item调整缓存粒度与访问模式
内存使用率看似不高,但无法再插入新数据Slab Allocation 导致”类满”但结构浪费重新规划 key/value 大小
过期时间到达后,仍能看到大量疑似已过期 key使用 Lazy Expiration对关键 key 通过主动 get 触发清理
CAS 更新失败比例高热点 key 写竞争严重对热点 key 做分桶/分片