OSD 操作线程池(osd_op_tp)工作机制分析

OSD 操作线程池(osd_op_tp)工作机制分析

1. 概述

osd_op_tp 是 OSD 中用于处理客户端操作和内部操作的核心线程池。它采用分片(Sharded)架构,将操作按照 PG 进行分片,每个分片有独立的调度队列,多个工作线程并发处理不同分片的操作。

2. 架构设计

2.1 核心组件

1
2
3
4
5
6
7
8
osd_op_tp (ShardedThreadPool)
├── 多个工作线程(根据配置创建)
└── op_shardedwq (ShardedOpWQ)
├── 多个分片(OSDShard)
│ ├── scheduler (操作调度器)
│ ├── pg_slots (PG 槽位映射)
│ └── context_queue (上下文队列)
└── 操作入队/出队逻辑

2.2 线程数量计算

1
2
3
4
5
6
7
8
9
10
11
12
int OSD::get_num_op_threads()
{
// 如果明确配置了每个分片的线程数
if (cct->_conf->osd_op_num_threads_per_shard)
return get_num_op_shards() * cct->_conf->osd_op_num_threads_per_shard;

// 根据存储类型选择不同的线程数
if (store_is_rotational) // HDD
return get_num_op_shards() * cct->_conf->osd_op_num_threads_per_shard_hdd;
else // SSD
return get_num_op_shards() * cct->_conf->osd_op_num_threads_per_shard_ssd;
}

线程数 = 分片数 × 每个分片的线程数

3. 工作流程

