注意
本文档适用于 Ceph 的开发版本。
基于日志的PG
背景
为什么选择PrimaryLogPG?
目前,所有ceph池类型的一致性都是通过基于主日志的复制来确保的。这适用于纠删码(EC)池和复制池。
基于主日志的复制
读取必须返回任何已完成写入(客户端可能已收到提交消息)所写入的数据。有很多方法可以处理这个问题,但是Ceph的架构使得任何map epoch上的每个人都能够轻松知道谁是主节点。因此,最简单的答案是:将特定PG的所有写入都通过单个排序的主节点路由,然后分发给副本。尽管我们实际上只需要在单个RADOS对象上序列化写入(即使如此,部分排序也只需要提供重叠区域写入之间的排序),但我们不妨在整个PG上序列化写入,因为它允许我们使用两个数字来表示PG的当前状态:最近一次写入开始时主节点上的map epoch(这比看起来要奇怪,因为map分发本身是异步的——参见Peering和区间变化的概念)以及一个递增的每PG版本号——这在代码中以类型eversion_t引用,并存储为pg_info_t::last_update。此外,我们维护一个“最近”操作的日志,其长度至少足以包含任何*不稳定*写入(已开始但未提交的写入)和本地不同步的对象(参见恢复和回填)。实际上,日志会延伸得更远(干净时为osd_min_pg_log_entries,不干净时为osd_max_pg_log_entries),因为它有助于快速执行恢复。
使用此日志,只要我们与非空子集(即在最近接受写入的区间中必须接受任何已完成写入的OSD子集)进行通信,我们就可以确定一个保守的日志,该日志必须包含已向客户端报告为已提交的任何写入。这里有一定的自由度,我们可以选择该集合中任何元素记住的最旧头部(任何更新的日志条目都不可能在不包含它的情况下完成)和记住的最新头部(显然,日志中的所有写入都已开始,因此我们记住它们是没问题的)之间的任何日志条目作为新的头部。这是PG/PrimaryLogPG中复制池和EC池的主要区别点:复制池尝试选择最新的有效选项,以避免客户端需要重放这些操作,而是恢复其他副本。而EC池则尝试选择可用的*最旧*选项。
其原因在于实现中其余差异的核心:单个副本通常不足以重建EC对象。事实上,有些编码下,某些日志组合会导致对象无法恢复(例如k=4,m=2编码,其中3个副本记住了写入,而另外3个没有——我们没有任一版本的3个副本)。因此,表示*不稳定*写入(尚未提交给客户端的写入)的日志条目必须能够仅使用EC池上的本地信息进行回滚。因此,日志条目通常可能可回滚(在这种情况下,通过延迟应用或通过一组用于回滚就地更新的指令)或不可回滚。复制池日志条目永远不能回滚。
有关更多详细信息,请参阅PGLog.h/cc、osd_types.h:pg_log_t、osd_types.h:pg_log_entry_t以及一般的peering。
ReplicatedBackend/ECBackend统一策略
PGBackend
复制和纠删码之间的根本区别在于复制可以进行破坏性更新,而纠删码不能。如果我们需要两个完整的PrimaryLogPG实现,那将非常麻烦,因为真正根本的区别只有几个
读取如何工作——仅异步,EC需要远程读取
写入如何工作——要么仅限于追加,要么必须写到一边并执行tpc
在peering期间我们选择最旧还是最新的可能头部条目
日志条目中的一些额外信息以实现回滚
而相似之处则非常多
对象的所有统计信息和元数据
用于混合客户端IO与恢复和scrub的高级锁定规则
用于混合读取和写入而不暴露未提交状态(以后可能回滚或遗忘)的高级锁定规则
确定参与最近接受写入的区间的osd集合所需的过程、元数据和协议
等等。
相反,我们选择了一些抽象(和一些修补程序)来弥补这些差异
PGBackendPGTransactionPG::choose_acting在calc_replicated_acting和calc_ec_acting之间进行选择写入管道的各个部分根据池类型禁用某些操作——例如omap操作、类操作读取以及非对齐追加写入(官方来说,到目前为止)对于EC
其他零星的修补程序
PGBackend和PGTransaction能够抽象上述第1和第2个差异,并根据需要向日志条目添加第4个差异。
复制实现位于ReplicatedBackend.h/cc,不需要太多额外解释。有关ECBackend的更多详细信息,请参阅doc/dev/osd_internals/erasure_coding/ecbackend.rst。
PGBackend接口说明
注意:这来自一个早于Firefly版本的设计文档,可能与某些方法名称不符。
可读 vs 降级
对于复制池,如果对象存在于主节点上(具有正确的版本),则它可读。对于EC池,我们需要至少m个分片存在才能执行读取,并且我们需要它在主节点上。因此,PGBackend需要包含一些接口来确定何时需要恢复才能提供读取而不是写入。这也改变了peering何时有足够的日志来证明它
核心变化
PGBackend需要能够返回IsPG(Recoverable|Readable)Predicate对象以允许用户做出这些判断。
客户端读取
来自复制池的读取始终可以由主OSD同步满足。在纠删码池中,主节点需要从一定数量的副本请求数据以满足读取。PGBackend因此需要提供单独的objects_read_sync和objects_read_async接口,其中前者不会由ECBackend实现。
PGBackend接口
objects_read_syncobjects_read_async
Scrub
我们目前有两种具有不同默认频率的scrub模式
[浅层] scrub:比较对象集和元数据,但不比较内容
深度 scrub:比较对象集、元数据和对象内容的CRC32(包括omap)
主节点向每个副本请求特定对象范围的scrubmap。副本填写该对象范围的scrubmap,如果进行深度scrub,则包括每个对象内容的CRC32。主节点从每个副本收集这些scrubmap并执行比较,以识别不一致的对象。
大部分功能可以与纠删码PG基本不变地工作,但需要注意的是,PGBackend实现必须负责实际执行扫描。
PGBackend接口
be_*
恢复
恢复对象的逻辑取决于后端。对于当前的复制策略,我们首先将对象副本拉到主节点,然后并发地将其推送到副本。对于纠删码策略,我们可能希望读取重建对象所需的最少副本块数,并并发地推送替换块。
另一个区别是纠删码PG中的对象可能在未找到的情况下无法恢复。unfound状态可能应该重命名为unrecoverable。此外,PGBackend实现必须能够指导搜索具有无法恢复对象块的PG副本,并能够确定特定对象是否可恢复。
核心变化
s/unfound/unrecoverable
PGBackend接口