强曰为道

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

第 9 章:多线程模型

第 9 章:多线程模型

9.1 线程架构概览

Memcached 从 1.2 版本开始支持多线程,采用 主线程 + Worker 线程池 的 Reactor 模型。

┌──────────────────────────────────────────────────────────────┐
│                      Memcached 进程                          │
│                                                              │
│  ┌──────────────┐                                           │
│  │   主线程      │                                           │
│  │   (Main)      │                                           │
│  │              │                                           │
│  │ - socket()   │    ┌──────────────────────────────────┐   │
│  │ - bind()     │    │        Worker 线程池              │   │
│  │ - listen()   │    │                                  │   │
│  │ - accept()   │    │  ┌────────┐  ┌────────┐         │   │
│  │ - dispatch   │───▶│  │Worker 1│  │Worker 2│  ...    │   │
│  │              │    │  │(Thread)│  │(Thread)│         │   │
│  └──────────────┘    │  └────────┘  └────────┘         │   │
│                      └──────────────────────────────────┘   │
│                                          │                   │
│  ┌───────────────────────────────────────┼─────────────────┐│
│  │           共享内存区域                   │                 ││
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐               ││
│  │  │Hash Table│ │Slab Alloc│ │LRU Chains│               ││
│  │  └──────────┘ └──────────┘ └──────────┘               ││
│  └───────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────┘

线程分工

线程类型数量职责
主线程1监听端口,accept 连接,Round-Robin 分发
Worker 线程-t 参数(默认 4)处理已连接客户端的读写和命令
LRU 维护线程1(可选)后台 LRU 链表维护
LRU 爬虫线程1(可选)后台清理过期 Item
Slab 迁移线程1(可选)Slab Class 间内存迁移

9.2 Reactor 模型详解

单线程 Reactor

早期 Memcached(1.2 之前)使用单线程 Reactor:

单线程 Reactor:
┌─────────────────────────────────────────┐
│              单线程                       │
│                                         │
│  while(true) {                          │
│      events = epoll_wait();             │
│      for each event:                    │
│          if (listen_fd) → accept()      │
│          if (client_fd readable) → read │
│          if (client_fd writable) → write│
│  }                                      │
│                                         │
│  所有连接在同一个 epoll 中管理           │
└─────────────────────────────────────────┘

优点:无锁,简单
缺点:无法利用多核 CPU

多线程 Reactor(当前架构)

多线程 Reactor(一主多从):

主线程:                               Worker 1:
┌─────────────────────┐              ┌─────────────────────┐
│  listen_epoll       │              │  client_epoll        │
│  ┌──────────────┐   │   pipe(2)    │  ┌──────────────┐   │
│  │ accept()     │───┼──────────────┼─▶│ 新连接加入    │   │
│  └──────────────┘   │              │  └──────────────┘   │
│                     │              │                      │
│  Round-Robin 选择   │              │  event_base_loop()   │
│  下一个 Worker      │              │  处理所有已分配连接   │
└─────────────────────┘              └─────────────────────┘

连接分发流程:
1. 主线程 accept() 获得新连接 fd
2. 轮询选择下一个 Worker 线程
3. 通过 pipe/eventfd 通知 Worker
4. Worker 将 fd 加入自己的 epoll
5. Worker 后续独立处理该连接的读写

9.3 线程间通信

主线程 → Worker 通知机制

// 每个 Worker 线程有一个 pipe
struct worker_thread {
    pthread_t thread_id;
    int notify_send_fd;   // 主线程写入
    int notify_recv_fd;   // Worker 读取
    struct event_base *base;
    // ...
};

// 主线程分发连接
void dispatch_conn(int client_fd, int worker_idx) {
    char buf[1];
    buf[0] = 'c';  // 'c' = new connection
    write(workers[worker_idx].notify_send_fd, buf, 1);
}

// Worker 收到通知
void on_notify(int fd, short event, void *arg) {
    char buf[1];
    read(fd, buf, 1);
    if (buf[0] == 'c') {
        // 接收新连接,注册到自己的 event_base
        int client_fd = receive_fd_from_main();
        add_client_to_epoll(client_fd);
    }
}

