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

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 大小的内存分组
Page1MB 的内存块,属于某个 Slab Class
ChunkPage 内的最小存储单元,大小由 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"
Classchunk_sizechunks_per_page说明
196B10,922最小 chunk
2120B8,738
3152B6,926
4192B5,461
5240B4,369
6304B3,463
7384B2,730
8480B2,184
9600B1,747
10752B1,393
11944B1,110
121,184B885
131,480B708
141,856B564
152,320B451
162,904B360
173,632B288
184,544B230
195,680B184
207,104B147
42~1MB1最大 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