Memcached 完全指南 / 第 6 章:Slab 分配器详解
第 6 章:Slab 分配器详解
6.1 Slab 分配器的作用
Memcached 使用 Slab 分配器管理内存,目的是:
- 消除内存碎片:预分配固定大小的 chunk,不需要
malloc/free - 提高分配速度:从空闲链表直接取 chunk,O(1) 操作
- 简化内存管理:按 Page 为单位向操作系统申请,按 Chunk 为单位分配给 Item
6.2 核心概念
层次结构
Memcached 总内存 (-m 参数)
│
├── Slab Class 1 (chunk_size = 96B)
│ ├── Page 1 (1MB) → [chunk1][chunk2][chunk3]...[chunkN]
│ ├── Page 2 (1MB) → [chunk1][chunk2][chunk3]...[chunkN]
│ └── ...
│
├── Slab Class 2 (chunk_size = 120B)
│ ├── Page 1 (1MB) → [chunk1][chunk2]...[chunkN]
│ └── ...
│
├── Slab Class 3 (chunk_size = 152B)
│ └── ...
│
└── Slab Class N (chunk_size ≈ 1MB)
└── Page 1 (1MB) → [chunk1]
关键术语
| 术语 | 说明 |
|---|---|
| Slab Class | 具有相同 chunk 大小的内存分组 |
| Page | 1MB 的内存块,属于某个 Slab Class |
| Chunk | Page 内的最小存储单元,大小由 Slab Class 决定 |
| Item | 存储的 K/V 数据结构,占用 1 个或多个 chunk |
| Growth Factor | 相邻 Slab Class chunk 大小的比值(-f 参数) |
| Min Size | 最小 chunk 空间(-n 参数,默认 48B) |
6.3 Chunk 大小计算
计算公式
Slab Class 1: chunk_size = max(96, align8(48 + 2 * item_overhead))
Slab Class N: chunk_size = align8(chunk_size[N-1] * factor)
其中:
align8(x) = 向上对齐到 8 字节
factor = -f 参数 (默认 1.25)
item_overhead = 48 字节 (Item 头部固定开销)
默认配置下的 Slab Class 列表
# 查看当前实例所有 Slab Class
echo "stats slabs" | nc localhost 11211 | grep -E "chunk_size|chunks_per_page"
| Class | chunk_size | chunks_per_page | 说明 |
|---|---|---|---|
| 1 | 96B | 10,922 | 最小 chunk |
| 2 | 120B | 8,738 | |
| 3 | 152B | 6,926 | |
| 4 | 192B | 5,461 | |
| 5 | 240B | 4,369 | |
| 6 | 304B | 3,463 | |
| 7 | 384B | 2,730 | |
| 8 | 480B | 2,184 | |
| 9 | 600B | 1,747 | |
| 10 | 752B | 1,393 | |
| 11 | 944B | 1,110 | |
| 12 | 1,184B | 885 | |
| 13 | 1,480B | 708 | |
| 14 | 1,856B | 564 | |
| 15 | 2,320B | 451 | |
| 16 | 2,904B | 360 | |
| 17 | 3,632B | 288 | |
| 18 | 4,544B | 230 | |
| 19 | 5,680B | 184 | |
| 20 | 7,104B | 147 | |
| … | … | … | |
| 42 | ~1MB | 1 | 最大 chunk |
注意:实际 chunk 数量略少于
1MB / chunk_size,因为每个 Page 有少量元数据开销。
自定义增长因子
# 更平缓的增长(factor=1.1)
memcached -f 1.1 -n 48 -m 64
# → 更多 Slab Class,更小的 chunk 大小差异,减少内部碎片
# 更陡峭的增长(factor=2.0)
memcached -f 2.0 -n 48 -m 64
# → 更少 Slab Class,chunk 大小差异大,可能浪费更多空间
增长因子的影响:
| 因子 | Slab Class 数 | 内部碎片 | 内存利用率 |
|---|---|---|---|
| 1.05 | ~60 | 极低 | 高 |
| 1.10 | ~45 | 低 | 较高 |
| 1.25 | ~38 | 适中 | 适中(推荐) |
| 1.50 | ~24 | 较高 | 较低 |
| 2.00 | ~17 | 高 | 低 |
6.4 内存分配流程
SET 操作的内存分配
1. 计算 Item 所需空间:
total_size = sizeof(item) + key_len + suffix_len + value_len
= 48 + key_len + 8 + value_len
2. 选择合适的 Slab Class:
从 Class 1 开始,找到 chunk_size >= total_size 的最小 Class
例: total_size = 130B → Class 3 (chunk_size = 152B)
3. 从 Slab Class 的 freelist 获取空闲 chunk:
├── freelist 非空 → 取出一个 chunk → 返回
└── freelist 为空 → 分配新 Page
├── 成功 → 1MB 切分为 chunks → 放入 freelist → 取出一个
└── 失败(总内存用尽)→ 尝试 LRU 淘汰
├── 有可淘汰的 Item → 释放其 chunk → 重用
└── 无可淘汰 → 返回错误
4. 初始化 Item 头部,写入 Key/Value
5. 插入 Hash 表和 LRU 链表
查看分配情况
# Slab 统计
echo "stats slabs" | nc localhost 11211
# STAT 1:chunk_size 96
# STAT 1:chunks_per_page 10922
# STAT 1:total_pages 5 ← 分配了 5 个 Page (5MB)
# STAT 1:total_chunks 54610 ← 总 chunk 数
# STAT 1:used_chunks 1234 ← 已使用 chunk 数
# STAT 1:free_chunks 53376 ← 空闲 chunk 数
# STAT 1:free_chunks_end 0 ← Page 末尾空闲 chunk
# STAT 1:mem_requested 114504 ← 用户请求的实际字节数
# STAT 1:get_hits 10000
# STAT 1:cmd_set 5000
# STAT 1:delete_hits 200
# STAT 1:incr_hits 100
# STAT 1:decr_hits 50
# STAT 1:cas_hits 30
# STAT 1:cas_badval 5
# 全局 Slab 统计
# STAT active_slabs 15 ← 活跃的 Slab Class 数
# STAT total_malloced 16777216 ← 总分配内存 (16MB)
6.5 内部碎片分析
什么是内部碎片?
当 Item 大小不能填满整个 chunk 时,剩余空间被浪费,这就是内部碎片(Internal Fragmentation)。
例: Item 需要 130B,分配了 152B 的 chunk
┌─────────────────────────────────────────┐
│ Chunk (152B) │
├───────────────────────────┬─────────────┤
│ Item 数据 (130B) │ 浪费 (22B) │
│ [header|key|suffix|val] │ (14.5%) │
└───────────────────────────┴─────────────┘
计算内存利用率
# 理论内存 = total_chunks * chunk_size
# 实际使用 = mem_requested
# 利用率 = mem_requested / (total_chunks * chunk_size) * 100%
echo "stats slabs" | nc localhost 11211
#!/usr/bin/env python3
"""计算 Memcached 内存利用率"""
import socket
def get_slab_stats(host='localhost', port=11211):
s = socket.socket()
s.connect((host, port))
s.send(b'stats slabs\r\n')
data = b''
while True:
chunk = s.recv(4096)
data += chunk
if b'END\r\n' in chunk:
break
s.close()
return data.decode()
def analyze():
raw = get_slab_stats()
slabs = {}
for line in raw.split('\r\n'):
if line.startswith('STAT ') and ':' in line:
parts = line.split()
key, value = parts[1], parts[2]
class_id, prop = key.split(':', 1)
if class_id not in slabs:
slabs[class_id] = {}
slabs[class_id][prop] = int(value)
total_mem = 0
total_requested = 0
print(f"{'Class':>6} {'chunk':>8} {'pages':>6} {'used':>8} {'req(B)':>10} {'util%':>7}")
print("-" * 55)
for cid in sorted(slabs.keys(), key=int):
s = slabs[cid]
if 'chunk_size' not in s:
continue
cs = s['chunk_size']
tp = s.get('total_pages', 0)
uc = s.get('used_chunks', 0)
mr = s.get('mem_requested', 0)
alloc = tp * 1048576 # pages * 1MB
util = (mr / alloc * 100) if alloc > 0 else 0
total_mem += alloc
total_requested += mr
print(f"{cid:>6} {cs:>8} {tp:>6} {uc:>8} {mr:>10} {util:>6.1f}%")
print("-" * 55)
overall = (total_requested / total_mem * 100) if total_mem > 0 else 0
print(f"{'Total':>6} {'':>8} {'':>6} {'':>8} {total_requested:>10} {overall:>6.1f}%")
if __name__ == '__main__':
analyze()
输出示例:
Class chunk pages used req(B) util%
-------------------------------------------------------
1 96 5 1234 105890 20.2%
2 120 3 567 62370 19.8%
3 152 2 234 32760 16.1%
4 192 4 890 156670 20.3%
...
-------------------------------------------------------
Total 5242880 35.8%
6.6 Slab Calc 工具
Memcached 自带 slab calc 工具,用于预估不同参数下的内存分配:
# 查看默认参数下的 Slab 分配
memcached -h | grep -A 50 "slab"
# 或
slab calc # 某些发行版提供的独立工具
手动计算示例
#!/usr/bin/env python3
"""Slab Calculator - 预估 Slab 分配"""
def calc_slabs(factor=1.25, min_size=48, max_size=1048576, max_bytes=67108864):
"""
计算 Slab Class 分配情况
factor: 增长因子
min_size: 最小 item 空间 (bytes)
max_size: 最大 chunk (1MB)
max_bytes: 总内存 (默认 64MB)
"""
item_overhead = 48 # Item 头部开销
align = 8 # 8 字节对齐
# 计算 Slab Class 1 的 chunk 大小
size = max(96, (min_size + item_overhead + align - 1) & ~(align - 1))
classes = []
while size <= max_size:
chunks_per_page = max_size // size
classes.append({
'class': len(classes) + 1,
'chunk_size': size,
'chunks_per_page': chunks_per_page,
})
# 下一个 Slab Class 的 chunk 大小
size = int(size * factor)
size = (size + align - 1) & ~(align - 1)
print(f"参数: factor={factor}, min_size={min_size}, total={max_bytes/1048576}MB")
print(f"Slab Class 数量: {len(classes)}")
print()
print(f"{'Class':>6} {'chunk_size':>12} {'chunks/page':>12} {'适用 Value':>14}")
print("-" * 50)
for c in classes:
cs = c['chunk_size']
cpp = c['chunks_per_page']
# 估算适用的 Value 大小范围
max_val = cs - item_overhead
print(f"{c['class']:>6} {cs:>10}B {cpp:>12} {max_val:>10}B")
# 不同 factor 对比
calc_slabs(factor=1.25)
print()
calc_slabs(factor=1.10)
6.7 Slab 内存迁移
问题:Slab Calc 不准怎么办?
实际业务中,Item 大小分布可能不均匀。某些 Slab Class 内存用尽,而其他 Class 大量空闲。
解决方案:Slab Reassign
# 启用 Slab 重分配
memcached -o slab_reassign,slab_automove
# 手动触发 Slab 迁移
# (通过 stats 命令不直接支持,需要通过 meta 协议或工具)
slab_automove 工作原理
1. LRU 爬虫定期扫描每个 Slab Class 的 LRU 尾部
2. 如果某个 Class 的 LRU 尾部 Item 仍然很新(刚被访问)
→ 说明该 Class 需要更多内存
3. 如果某个 Class 有大量空闲 Page
→ 可以将 Page 迁移给需要的 Class
4. 每 10 秒检查一次,每次迁移 1 个 Page
# 查看 Slab 迁移统计
echo "stats slabs" | nc localhost 11211 | grep -E "reassign|automove"
# STAT slab_reassign_running 0
# STAT slabs_moved 42 ← 已迁移的 Page 数
slab_automove 调优参数
# 自动迁移(保守模式)
memcached -o slab_automove=1
# 自动迁移(激进模式,每次迁移更多 Page)
memcached -o slab_automove=2
| 模式 | 说明 |
|---|---|
| 0 | 禁用自动迁移 |
| 1 | 保守模式(默认推荐) |
| 2 | 激进模式(内存压力大时使用) |
6.8 内存碎片优化实践
策略一:统一 Value 大小
# 好的做法:Value 大小集中在 1-2 个 Slab Class
# 例如:缓存的用户信息都在 200-250B 之间
# → 主要使用 Slab Class 5 (chunk=240B)
# 不好的做法:Value 大小差异极大
# 10B 到 500KB 随机分布
# → 大量 Slab Class,碎片严重
策略二:调整增长因子
# Value 大小分布均匀时,使用较小的 factor
memcached -f 1.10 -m 4096
# Value 大小差异大时,保持默认
memcached -f 1.25 -m 4096
策略三:使用 -n 调整最小 Item 空间
# 如果你的 Key 较长(如 UUID + 前缀)
# 计算最小 item 大小: 48(头部) + key_len + 8(后缀) + value_len
# 调整 -n 参数避免落入过大的 Slab Class
# 例: key 平均 40B, value 平均 20B → 总需 116B
# 使用 -n 68 使最小 chunk 覆盖此场景
memcached -n 68 -f 1.25
策略四:监控 + 手动迁移
#!/bin/bash
# 监控 Slab 分布,发现碎片问题
while true; do
echo "=== $(date) ==="
echo "stats slabs" | nc localhost 11211 | grep -E "chunk_size|used_chunks|total_chunks|mem_requested"
sleep 60
done
6.9 常见问题
Q: STAT eviction 不断增长,但很多 Slab Class 有空闲?
A: 这是经典的 Slab Calc 不准问题。Item 落在某些 Class 中导致那些 Class 的 Page 用完,而其他 Class 有大量空闲 Page。
解决:
# 启用自动 Slab 迁移
memcached -o slab_automove,slab_reassign
Q: 如何选择合适的 -m 参数?
A: 经验公式:
所需内存 = 预估 Item 数 × 平均 chunk 大小 × 1.2(碎片余量)
例: 100 万 Item × 300B × 1.2 = 360MB
→ 设置 -m 512(留有余量)
Q: 能否动态修改 Slab 配置?
A: 不能。Slab 配置在启动时确定,运行时无法修改。需要重启进程。
扩展阅读
小结
| 要点 | 内容 |
|---|---|
| Slab Class | 由 -f 决定的 chunk 大小系列 |
| 内部碎片 | Item 不能填满整个 chunk,剩余空间浪费 |
| slab_automove | 推荐启用,在 Slab Class 间自动迁移内存页 |
| 调优核心 | 让 Item 均匀分布在 1-2 个 Slab Class |
-n 参数 | 调整最小 Item 空间,优化首个 Slab Class |