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

Memcached 完全指南 / 第 6 章:Slab 分配器详解

第 6 章:Slab 分配器详解

6.1 Slab 分配器的作用

Memcached 使用 Slab 分配器管理内存,目的是:

  1. 消除内存碎片:预分配固定大小的 chunk,不需要 malloc/free
  2. 提高分配速度:从空闲链表直接取 chunk,O(1) 操作
  3. 简化内存管理:按 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