9.4 锁机制

锁类型与粒度

Memcached 中的锁:

1. 全局锁 (stats_lock)
   用途: 保护全局统计信息
   竞争: 低(只有 stats 更新时需要)

2. Slab 锁 (slabs_lock)
   用途: 保护 Slab 分配/释放
   竞争: 中(每次 set/delete 都需要)

3. 哈希表锁 (hash_lock 或 hash_item_lock)
   用途: 保护哈希表查找/插入/删除
   竞争: 中高(每次操作都需要)

4. LRU 锁 (lru_locks[class_id])
   用途: 保护 LRU 链表操作
   竞争: 高(频繁的 get 会触发 LRU 移动)

5. 连接锁 (conn_lock)
   用途: 保护连接列表
   竞争: 低(只有连接/断开时需要)

锁性能影响

锁竞争热点分析:

高并发读场景 (90% get + 10% set):
  - LRU 锁: 高竞争(每次 get 命中要移动 LRU)
  - Hash 锁: 中竞争
  - Slab 锁: 低竞争

高并发写场景 (10% get + 90% set):
  - Slab 锁: 高竞争(频繁分配 chunk)
  - Hash 锁: 高竞争(频繁插入/更新)
  - LRU 锁: 中竞争

9.5 连接数管理

最大连接数

# 设置最大连接数
memcached -c 65535

# 系统层面也需要调整
ulimit -n 65535

连接数监控

echo "stats" | nc localhost 11211 | grep -i conn
# STAT max_connections 1024       ← 最大连接数
# STAT curr_connections 50        ← 当前连接数
# STAT total_connections 10000    ← 总连接数(累计)
# STAT connection_structures 55   ← 已分配的连接结构数
# STAT rejected_connections 10    ← 被拒绝的连接数(达到上限)

连接数调优

# 检查当前连接数使用率
echo "stats" | nc localhost 11211 | grep -E "curr_connections|max_connections"

# 经验公式:
# max_connections = 预估并发连接数 × 1.5
# 例: 预估 5000 并发 → -c 7500

maxconns_fast 参数

# 默认:连接超限时返回错误但有延迟
# maxconns_fast:快速拒绝(推荐)

memcached -o maxconns_fast
不启用 maxconns_fast:
  新连接 → accept() → 发现超限 → close()
  问题: accept 后再 close,有时间窗口

启用 maxconns_fast:
  新连接 → 检查 curr_connections → 超限 → 立即拒绝
  优势: 减少无效连接开销

9.6 线程数调优

设置 Worker 线程数

# 设置线程数
memcached -t 8

线程数选择指南

CPU 核心数推荐线程数说明
22小规模部署
44默认推荐
86-8中等规模
168-12大规模部署
32+12-16超大规模,不必等于核心数

线程数过多的问题

线程数过多的负面影响:

1. 锁竞争加剧
   - 更多线程 → 更多并发访问共享数据 → 锁等待增加

2. 上下文切换开销
   - 每次上下文切换约 1-10μs
   - 线程过多 → 频繁切换 → CPU 利用率反而下降

3. 内存开销
   - 每个线程约 2-8MB 栈空间
   - 16 线程 → 32-128MB 额外内存

4. 事件循环效率
   - 每个 Worker 有自己的 epoll
   - 连接被分摊到更多 Worker → 每个 Worker 的 epoll 事件更少

线程数 vs QPS

典型性能曲线:

QPS (万)
│
│          ___________
│         /           \
│        /             \
│       /               \
│      /                 \
│     /                   \
│    /                     \
│   /                       \
│──/─────────────────────────\─────
│                                线程数
  1  2  4  6  8  12  16  24  32

最佳线程数通常在 6-12 之间(取决于 CPU 核心数和负载类型)

9.7 高级调优参数

事件处理参数

# -R: 每个事件最大处理请求数(默认 20)
memcached -R 50

