本文主要分析了Ceph读写的关键路径上的一些函数的处理和实现,一些比较细节的函数,有待进一步分析和研究。
接收请求
首先,读写请求都是从ms_fast_dispatch,开始, 它是接收读写消息message的入口,就从这里开始分析读写路径。这部分函数的处理是在网络的回调函数里处理的,所有理论上应该尽可能的简单处理,之后交给后面的OpWQ线程池来主要处理。
OSD::ms_fast_dispatch
void OSD::ms_fast_dispatch(Message *m)
- 首先检查service是否已经停止了,如果停止了,就直接返回
- 调用函数op_tracker.create_request把Message消息转换成OpRequest, 数据结构OpRequest包装了Message,并添加了一些相关的信息,在后面用到的时候,会介绍到。
- 获取nextmap, 应该是最新的osdmap, 和 session
dispatch_op_fast
- 检查is_stopping,有必要每次都检查吗?
- 首先调用函数op_required_epoch(op), 从OpRequest中获取 msg带的epoch,比较msg的 epoch 的比较msg_epoch > osdmap->get_epoch(), 如果 msg 带的epoch 大于osd 最新的epoch,则调用更新自己的epoch,首先检查该请求的链接是否存在,如果不存在,就直接返回
- 更加消息类型,调用相应的类型的处理函数
OSD::handle_op
- 首先调用op_is_discardable,检查该op是否可以discard
- 构建share_map结构体,获取client_session, 从client_session获取last_sent_epoch,调用函数service.should_share_map,来设置share_map.should_send ,主要用于如果epoch不一致,需要通知对方更新最新的epoch,这里和dispatch_op_fast的处理区别是,上次是更新自己,这里是通知对方更新。
需要注意是,client 和 osd 的 epoch 不一致,并不影响读写,只要 epoch的变化不影响本次读写的PG的OSD list的变化。 - 从消息里获取_pgid,从_pg_id里获取pool
- 调用osdmap->raw_pg_to_pg,最终调用pg_pool_t::raw_pg_to_pg函数,对pg做了调整
- 调用osdmap->get_primary_shard函数,由pg_t 转换为spg_t,spgt_t比pg_t多了一个shard_id_t shard, 也就是osd在osd 列表中的序号,这个对于replicatePG无意义,一般设置为NO_SHARD,对于ErasureCodePG,各个chunk的序号比较重要,关系到数据的恢复。get_primary_shard获取EC的primay osd 的shard id
- 调用函数get_pg_or_queue_for_pg, 通过pgid, 获取PG类指针,如果获取成功,就调用enqueue_op处理请求
- 否则,做一些错误检查
总结,本函数主要检查了epoch是否share,其次最主要的是获取读写请求相关的PG类。
PG::queue_op
void PG::queue_op(OpRequestRef& op)
- 加map_lock 锁,该锁保护waiting_for_map列表,判断waiting_for_map列表不为空,就把当前op 加入该列表,直接return。waiting_for_map列表不为空,说明有操作在等待osd map 的更新,说明当前osd map 不信任,不能继续当前的处理。
- 函数op_must_wait_for_map,判断当前的epch 大于 op 的epoch, 则必须加入waiting_for_map等待,这里的osdmap的epoch的判断,是一个PG层的epoch的判断。和前面的判断不在一个层次,这里是需要等待的。当op的epoch 大于 pg的epoch,需要等待更新pg当前的epoch
- 加入osd的op_wq处理队列里
总结,本函数做在PG类里,做PG 层面的相关检查,如果ok,就加入osd的op_wq继续处理。
OSD 的op_wq处理
以下操作都是被op_wq的线程池调用,做相应的处理。op_wq是一个ShardedWQ,具体的实现这里不详细分析,代码比较简单,本文着重分析读写流程。
OSD::dequeue_op
void OSD::dequeue_op(PGRef pg, OpRequestRef op ,ThreadPool::TPHandle &handle)
- 检查是否op->send_map_update,如果需要更新,调用函数service.share_map 通知对方更新osdmap信息。在操作OSD::handle_op里,只是设置了share_map的标记,并设置在op->send_map_update,在这里才真正的发消息。
- 调用 pg->do_request(op, handle) 处理 请求
总结:这里一开始,做share_map操作,接下来在PG里调do_request来处理。
ReplicatedPG::do_request
void ReplicatedPG::do_request( OpRequestRef& op, ThreadPool::TPHandle &handle)
- 调用函数can_discard_request检查op是否可以discard,具体可查找该函数源代码。
- 检查flushes_in_progress,如果还有flush操作,则把op加入waiting_for_peered队列,等待。
- 如果PG还没有peered,检查pgbackend能否处理该请求,否则加入waiting_for_peered队列,等待pg完成peered后再处理。
- 检查pgbacnehd能否处理该请求,如果能处理,就返回
- 如果是CEPH_MSG_OSD_OP, 检查该PG的状态,如果处于非active或者reply状态,则把请求添加到waiting_for_active等待队列,检查如果该pool是CachePool,而该操作没有带CEPH_FEATURE_OSD_CACHEPOOL的feature标志,返回错误EOPNOTSUPP
- 根据消息的类型,调用相应的处理函数来处理
总结,本函数开始进入相关的ReplicatedPG来处理,首先就是检查PG的状态是否正常
ReplicatedPG::do_op
这个函数是比较复杂,也是比较长的,必须要理解snap相关的一些概念。
void ReplicatedPG::do_op(OpRequestRef& op)
- 消息解包m->finish_decode
- 调用osd->osd->init_op_flags 初始化 op->rmw_flags, 函数init_op_flags根据flag 来设置rmw_flags
- 如果是读操作,并且有CEPH_OSD_FLAG_BALANCE_READS或者CEPH_OSD_FLAG_LOCALIZE_READS标志,说明主从副本都可以读。检查本osd是否是该pg的primay或者replica ; 否则,本osd必须是该pg的primay
- 如果是includes_pg_op操作,调用pg_op_must_wait检查该操作是否需要等待,如果需要等待,加入waiting_for_all_missing队列,如果不需要等待,调用do_pg_op处理pg相关的操作
- 调用op_has_sufficient_caps检查权限
- 检查对象的名字是否超长
- 检查操作的客户端是否在blacklist中
- 检查是磁盘空间否full
检查如果是写操作,如果是snap,返回-EINVAL,快照不允许写操作。如果写操作带的数据大于osd_max_write_size(如果设置),直接返回-OSD_WRITETOOBIG错误
以上, 检查操作PG和osd是否一致,基本的操作相关的参数的检查
构建要访问对象的head对象(需要主要的时,这是head对象,不是要访问的对象, 任何操作都需要head对象)
- 如果是顺序写,调用函数scrubber.write_blocked_by_scrub检查head对象,如果正在进行scrub操作,就加入waiting_for_active等待scrub操作完成再处理。
- 检查head对象是否missing:也就是处于缺失状态,需要恢复,调用函数wait_for_unreadable_object加入相应的队列等待
- 如果是顺序写,检查是否head对象是否is_degraded_or_backfilling_object, 也就是正在recover状态,都需要调用wait_for_degraded_object加入相应的队列等待
是否blocked by snap,objects_blocked_on_degraded_snap队列里保存head对象,这些head对象在rollback到某一个snap对象时,snap对象处于缺失状态,,就必须等待恢复,此时head对象不能写操作。objects_blocked_on_snap_promotion里的对象表示head对象rollback时,该对象在Cache pool层没有,需要Data pool层获取。
这里最好了解一下snapshot的相关的知识和Cache Tier相关的知识。以上 10,11,12,13, 14 构建并检查head对象的状态
如果是 顺序写操作,检查Cache 层是否Full
- 检查snapdir对象,是否missing等状态
- 检查snapdir对象, 如果是写操作,就-EINVAL。只有读操作,才访问snapdir对象
- 检查是否是dup/replay
- 构建对象oid,这才是实际要操作的对象,可能是snapshot 也可能是head
- 调用函数检查maybe_await_blocked_snapset是否被block by obc,ObjectContext设置为blocked状态,该object 有可能正在flush,或者copy(由于Cache Tier),暂时不能写,需要等待
调用函数find_object_context 获取object_context,如果获取成功,需要检查oid的状态
如果hit_set不为空,就需要设置hit_set. hitset, hit_set 和 agent_state都是Cache tier的机制,hit_set记录cache是否命中,暂时不深入分析。
- 如果设置了agent_state, 就处理Cache 相关的信息
- 获取object_locator
- 检查obc is_blocked or blocked by
- 获取src_obc,一个Op带的多个OSDOp操作中,需要src oid, 例如rados_clone_range中,需要src_obj
- 如果是snapdir,需要所有的clone的objectContext。 如果是snapdir操作,就需要所有的clone对象,就构建所有的clone对象的objectContext,并把它加入的src_obs中
- 创建opContext
- 调用execute_ctx(ctx);
总之,do_op主要检查相关的对象的状态是否正常,并获取ObjectContext,OpContext相关的上下文信息
ReplicatedPG::execute_ctx
- 创建一个新的事务 ctx->op_t = pgbackend->get_transaction()
- 如果是写操作, 更新ctx->snapc
- 如果是read,加各种ondisk_read_lock 所
- 调用prepare_transaction
- 调用calc_trim_to,计算需要trim的pg log的 版本
- 调用函数 issue_repop发起replicat op操作:
- 调用函数eval_repop,检查各个副本已经reply,做相应的操作
总结,execute_ctx 执行操作,把相关的操作打包成事务,并没有真正修改文件系统的数据,后面的操作都是在主从副本上应用已经打包好的日志。
ReplicatedPG::get_object_context
获取对象的object context
- 从object_contexts的lur缓存中查找对象对应的ObjectContextRef
- 如果查找就返回,如果没有查找到,就需要创建
ReplicatedPG::calc_trim_to
- 计算target,设置为最小的日志保存数量cct->_conf->osd_min_pg_log_entries,如果pg处于degraded,或者正在修复的状态,设置为最大target = cct->_conf->osd_max_pg_log_entries (10000)
- 如果min_last_complete_ondisk不为空,且不等于pg_trim_to, pg_log的size大于target,计算pg_log的前 num_to_trim 个日志的最小的 版本
peer_last_complete_ondisk 保存了各个osd在该pg上的最后成功完成操作的版本,函数update_peer_last_complete_ondisk在每次sub_op_modify_reply完成后调用,用于更新该值
函数calc_min_last_complete_ondisk计算 min_last_complete_ondisk,在每次函数eval_repop中完成操作调用计算。
ReplicatedPG::prepare_transaction
把相关的操作打包,包括比较复杂的 snapshot的处理
ReplicatedPG::issue_repop
- 首先更新peer_info的相关信息,pinfo.last_update和pinfo.last_complete,如果pinfo.last_complete == pinfo.last_update,说明该peer的状态处于clean状态,没有需要recover的对象,就同时更新二者,否则只更新pinfo.last_update
- 加各种ondisk_write_lock
- 调用 repop->ctx->apply_pending_attrs()
- 检查如果是 EC,是否可以rollback
- 设置回调Context,调用函数 pgbackend->submit_transaction
- 解ondisk_read_unlock
PGBackend 的处理
PGBackent的处理,就是把打包好的transaction,分发到各个从osd上应用,对与EarasreCode,就实现就是ECBackend,也是主chunk向各个分片chunk分发数据。PGBacend的设计,相当于增加了一层后端存储相关的。
ReplicatedBackend::submit_transaction
- 调用issure_op
- 并调用parent->queue_transactions,修改自己,也就是主osd
ReplicatedBackend::issue_op
把请求发送到pg的各个从osd