第 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 核心数 | 推荐线程数 | 说明 |
|---|---|---|
| 2 | 2 | 小规模部署 |
| 4 | 4 | 默认推荐 |
| 8 | 6-8 | 中等规模 |
| 16 | 8-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 降低延迟 |