# 含义:在一次事件循环中,每个连接最多处理 R 个请求
# 增大 R → 单连接吞吐更高,但可能饿死其他连接
# 减小 R → 更公平,但单连接延迟增加

监听队列

# -b: 监听队列长度(默认 1024)
memcached -b 4096

# 高并发场景下,增加监听队列可以减少连接拒绝

TCP 参数

# 通过 -o 设置 TCP 选项
memcached -o tcp_nodelay

# tcp_nodelay: 禁用 Nagle 算法,减少小包延迟
# 推荐在低延迟场景下启用

9.8 性能基准测试

使用 mc-perf 测试

# 安装 mc-perf(Memcached 自带的性能测试工具)
# 在源码目录中编译
cd /tmp/memcached-1.6.31
make mc-perf 2>/dev/null || make -C tools

# 基准测试
./mc-perf -s localhost:11211 -t 4 -c 50 -n 100000

# 参数说明:
# -s: 服务器地址
# -t: 客户端线程数
# -c: 每线程并发连接数
# -n: 请求总数

使用 memtier_benchmark 测试

# 安装 memtier_benchmark
sudo apt-get install -y memtier-benchmark

# 基准测试
memtier_benchmark \
    -s localhost \
    -p 11211 \
    -P memcache_text \
    -t 4 \
    -c 50 \
    -n 100000 \
    --ratio=1:1 \
    --data-size=128 \
    --key-pattern=R:R

# 输出示例:
# Type        Ops/sec     Hits/sec   Misses/sec    Avg Latency
# SET         185,432     --         --            0.267 ms
# GET         185,432     178,001    7,431         0.271 ms
# Totals      370,864     178,001    7,431         0.269 ms

不同线程数的性能对比

#!/bin/bash
# 线程数性能对比测试
for threads in 1 2 4 6 8 12 16; do
    echo "=== Testing with $threads threads ==="
    pkill memcached
    sleep 1
    memcached -m 256 -t $threads -c 10000 -o lru_maintainer -d
    sleep 1
    memtier_benchmark \
        -s localhost -p 11211 -P memcache_text \
        -t 4 -c 25 -n 50000 \
        --ratio=1:1 --data-size=128 \
        --hide-histogram 2>/dev/null | grep "Totals"
done

9.9 连接池配置

为什么需要连接池?

无连接池:
  每次请求: 创建连接 → 发送命令 → 关闭连接
  开销: connect() + TCP 握手 + close()

有连接池:
  初始化: 创建 N 个持久连接
  每次请求: 从池中取连接 → 发送命令 → 归还连接
  开销: 仅数据传输

各语言连接池配置

PHP:

<?php
$mc = new Memcached('persistent_pool_1');
$mc->addServers([
    ['mc1', 11211, 100],
    ['mc2', 11211, 100],
]);
$mc->setOption(Memcached::OPT_RECV_TIMEOUT, 100000);
$mc->setOption(Memcached::OPT_SEND_TIMEOUT, 100000);
$mc->setOption(Memcached::OPT_TCP_NODELAY, true);

Java (SpyMemcached):

ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder()
    .setProtocol(Protocol.TEXT)
    .setOpTimeout(1000)
    .setShouldOptimize(true)
    .setFailureMode(FailureMode.Redistribute)
    .setLocatorType(LocatorType.CONSISTENT);

MemcachedClient mc = new MemcachedClient(
    builder.build(),
    AddrUtil.getAddresses("mc1:11211 mc2:11211 mc3:11211")
);

Go:

mc := memcache.New("mc1:11211", "mc2:11211", "mc3:11211")
mc.Timeout = 100 * time.Millisecond
mc.MaxIdleConns = 50  // 每个服务器最大空闲连接

扩展阅读

小结

要点内容
架构主线程 accept + Worker 线程池,通过 pipe 通知
线程数-t 设置,推荐 6-12,不必等于 CPU 核心数
锁粒度Slab 锁 / Hash 锁 / LRU 锁,影响并发性能
连接数-c 设置最大连接数,启用 maxconns_fast
关键参数-R 控制事件处理量,tcp_nodelay 降低延迟