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) return get_num_op_shards() * cct->_conf->osd_op_num_threads_per_shard_hdd; else 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) { uint32_t shard_index = item.get_ordering_token().hash_to_shard(osd->shards.size()); OSDShard* sdata = osd->shards[shard_index]; { std::lock_guard l{sdata->shard_lock}; sdata->scheduler->enqueue(std::move(item)); } 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) { uint32_t shard_index = thread_index % osd->num_shards; OSDShard* sdata = osd->shards[shard_index]; sdata->shard_lock.lock(); if (sdata->scheduler->empty()) { sdata->sdata_cond.wait(...); } WorkItem work_item = sdata->scheduler->dequeue(); auto item = std::move(std::get<OpSchedulerItem>(work_item)); const auto token = item.get_ordering_token(); auto r = sdata->pg_slots.emplace(token, nullptr); if (r.second) { r.first->second = make_unique<OSDShardPGSlot>(); } OSDShardPGSlot *slot = r.first->second.get(); slot->to_process.push_back(std::move(item)); PGRef pg = slot->pg; if (pg) { pg->lock(); } auto qi = std::move(slot->to_process.front()); slot->to_process.pop_front(); qi.run(osd, sdata, pg, tp_handle); 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; unique_ptr<OpScheduler> scheduler; map<spg_t, unique_ptr<OSDShardPGSlot>> pg_slots; ceph::mutex sdata_wait_lock; ceph::condition_variable sdata_cond; uint32_t waiting_threads; bool stop_waiting; list<Context*> context_queue; };
|
4.2 OSDShardPGSlot(PG 槽位)
1 2 3 4 5 6 7 8 9 10
| class OSDShardPGSlot { PGRef pg; list<OpSchedulerItem> to_process; list<OpSchedulerItem> waiting; list<OpSchedulerItem> waiting_peering; uint64_t requeue_seq; uint32_t num_running; set<spg_t> waiting_for_split; };
|
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 调度优先级
操作根据以下因素确定优先级:
- 操作类型:Peering 事件通常优先级较高
- 客户端优先级:客户端请求的优先级
- 成本:操作的预期成本(IO 数量、延迟等)
- 时间戳:操作的时间戳
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 等待条件
线程在以下情况会等待:
- 调度器队列为空:等待新操作到达
- 操作被调度到未来:等待调度时间到达
- PG 不存在:等待 PG 创建或加载
- Map epoch 不匹配:等待 OSDMap 更新
7.2 唤醒机制
1 2 3 4 5
| sdata->sdata_cond.notify_all();
sdata->sdata_cond.wait(lock);
|
8. 特殊处理场景
8.1 PG 不存在
1 2 3 4 5 6 7 8 9 10
| while (!pg) { if (create_info) { pg = osd->handle_pg_create_info(osdmap, create_info); } 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()) { _add_slot_waiter(token, slot, std::move(qi)); }
|
8.3 PG 分裂
1 2 3 4
| if (!slot->waiting_for_split.empty()) { _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 通过以下机制实现高效的操作处理:
- 分片架构:将操作按 PG 分片,减少锁竞争
- 多线程并发:每个分片多个线程,提高吞吐量
- 智能调度:根据优先级、成本等因素调度操作
- 顺序保证:同一 PG 的操作串行处理,保证一致性
- 等待机制:合理等待 PG 创建、Map 更新等条件
这种设计在保证操作顺序性的同时,最大化了并发性能。
正在加载留言…