3.1 操作入队(Enqueue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void OSD::ShardedOpWQ::_enqueue(OpSchedulerItem&& item)
{
// 1. 根据 PG ID 计算分片索引(哈希)
uint32_t shard_index =
item.get_ordering_token().hash_to_shard(osd->shards.size());

// 2. 获取对应的分片
OSDShard* sdata = osd->shards[shard_index];

// 3. 将操作加入调度器队列
{
std::lock_guard l{sdata->shard_lock};
sdata->scheduler->enqueue(std::move(item));
}

// 4. 通知等待的线程
sdata->sdata_cond.notify_all();
}

关键点:

  • 使用 PG ID 的哈希值确定分片,确保同一 PG 的操作总是路由到同一分片
  • 这保证了同一 PG 的操作顺序性

3.2 操作处理(Process)

每个工作线程执行 _process 方法,这是核心处理循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void OSD::ShardedOpWQ::_process(uint32_t thread_index, 
ceph::heartbeat_handle_d *hb)
{
// 1. 确定处理哪个分片
uint32_t shard_index = thread_index % osd->num_shards;
OSDShard* sdata = osd->shards[shard_index];

// 2. 检查队列是否为空
sdata->shard_lock.lock();
if (sdata->scheduler->empty()) {
// 等待新操作到达
sdata->sdata_cond.wait(...);
}

// 3. 从调度器出队一个操作
WorkItem work_item = sdata->scheduler->dequeue();
auto item = std::move(std::get<OpSchedulerItem>(work_item));

// 4. 获取或创建 PG 槽位
const auto token = item.get_ordering_token(); // PG ID
auto r = sdata->pg_slots.emplace(token, nullptr);
if (r.second) {
r.first->second = make_unique<OSDShardPGSlot>();
}
OSDShardPGSlot *slot = r.first->second.get();

// 5. 将操作加入 PG 槽位的待处理队列
slot->to_process.push_back(std::move(item));

// 6. 获取 PG 引用并加锁
PGRef pg = slot->pg;
if (pg) {
pg->lock(); // 获取 PG 锁
}

// 7. 从待处理队列取出操作
auto qi = std::move(slot->to_process.front());
slot->to_process.pop_front();

// 8. 执行操作
qi.run(osd, sdata, pg, tp_handle);

// 9. 释放 PG 锁
if (pg) {
pg->unlock();
}
}

3.3 操作执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
操作入队

根据 PG ID 哈希到分片

加入调度器队列

工作线程从调度器出队

加入 PG 槽位的 to_process 队列

获取 PG 锁

从 to_process 取出操作

执行操作(qi.run())
├── 客户端操作 → PG::do_op()
├── Peering 事件 → PG::do_peering_event()
└── 其他操作 → 相应处理函数

释放 PG 锁

4. 关键数据结构

4.1 OSDShard(分片)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OSDShard {
ceph::mutex shard_lock; // 分片锁
OSDMapRef shard_osdmap; // 分片的 OSDMap 引用
unique_ptr<OpScheduler> scheduler; // 操作调度器
map<spg_t, unique_ptr<OSDShardPGSlot>> pg_slots; // PG 槽位映射

// 等待机制
ceph::mutex sdata_wait_lock;
ceph::condition_variable sdata_cond;
uint32_t waiting_threads; // 等待的线程数
bool stop_waiting; // 停止等待标志

list<Context*> context_queue; // 上下文队列(用于 oncommit 回调)
};

4.2 OSDShardPGSlot(PG 槽位)

1
2
3
4
5
6
7
8
9
10
class OSDShardPGSlot {
PGRef pg; // PG 引用
list<OpSchedulerItem> to_process; // 待处理操作队列
list<OpSchedulerItem> waiting; // 等待操作队列
list<OpSchedulerItem> waiting_peering; // 等待 Peering 的操作

uint64_t requeue_seq; // 重新入队序列号
uint32_t num_running; // 正在运行的操作数
set<spg_t> waiting_for_split; // 等待分裂的 PG
};

4.3 OpSchedulerItem(操作调度项)

包含:

  • ordering_token:排序令牌(通常是 PG ID),用于确定分片
  • map_epoch:操作对应的 OSDMap epoch
  • 操作类型:客户端操作、Peering 事件等
  • 操作数据:OpRequestRef、PGPeeringEventRef 等

5. 调度策略

5.1 调度器类型

OSD 支持多种调度器:

  • mClockScheduler:基于 mClock 算法的 QoS 调度器
  • WeightedPriorityQueue:加权优先级队列
  • SimpleQueue:简单队列(用于测试)

5.2 调度优先级

操作根据以下因素确定优先级:

  1. 操作类型:Peering 事件通常优先级较高
  2. 客户端优先级:客户端请求的优先级
  3. 成本:操作的预期成本(IO 数量、延迟等)
  4. 时间戳:操作的时间戳

6. 并发控制

6.1 分片级别的并发

  • 每个分片有独立的锁(shard_lock
  • 不同分片的操作可以并发处理
  • 同一分片内的操作串行处理(通过调度器)

6.2 PG 级别的并发

  • 每个 PG 有自己的锁(PG::lock()
  • 同一 PG 的操作必须串行处理
  • 不同 PG 的操作可以并发处理

6.3 线程分配策略

1
uint32_t shard_index = thread_index % osd->num_shards;
  • 线程通过 thread_index % num_shards 分配到分片
  • 多个线程可以处理同一分片(提高并发度)
  • 但同一 PG 的操作仍然串行(通过 PG 锁保证)

7. 等待和唤醒机制

7.1 等待条件

线程在以下情况会等待:

  1. 调度器队列为空:等待新操作到达
  2. 操作被调度到未来:等待调度时间到达
  3. PG 不存在:等待 PG 创建或加载
  4. Map epoch 不匹配:等待 OSDMap 更新

7.2 唤醒机制

1
2
3
4
5
// 入队时唤醒
sdata->sdata_cond.notify_all(); // 或 notify_one()

// 等待条件
sdata->sdata_cond.wait(lock);

8. 特殊处理场景

8.1 PG 不存在

1
2
3
4
5
6
7
8
9
10
while (!pg) {
// 检查是否需要创建 PG
if (create_info) {
pg = osd->handle_pg_create_info(osdmap, create_info);
}
// 或者等待 PG 加载
else {
_add_slot_waiter(token, slot, std::move(qi));
}
}

8.2 Map Epoch 不匹配

1
2
3
4
if (qi.get_map_epoch() > osdmap->get_epoch()) {
// 操作需要更新的 OSDMap,等待 Map 更新
_add_slot_waiter(token, slot, std::move(qi));
}

8.3 PG 分裂

1
2
3
4
if (!slot->waiting_for_split.empty()) {
// PG 正在分裂,等待分裂完成
_add_slot_waiter(token, slot, std::move(qi));
}

9. 性能优化

9.1 分片设计

  • 减少锁竞争:不同分片使用不同的锁
  • 提高缓存局部性:同一 PG 的操作总是在同一分片处理
  • 负载均衡:通过哈希均匀分布 PG 到分片

9.2 调度优化

  • 优先级调度:重要操作优先处理
  • 成本感知:考虑操作的 IO 成本
  • QoS 保证:通过 mClock 算法保证不同客户端的服务质量

9.3 批量处理

  • 上下文队列:批量处理 oncommit 回调
  • 操作合并:某些操作可以合并处理

10. 配置参数

参数 说明 默认值
osd_op_num_threads_per_shard 每个分片的线程数 -
osd_op_num_threads_per_shard_hdd HDD 每个分片的线程数 2
osd_op_num_threads_per_shard_ssd SSD 每个分片的线程数 3
osd_op_thread_timeout 线程超时时间 60s
osd_op_thread_suicide_timeout 线程自杀超时 300s

11. 总结

osd_op_tp 通过以下机制实现高效的操作处理:

  1. 分片架构:将操作按 PG 分片,减少锁竞争
  2. 多线程并发:每个分片多个线程,提高吞吐量
  3. 智能调度:根据优先级、成本等因素调度操作
  4. 顺序保证:同一 PG 的操作串行处理,保证一致性
  5. 等待机制:合理等待 PG 创建、Map 更新等条件

这种设计在保证操作顺序性的同时,最大化了并发性能。

文章互动

阅读 --

留言

0 条留言

正在加载留言…