Memcached 完全指南 / 第 3 章:内存架构
第 3 章:内存架构
3.1 整体架构
Memcached 的进程模型非常简洁:单进程多线程,基于事件驱动(event-driven)处理网络 I/O。
┌──────────────────────────────────────────────────────┐
│ Memcached 进程 │
│ │
│ ┌─────────────┐ ┌────────────────────────────┐ │
│ │ 主线程 │ │ Worker 线程池 │ │
│ │ (Main) │ │ ┌──────┐ ┌──────┐ │ │
│ │ │ │ │ W-1 │ │ W-2 │ ... │ │
│ │ - accept() │───▶│ │ │ │ │ │ │
│ │ - 分发连接 │ │ └──────┘ └──────┘ │ │
│ └─────────────┘ └────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 内存管理层 │ │
│ │ ┌───────────┐ ┌──────────┐ ┌─────────────┐ │ │
│ │ │ Item 管理 │ │ Slab 分配│ │ Hash 表 │ │ │
│ │ │ (LRU链) │ │ (分页) │ │ (查找) │ │ │
│ │ └───────────┘ └──────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 网络层 (libevent) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ TCP 监听 │ │ UDP 监听 │ │ Unix Socket │ │ │
│ │ └──────────┘ └──────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
关键组件职责
| 组件 | 职责 |
|---|---|
| 主线程 | 监听端口,accept 新连接,Round-Robin 分发给 Worker |
| Worker 线程 | 处理已连接的客户端请求(读/写/命令解析) |
| Item | 存储一个 K/V 对的数据结构 |
| Slab 分配器 | 预分配内存页,切分为固定大小的 chunk |
| Hash 表 | Key → Item 的 O(1) 查找 |
| LRU 链表 | 按访问时间维护 Item,用于淘汰 |
3.2 Slab 分配器
Slab 分配器是 Memcached 内存管理的核心,灵感来自 Linux 内核的 Slab 分配器。
设计目的
传统的 malloc/free 会产生内存碎片(fragmentation),导致:
- 实际可用内存远小于分配内存
- 频繁分配/释放造成性能抖动
Slab 分配器通过预分配 + 固定大小 chunk 来解决这两个问题。
Slab 内存布局
Memcached 总内存(-m 参数,单位 MB)
│
├── Slab Class 1 (chunk: 96B) ─┬─ Page 1 (1MB)
│ ├─ Page 2 (1MB)
│ └─ Page 3 (1MB)
│
├── Slab Class 2 (chunk: 120B) ─┬─ Page 4 (1MB)
│ └─ Page 5 (1MB)
│
├── Slab Class 3 (chunk: 152B) ─┬─ Page 6 (1MB)
│ ...
│
├── ...
│
└── Slab Class N (chunk: 1MB) ─── Page M (1MB)
Slab Class 生成规则
Slab Class 的 chunk 大小由启动参数 -f(增长因子,默认 1.25)和 -n(最小空间,默认 48B)决定:
Slab Class 1: chunk = 96B (最小为 96B)
Slab Class 2: chunk = 96 * 1.25 = 120B
Slab Class 3: chunk = 120 * 1.25 = 150B → 8 对齐 → 152B
Slab Class 4: chunk = 152 * 1.25 = 190B → 8 对齐 → 192B
...
Slab Class N: chunk ≤ 1MB (1048576B)
查看当前实例的 Slab 信息:
echo "stats slabs" | nc localhost 11211
# 关键输出:
# STAT 1:chunk_size 96
# STAT 1:chunks_per_page 10922
# STAT 1:total_pages 1
# STAT 1:total_chunks 10922
# STAT 1:used_chunks 523
# STAT 2:chunk_size 120
# ...
Item 结构
每个存储在 Memcached 中的 K/V 对都由一个 Item 结构管理:
┌─────────────────────────────────────────┐
│ Item 结构 │
├─────────────────────────────────────────┤
│ next / prev (LRU 链表指针, 16B) │
│ h_next (Hash 冲突链, 8B) │
│ time (最后访问时间, 4B) │
│ exptime (过期时间, 4B) │
│ nbytes (Value 长度, 4B) │
│ refcount (引用计数, 2B) │
│ nsuffix (后缀长度, 1B) │
│ flags (客户端标志, 1B) │
│ it_flags (内部标志, 1B) │
│ slabs_clsid (Slab Class ID, 1B) │
│ key (Key 数据) │
│ suffix (CAS ID + 换行符) │
│ data (Value 数据) │
└─────────────────────────────────────────┘
一个 Item 的总开销约为 48-56 字节(不含 Key 和 Value)。
内存分配流程
当 set key 0 0 5 执行时:
1. 计算需要的 chunk 大小
= Item 头部(~48B) + Key 长度 + 后缀长度 + Value 长度
= 48 + 3("key") + 8 + 5 = 64B
→ 选择 Slab Class 1 (chunk ≥ 64B, 实际 96B)
2. 从 Slab Class 1 的空闲链表获取一个 chunk
├── 有空闲 chunk → 直接分配
└── 无空闲 chunk → 分配新的 1MB Page → 切分为 10922 个 96B chunk
3. 将 Item 数据写入 chunk
4. 插入 Hash 表和 LRU 链表
3.3 Hash 表
Memcached 使用哈希表实现 Key → Item 的 O(1) 查找。
哈希算法选择
# 启动时指定哈希算法
memcached -o hash_algorithm=murmur3
# 可选算法
# murmur3 — 推荐,分布均匀,速度快
# jenkins — 经典算法
# crc32 — 简单但分布不够均匀
# md5 — 慢但均匀
# fnv1_32 — FNV-1 32位
# fnv1a_32 — FNV-1a 32位(推荐备选)
哈希表结构
Hash Table (哈希表)
├── bucket[0] → Item_A → NULL
├── bucket[1] → NULL
├── bucket[2] → Item_B → Item_C → NULL (冲突链)
├── bucket[3] → Item_D → NULL
└── ...
查找流程: hash(key) % bucket_count → bucket → 遍历冲突链 → 比较 key
哈希表会自动扩展(hash expansion),当 Item 数量远大于 bucket 数量时,冲突链变长,查找性能下降。可以通过 stats hash 查看:
echo "stats hash" | nc localhost 11211
# STAT hash_power_level 20 # bucket 数 = 2^20 ≈ 100万
# STAT hash_bytes 8388608 # 哈希表占用内存
# STAT hash_is_expanding 0 # 是否正在扩展
3.4 LRU 链表
LRU(Least Recently Used)是 Memcached 的缓存淘汰机制。
LRU 链表结构
每个 Slab Class 维护自己的 LRU 链表:
Slab Class 1 的 LRU 链表:
HEAD ←→ [Item_A] ←→ [Item_B] ←→ [Item_C] ←→ ... ←→ [Item_Z] ←→ TAIL
最近使用 最久未使用
(热数据) (淘汰候选)
LRU 操作
| 操作 | 动作 |
|---|---|
get 命中 | 将 Item 移动到链表头部(MRU 端) |
set 新值 | 新 Item 插入链表头部 |
| Item 过期 | 不立即移除,被访问时检测并移除 |
| 内存不足 | 从链表尾部开始淘汰(LRU 端) |
LRU 改进(1.5+)
Memcached 1.5 引入了 lru_maintainer 模式,改进了传统 LRU 的问题:
传统 LRU(单链表):
HEAD ←→ [Hot] ←→ [Warm] ←→ [Cold] ←→ [Expired] ←→ TAIL
问题:新插入的冷数据可能淘汰热数据
lru_maintainer 模式(TEMP LRU):
├── HOT LRU: 高频访问的热数据
├── WARM LRU: 中等频率数据
├── COLD LRU: 低频访问的冷数据
└── TEMP LRU: TTL 很短的临时数据(独立淘汰)
# 启用 lru_maintainer(推荐)
memcached -o lru_maintainer
# TEMP LRU 的 Item 选择:TTL <= 临时阈值的 Item
# 默认阈值:TTL <= 60秒 + 已使用的 Slab 页超过 25%
# 可通过 -o temp_ttl=<seconds> 调整
3.5 多线程模型
线程架构
┌────────────────────────────────────────────────────┐
│ 主线程 (Main Thread) │
│ │
│ ┌──────────┐ Round-Robin 分发 │
│ │ listen() │─────────────────────────────────┐ │
│ │ accept() │──────┐ │ │
│ └──────────┘ │ │ │
│ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐
│ │ Worker 1 │ │ Worker N │
│ │ │ │ │
│ │ libevent │ │ libevent │
│ │ event_base │ │ event_base │
│ │ │ │ │
│ │ 处理读写 │ │ 处理读写 │
│ └────────────┘ └────────────┘
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ 共享内存(Hash + Slab) │ │
│ │ 全局锁 / Item 级锁 │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
线程分工
| 线程 | 职责 |
|---|---|
| 主线程 | 监听端口,accept 连接,通过 pipe 通知 Worker |
| Worker 1..N | 管理已连接客户端,解析命令,读写 Item |
| LRU 维护线程 | lru_maintainer 启用时,定期调整 LRU 链表 |
| LRU 爬虫线程 | lru_crawler 启用时,扫描过期 Item |
| Slab 迁移线程 | slab_automove 启用时,在 Slab Class 间迁移内存页 |
锁机制
Memcached 使用细粒度锁来减少线程间竞争:
锁类型:
├── 全局锁(stats_lock): 保护统计信息
├── Slab 锁(slabs_lock): 保护 Slab 分配/释放
├── Hash 锁(hash_lock): 保护哈希表操作
└── LRU 锁(lru_locks[]): 每个 Slab Class 独立的 LRU 锁
Item 级操作:
1. Hash 查找 → 持有 Hash 锁 → 获取 Item → 增加 refcount → 释放 Hash 锁
2. 操作 Item(读/写)→ 无需全局锁
3. 更新 LRU 位置 → 持有 LRU 锁
4. 释放 Item → 减少 refcount → refcount=0 时回收 chunk
3.6 事件驱动模型
Memcached 使用 libevent 实现事件驱动 I/O(类似 NIO):
Worker 线程事件循环:
while (true) {
event_base_loop(base, EVLOOP_ONCE);
// libevent 触发回调:
// - on_read: 读取客户端数据 → 解析命令 → 执行
// - on_write: 发送响应数据
// - on_close: 清理连接
}
每个 Worker 线程维护独立的 event_base,管理该线程负责的所有连接。
3.7 关键内部数据结构
conn 状态机
每个客户端连接由一个 conn 结构表示,内部维护状态机:
conn 状态转换:
LISTENING → ESTABLISHED → READ_CMD → EXECUTE_CMD → WRITE_RESULT → READ_CMD
│ ↑
└── CLOSE ←──────────────────────────────────────┘
stats 统计信息
# 内存相关统计
echo "stats" | nc localhost 11211
# 关键指标:
# STAT pid 1234 — 进程 ID
# STAT uptime 86400 — 运行时间(秒)
# STAT curr_connections 50 — 当前连接数
# STAT total_connections 10000 — 总连接数
# STAT cmd_get 1000000 — GET 请求数
# STAT cmd_set 500000 — SET 请求数
# STAT get_hits 950000 — GET 命中数
# STAT get_misses 50000 — GET 未命中数
# STAT bytes 1073741824 — 当前占用字节数
# STAT limit_maxbytes 4294967296 — 最大内存(字节)
# STAT curr_items 100000 — 当前 Item 数
# STAT total_items 500000 — 总 Item 数(含已淘汰)
# STAT evictions 1000 — 淘汰次数
# STAT threads 8 — Worker 线程数
3.8 构建高性能的关键设计
为什么 Memcached 这么快?
| 设计选择 | 性能影响 |
|---|---|
| 纯内存存储 | 无磁盘 I/O,微秒级访问 |
| Slab 预分配 | 无 malloc/free 开销,无碎片 |
| O(1) 哈希查找 | 不随数据量增长而变慢 |
| 多线程 | 充分利用多核 CPU |
| 事件驱动 | 单线程处理数千连接 |
| 简单协议 | 解析开销极小 |
| 无持久化 | 无 fsync / 写盘阻塞 |
| 协议简洁 | 网络传输数据量小 |
扩展阅读
小结
| 要点 | 内容 |
|---|---|
| 内存管理 | Slab 分配器,预分配 1MB 页,切分为固定 chunk |
| 淘汰机制 | LRU 链表,优先淘汰最久未访问的 Item |
| 多线程 | 主线程 accept + Worker 线程池,细粒度锁 |
| 性能来源 | 纯内存 + O(1) 查找 + 事件驱动 + 无持久化 |
| 调优关键 | 启用 lru_maintainer,选择 murmur3 哈希 |