ceph的PGLog是由PG来维护,记录了该PG的所有操作,其作用类似于数据库里的undo log。PGLog通常只保存近千条的操作记录(默认是3000条, 由osd_min_pg_log_entries指定),但是当PG处于降级状态时,就会保存更多的日志(默认是10000条),这样就可以在故障的PG重新上线后用来恢复PG的数据。本文主要从PG的格式、存储方式、如何参与恢复来解析PGLog。
相关配置说明:
1. PGLog模块静态类图
PGLog模块的静态类图如下图所示:
2. PGLog的格式
ceph使用版本控制的方式来标记一个PG内的每一次更新,每个版本包括一个(epoch, version)来组成。其中,epoch是osdmap的版本,每当有OSD状态变化(如增加、删除等时),epoch就递增;version是PG内每次更新操作的版本号,是递增的,由PG内的Primary OSD进行分配。
PGLog在代码实现中有3个主要的数据结构来维护(相关代码位于src/osd/osd_types.h中):
结构体pg_log_entry_t记录了PG日志的单条记录,其数据结构如下:
结构体pg_log_t在内存中保存了该PG的所有操作日志,以及相关的控制结构:
需要注意的是,PG日志的记录是以整个PG为单位,包括该PG内的所有对象的修改记录。
结构体pg_info_t是对当前PG信息的一个统计:
下面简单画出三者之间的关系示意图:
其中:
- last_complete: 表示从[0, last_complete]这段区间的所有object均在本PG副本上已经存在,而(last_complete, last_update]这段区间的object可能还不存在。
ps: 在写PGLog时也会写pg_info,参看PG::write_if_dirty()中调用prepare_write_info()部分
从上面结构可以得知,PGLog里只有对象更新操作相关的内容,没有具体的数据以及偏移大小等,所以后续以PGLog来进行恢复时都是按照整个对象来进行恢复的(默认对象大小是4MB)。
另外,这里再介绍两个概念:
- epoch是一个单调递增序列,其序列由monitor负责维护,当集群中的配置及OSD状态(up、down、in、out)发生变更时,其数值加1。这一机制等同于时间轴,每次序列变化是时间轴上的点。这里说的epoch是针对OSD的,具体到PG时,即对于每个PG的版本eversion中的epoch的变化并不是跟随集群epoch变化的,而是当前PG所在OSD的状态变化,当前PG的epoch才会发生变化。
如下图所示:
- 根据epoch增长的概念,即引入第二个重要概念interval
因为pg的epoch在其变化的时间轴上并非是完全连续的,所以在每两个变化的pg epoch所经历的时间段我们称之为intervals。
3. PGLog的存储方式
了解了PGLog的格式之后,我们就来分析一下PGLog的存储方式。在ceph实现里,对于写IO的处理,都是先封装成一个transaction,然后将这个transaction写到journal里。在Journal写完后,触发回调流程,经过多个线程及回调的处理后,再进行写数据到buffer cache的操作,从而完成整个写journal和本地缓存的流程(具体的流程在《OSD读写处理流程》一文中有详细描述)。
总体来说,PGLog也是封装到transaction中,在写journal的时候一起写到日志磁盘上,最后在写本地缓存的时候遍历transaction里的内容,将PGLog相关的东西写到LevelDB里,从而完成该OSD上PGLog的更新操作。
3.1 PGLog更新到journal
3.1.1 写IO序列化到transaction
在《OSD读写流程》里描述了主OSD上的读写处理流程,这里就不说明。在ReplicatedPG::do_osd_ops()函数里根据类型CEPH_OSD_OP_WRITE就会进行封装写IO到transaction的操作(即: 将要写的数据encode到ObjectStore::Transaction::tbl里,这是个bufferlist,encode时都先将op编码进去,这样后续在处理时就可以根据op来操作。注意这里的encode其实就是序列化操作)。
这个transaction经过的过程如下:
3.1.2 PGLog序列化到transaction
- 在ReplicatedPG::do_op()中创建了一个对象修改操作的上下文OpContext,然后在ReplicatedPG::execute_ctx()中完成PGLog日志版本等的设置:
上面我们看到submit_transaction()的第三个参数传递的就是ctx->opt_t,在prepare_transaction()中我们已经将要修改的对象数据打包放入了该transaction。
- 在ReplicatedPG::prepare_transaction()里调用ReplicatedPG::finish_ctx,然后finish_ctx函数里就会调用ctx->log.push_back(),在此就会构造pg_log_entry_t插入到vector log里;
上面我们可以看到将ctx->at_version
传递给了pg_log_entry_t.version; 将ctx->obs->oi.version
传递给了pg_log_entry_t.prior_version;将ctx->user_at_version
传递给了pg_log_entry_t.user_version。
对于ctx->obs->oi.version
,其值是在如下函数中赋予的:
- 在ReplicatedBackend::submit_transaction()里调用parent->log_operation()将PGLog序列化到transaction里。在PG::append_log()里将PGLog相关信息序列化到transaction里。
上面我们注意到对于PGLog的处理,PGLog所对应的Transaction与实际的对象数据对应的Transaction是相同的。
在ReplicatedPG::prepare_transaction()中我们构造了pg_log_entry_t对象放入了ctx->log中。接着在如下函数中将会把这些与PGLog相关的信息序列化到ctx->op_t这一transaction中:
在上面PG::append_log()函数中,首先调用PG::add_log_entry()将PGLog添加到pg_log
中进行缓存,以方便查询。之后再调用write_if_dirty():
ps: 注意到在这里调用write_if_dirty()前已经将dirty_info置为了true,因此会调用prepare_write_info()将pg_info也一并打包到transaction中,随PGLog一起写到journal日志中。
在PG::write_if_dirty()中,由于在PG::append_log()时将dirty_info设置为了true,因此肯定先调用prepare_write_info()函数,该函数可能会将当前的epoch信息、pg_info信息打包放入km
中。之后如果km不为空,则调用t.omap_setkeys()将相关信息打包进transaction中。
注:通过上面我们可以看到,PGLog中除了包含当前所更新的object信息外,还可能包含如下:
现在我们来看pg_log.write_log():
通过上面,我们可以看到在PGLog::_write_log()函数中将pg_log_entry_t数据放入了km
对应的bufferlist中,然后PG::write_if_dirty()函数的最后
将这些bufferlist打包进transaction中。
注: 这里PGLog所在的transaction与实际的object对象数据所在的transaction是同一个
- 完成PGLog日志数据、object对象数据的写入
在前面的步骤中完成了Transaction的构建,在这里调用ReplicatedPG::queue_transactions()来写入到ObjectStore中。
3.1.3 Trim Log
前面说到PGLog的记录数是有限制的,正常情况下默认是3000条(由参数osd_min_pg_log_entries控制),PG降级情况下默认增加到10000条(由参数osd_max_pg_log_entries)。当达到限制时,就会trim log进行截断。
在ReplicatedPG::execute_ctx()里调用ReplicatedPG::calc_trim_to()来进行计算。计算的时候从log的tail(tail指向最老的记录的前一个)开始,需要trim的条数为log.head - log.tail - max_entries
。但是trim的时候需要考虑到min_last_complete_ondisk(这个表示各个副本上last_complete的最小版本,是主OSD在收到3个副本都完成时再进行计算的,也就是计算last_complete_ondisk和其他副本OSD上的last_complete_ondisk,即peer_last_complete_ondisk的最小值得到min_last_complete_ondisk),也就是说trim的时候不能超过min_last_complete_ondisk,因为超过了也trim掉的话就会导致没有更新到磁盘上的pg log丢失。所以说可能存在某个时刻,pglog的记录数超过max_entries。例如:
在ReplicatedPG::log_operation()的trim_to就是pg_trim_to,trim_rollback_to就是min_last_complete_ondisk。log_operation()里调用pg_log.trim(&handler, trim_to, info)进行trim,会将需要trim的key加入到PGLog::trimmed这个set里。然后在_write_log()里将trimmed中的元素插入到to_remove里,最后再调用t.omap_rmkeys()序列化到transaction的bufferlist里。
3.1.4 PGLog写到journal盘
PGLog写到Journal盘上就是journal一样的流程,具体如下:
- 在ReplicatedBackend::submit_transaction()调用log_operation()将PGLog序列化到transaction里,然后调用queue_transactions()传到后续处理;
这里ReplicatedPG实现了PGBackend::Listener接口:
因此这里调用的parent->queue_transactions()就是ReplicatedPG::queue_transactions()
- 调用到FileStore::queue_transactions()里,就将list构造成一个FileStore::Op,对应的list放到FileStore::Op::tls里
- 接着在FileJournal::prepare_entry()中遍历vector &tls,将ObjectStore::Transaction encode到一个bufferlist里(记为tbl)
- 然后在JournalingObjectStore::_op_journal_transactions()函数里调用FileJournal::submit_entry(),将bufferlist构造成write_item放到writeq里
- 接着在FileJournal::write_thread_entry()函数里,会从writeq里取出write_item,放到另一个bufferlist里
- 最后调用do_write()将bufferlist的内容异步写到磁盘上(也就是写journal)
3.1.5 PGLog写入leveldb
在上面完成了journal盘的写操作之后,接着就会有另外的线程异步的将这些日志数据写成实际的object对象、pglog等。如下我们主要关注对pglog的持久化操作:
在《OSD读写流程》里描述到是在FileStore::_do_op()里进行写数据到本地缓存的操作。将pglog写入到leveldb里的操作也是从这里出发的,会根据不同的op类型来进行不同的操作。
1) 比如OP_OMAP_SETKEYS
(PGLog写入leveldb就是根据这个key)
2) 再比如OP_OMAP_RMKEYS
(trim pglog的时候就是用到了这个key)
PGLog封装到transaction里面和journal一起写到盘上的好处: 如果osd异常崩溃时,journal写完成了,但是数据有可能没有写到磁盘上,相应的pg log也没有写到leveldb里,这样在OSD再启动起来时,就会进行journal replay,这样从journal里就能读出完整的transaction,然后再进行事务的处理,也就是将数据写到盘上,pglog写到leveldb里。
4. PGLog的查看方法
具体的PGLog内容可以使用如下工具查看:
4.1 某一个PG的整体pglog信息
1) 停掉运行中的osd,获取osd的挂载路径,使用如下命令获取pg列表
注意: 对于type为filestore类型,我们还必须指定--journal-path
选项;而对于bluestore类型,则不需要指定该选项。
2) 获取具体的pg_log_t信息
注: 对于某一些PG,可能查询出来pg log信息为空。
3) 获取具体的pg_info_t信息
4.2 追踪单个op的pglog
1) 查看某个object映射到的PG
采用ceph osd map pool-name object-name-id
命令查看object映射到的PG,例如:
具体查看方法请参看《如何在ceph中定位文件》
2) 确认PG所在的OSD
# ceph pg dump pgs_brief |grep ^19|grep 19.3f
3) 通过以上两步找到会落入到指定pg的对象,以该对象为名将指定文件put到资源池中
# rados -p oss-uat.rgw.buckets.data put 135882fc-2865-43ab-9f71-7dd4b2095406.20037185.269__multipart_批量上传走joss文件 -003-KZyxg.docx.VLRHO5x1l3nV4-v5W4r6YA2Fkqlfwj3.107 test.file
4) 从该pg所在的osd集合中任意选择一个down掉,查看写入的关于135882fc-2865-43ab-9f71...
的log信息
5. PGLog如何参与恢复
根据PG的状态机(主要是pg 从reset -> activte
过程中的状态转换,其中包括pg从peering到activate 以及epoch变化时pg 状态恢复的处理流程。如下图所示)我们可以看到,
PG状态恢复为active的过程需要区分Primary
和Replicated
两种,因为不论是pg还是osd的消息都是由Primary
主导,再分发给从组件。同时PGLog参与恢复主要体现在ceph进行peering的时候建立missing列表来标记过时的数据,以便于对这些数据进行恢复。故障OSD重新上线后,PG就会标记为peering状态并暂停处理请求。
对于故障OSD所拥有的Primary PG
-
它作为这部分数据“权责”
主体,需要发送查询PG元数据请求给所有属于该PG的Replicate角色节点;
-
该PG的Replicate角色节点实际上在故障OSD下线期间成为了Primary角色并维护了权威
的PGLog,该PG在得到故障OSD的Primary PG的查询请求后会发送响应;
-
Primary PG通过对比Replicate PG发送的元数据 和 PG版本信息后发现处于落后状态,因此它会合并得到的PGLog并建立权威
PGLog,同时会建立missing列表来标记过时数据;
-
Primary PG在完成权威
PGLog的建立后,就可以标志自己处于Active状态。
对于故障OSD所拥有的Replicate PG
-
这时上线后故障OSD的Replicate PG会得到Primary PG的查询请求,发送自己这份“过时”
的元数据和PGLog;
-
Primary PG对比数据后发现该PG落后并且过时,然后通过PGLog建立missing列表(注: 这里其实是peer_missing列表);
-
Primary PG标记自己处于Active状态;
Peering过程中涉及到PGLog(pg_info、pg_log)的主要步骤
1) GetInfo
PG的Primary OSD通过发送消息获取各个Replicate OSD的pg_info信息。在收到各个Replicate OSD的pg_info后,会调用PG::proc_replica_info()处理副本OSD的pg_info,在这里面会调用info.history.merge()合并Replicate OSD发过来的pg_info信息,合并的原则就是更新为最新的字段(比如last_epoch_started和last_epoch_clean都变成最新的)
2) GetLog
根据pg_info的比较,选择一个拥有权威日志的OSD(auth_log_shard),如果Primary OSD不是拥有权威日志的OSD,就去该OSD上获取权威日志。
选取拥有权威日志的OSD时,遵循3个原则(在find_best_info()函数里)
也就是说对比各个OSD的pg_info_t,谁的last_update大,就选谁; 如果last_update一样大,则谁的log_tail小,就选谁;如果log_tail也一样,就选当前的Primary OSD
如果Primary OSD不是拥有权威日志的OSD,则需要去拥有权威日志的OSD上去拉取权威日志,收到权威日志后,会调用proc_master_log()将权威日志合并到本地pg log。在merge权威日志到本地pg log的过程中,会将merge的pg_log_entry_t对应的oid和eversion放到missing列表里,这个missing列表里的对象就是Primary OSD缺失的对象,后续在recovery的时候需要从其他OSD pull的。
3) GetMissing
拉取其他Replicate OSD的pg log(或者部分获取,或者全部获取FULL_LOG),通过与本地的auth log对比,调用proc_replica_log()处理日志,会将Replicate OSD里缺失的对象放到peer_missing列表里,以用于后续recovery过程的依据。
注意: 实际上是在PG::activate()里更新peer_missing列表的,在proc_replica_log()处理的只是从replica传过来它本地的missing(就是replica重启后根据自身的last_update和last_complete构造的missing列表),一般情况下这个missing列表是空。
[参看]
-
PGLog写流程梳理
-
ceph存储 ceph中pglog处理流程
-
ceph PGLog处理流程
-
Log Based PG
-
ceph基于pglog的一致性协议
-
Ceph读写流程