强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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 哈希