注意
本文档适用于 Ceph 的开发版本。
纠删码直接读取
本文档介绍了 EC 直接读取的设计,它还涵盖了大型副本 IO 的拆分读取。
基本设计
小型读取
目前,纠删码池中的所有读取都定向到每个 PG 的主 OSD。对于小型(子块)读取,这将导致以下消息流:
拟议的直接读取机制允许客户端绕过主 OSD,直接与目标 OSD 交互,从而简化消息流。
如果 OSD 不可用或发生读取错误,客户端将回退到传统的、由主 OSD 协调的读取方法,以确保可以从编码分片重建数据。
中型读取
当读取跨越多个数据分片但包含在单个条带内时,将其归类为“中型”。此类读取的标准消息流为:
使用直接读取机制,消息流优化如下:
成功接收所有数据分片后,客户端负责通过按正确顺序连接缓冲区来重建原始数据。
大型读取
大型读取是指超过单个数据条带大小的读取。虽然每个 OSD 仍然可以执行单个读取操作,但客户端不能仅仅连接数据缓冲区。它必须执行去条带化操作,从各个条带单元中重建对象数据。
对象管理器(Objecter)
客户端的 Objecter 组件负责通过以下过程协调直接读取:
在启动直接读取之前,Objecter 会根据其当前的 OSD 映射执行初步的最佳尝试检查,以验证所有必需的 OSD 是否可用。由于 OSD 映射过时而导致的竞争条件发生的可能性被认为是罕见的事件,对性能影响极小。如果任何必需的 OSD 被标记为关闭,读取会立即回退到主 OSD。
如果所有必需的 OSD 都可用,则会实例化一个新的 SplitRead 对象来管理直接读取操作。
SplitRead 对象计算目标 OSD,并向每个 OSD 并行提交读取请求。
完成所有并行读取后,SplitRead 对象检查结果是否存在故障。
如果所有读取都成功,它会去条带化(对于大型读取)或连接(对于中型读取)数据,并完成客户端的原始读取请求。
如果任何读取失败,它将丢弃任何部分数据,并将整个读取操作重新发送给主 OSD。虽然理论上可以使用奇偶校验分片进行客户端恢复,但设计上避免了这种情况。在每个客户端中实现复杂的 EC 插件架构被认为是不理想的。鉴于 OSD 故障是不频繁的事件,并且集群能迅速适应,成本效益分析强烈倾向于这种更简单、更健壮的回退机制。
EC 后端
纠删码后端将得到增强以实现同步读取。此功能将专门用于直接读取机制,并将通过 EC 读取操作上的 BALANCED_READ 标志进行识别。读取将尝试一次;如果尝试失败,I/O 操作将失败回退到客户端,此层不再进行重试。
缺失对象
在对象由于恢复操作而暂时不可用的场景中,OSD 将使直接读取请求失败。将引入一个新的错误代码(名称待定),该代码指示客户端不仅要重试 I/O,还要禁用受影响 PG 的直接读取,直到下一个 epoch。此机制可防止在集群恢复活动期间引入过多的延迟。
撕裂写入
撕裂写入是一种潜在的一致性问题,源于读取和写入操作之间的竞争条件。将通过版本检查机制来缓解此情况。对于任何多 OSD 读取,主 OSD 上的“OI”(对象信息)属性与来自其他分片的数据读取*并行*读取。版本不匹配被认为是一个不频繁的事件。与数据读取同时执行此检查可显著改善延迟,特别是对于受限于大型块大小的带宽受限型工作负载。如果检测到主 OSD 提供的版本列表与从其他分片读取的版本之间存在不匹配,操作将回退到传统读取机制。
默认块大小的更改
为了验证 RADOS 对象版本,中型读取通常需要对主 OSD 进行额外的 I/O 操作。为了减少这些多分片读取的频率,将增加默认块大小。虽然最近对纠删码的增强消除了 Ceph 中大型块的许多性能限制,但在小型对象的存储效率方面仍然存在关键的权衡。
增加块大小的主要限制表现在平均对象大小小于块大小的情况下。在这种情况下,数据块会部分为空,存储开销增加。对于小文件,K+M 池基本上会退化为 1+M 数组,浪费空间。因此,必须审慎地增加默认块大小。我们的数据显示,实际上我们无法将块大小增加到超过当前推荐的 16k。然而,未来的设计可能允许将多个小对象打包到单个 EC 条带中。这将减轻小对象惩罚的影响,届时我们应该增加块大小。我们认为 256k 对于 HDD 是最佳的,32k 对于 SSD 是最佳的,但需要仔细的性能测试。
读写排序
如果未在 I/O 操作上设置 RWORDERING 标志,则读取可能会相对于任何先前的、未提交的写入操作重新排序。
例如,考虑对单个对象进行以下操作序列:
客户端向对象提交包含数据“AAA”的
Write_1。Write_1完成。对象内容现在为“AAA”。客户端向同一对象提交包含数据“BBB”的
Write_2。在
Write_2完成之前,客户端提交一个Read操作。
在这些条件下,读取操作保证返回已完成的 Write_1(“AAA”)或后续的 Write_2(“BBB”)中的数据,但绝不会返回包含两者混合的撕裂结果(例如,“AAB”)。这提供了一种类似于副本池中平衡读取的一致性保证。
如果设置了 RWORDERING 标志,则操作必须使用传统的、由主 OSD 协调的读取路径来保证严格排序。这确保了严格排序得以维持,尤其是在故障场景中。如果尝试进行直接读取,然后通过回退路径重试到主 OSD,则时间变化可能会违反排序保证。从一开始就强制使用由主 OSD 协调的路径可以防止这种歧义,并且与副本池的等效实现保持一致。
读取不稳定性
读取不稳定性是指在没有后续写入的情况下,读取的数据在首次读取后发生变化。
考虑以下序列:
写入 A(完成)
写入 B(尚未完成)
读取,获取数据 B。
客户端执行一些活动,假设 B 已提交到磁盘。
写入回滚(并失败,或从未完成)。
即使写入 B 尚未完成,客户端通常也会假设 B 永远不会回滚。
平衡副本读取通过在日志中存在针对此对象的未提交写入时拒绝非主 OSD 读取来处理这种情况。如果所有读取都不适用于主 OSD,那么 EC 直接读取也需要这种机制。
注意
此处实现了类似的副本机制:https://github.com/ceph/ceph/pull/49380
内核客户端修改
使用 Ceph 的内核模式客户端(例如用于块设备的 krbd 驱动程序)也需要修改。本文档中描述的、在用户空间 librados 库中实现的逻辑必须在内核驱动程序中复制。
这涉及实现 SplitRead 机制,包括计算目标 OSD、并行提交 I/O 请求以及随后的数据重新组装(连接或去条带化)。这是一项非平凡的任务,因为内核开发带来了额外的复杂性,涉及内存管理、异步 I/O 处理和确保系统稳定性。这些内核级更改的设计和实现被认为是必要的工作项,但具体方法需要单独进行详细调查。此外,向内核本身提交代码的挑战也可能被证明是一个有趣的挑战!
性能、QoS 和限流
从 OSD 的角度来看,直接读取机制从根本上改变了 I/O 模式。以前,客户端读取会导致对主 OSD 进行单个 I/O 操作,而此设计现在将并行向不同的 OSD 提交多个较小的 I/O 操作。
网络操作的平衡将发生变化,这需要在性能评估中仔细评估,以便我们能够向最终用户提供足够的数据,帮助他们决定是否启用此可选功能。
拆分将显著减少集群内的网络流量。公共客户端网络上的网络负载将看到更复杂的变化:
小型 IO(子块)的流量根本不会增加。
超大型 IO 的消息数量会增加,但这与相关数据传输相比可能微不足道。
对于中间 IO,性能考虑因素更为微妙,IO 的拆分将有助于进一步分散网络流量,但会带来更大的开销,因为 IO 计数会增加。
CPU 负载
作为实现的一部分,将测量对客户端 CPU 利用率的影响。“去条带化”机制是 CPU 密集型的,并导致非连续缓冲区。对于某些应用程序来说,这可能很重要,因为 ceph 客户端正在与客户端应用程序争夺 CPU 资源。
重要的是,当此功能关闭时,CPU“成本”可以忽略不计:旁路代码必须非常简单。
限流考虑因素
此更改对现有的限流框架提出了挑战。关键是单个逻辑客户端读取的限流行为必须保持一致,无论它被拆分为多少个物理 OSD 操作。确保限流调度程序正确地将这些碎片操作计为单个逻辑单元的具体方法是一个悬而未决的问题,需要进一步调查。因此,此组件的设计尚未最终确定,将在实施阶段解决。
副本平衡读取
对于大型、带宽受限的 I/O 模式,与副本读取相比,直接 EC 读取预计会显示出显著的延迟改进。考虑到这一点,副本读取机制也将进行调整,以利用 SplitRead 对象进行大型、带宽受限的 I/O。这将允许读取工作负载拆分到多个副本 OSD 上,防止单个 OSD 成为瓶颈,并提高这些工作负载的整体性能。副本读取将使用单独的 PR,但目前我们将继续使用相同的设计文档。
拆分读取
早期原型表明,在以下情况下应拆分副本 IO:
SSD:如果副本 IO >= 2 x 128k,IO 将拆分为至少 128k 的 I/O。
HDD:如果副本 IO >= 2 x 256k,IO 将拆分为至少 256k 的 I/O。
将进行进一步的性能测量以验证这些更改,并且将提供一个用户参数以在需要时调整这些阈值。
插件支持
这有可能适用于所有插件。但是,为了减少测试开销,我们将限制为以下插件:
Jerasure
ISA-L
我们将把支持 LRC 设定为延伸目标,但这取决于增强型 EC 工作是否支持 LRC,目前 Umbrella 不需要 LRC。
支持的操作
在实施过程中将进行进一步调查,因此以下内容可能会发生变化。目的是支持操作,以便 RBD、CephFS 和 RGW 的实际工作负载中的绝大多数 IO 执行 EC 直接读取。在撰写本文时,以下限制似乎是合理的:
Objecter ops 必须包含单个操作:
读取(Read)
稀疏读取(Sparse read)
允许以下操作是可能的,但更复杂:
多次读取/稀疏读取
属性读取(与读取或稀疏读取一起执行时)
稀疏读取
稀疏读取目前被 EC 视为完整读取。直接读取将支持稀疏读取。OSD EC 后端需要一个新的同步代码路径,实现它以支持稀疏读取非常简单,值得实现。
不会尝试在任何其他 EC 读取上支持稀疏读取。这意味着在故障场景中,后端的稀疏性将消失。这对于加密来说可能是一个重大问题,因此需要在支持加密之前解决。任何支持 EC 的客户端都不能依赖稀疏读取得到完全实现,因此这不应导致客户端出现任何回归。
对象(用户)版本
客户端可以(并且经常)请求一个“对象版本”。在 OSD 中,这被称为“用户版本”,并与 OSD 的由主日志 PG 生成的间隔版本一起存储在 OI 中。他们通过在读取请求中传递非空指针到 objver 来实现这一点。objver 在非主 OSD 上并不总是最新的。这意味着任何请求 objver 的操作都必须向主 OSD 发送读取请求,即使此类读取在其他方面为空。这将对小型读取的性能产生重大影响。请求 objver 以前在性能方面可以忽略不计,因此将对 RBD、CephFS 和 RGW 进行审查,以确定 objver 是否有时在不必要的情况下被请求。
对于副本拆分读取,用户版本与所有分片上的数据保持同步,因此将从单个任意读取中请求对象版本。
测试
ceph_test_rados_io_sequence
小型与中型与大型读取
已通过 ceph_test_rados_io_sequence 的现有设计涵盖了 IO exerciser。运行它应该能覆盖各种读取。
对象管理器(Objecter)
已通过 IO exerciser 涵盖,目前所有 IO 都通过 librados 发送,后者调用 Objecter。运行它应该能覆盖各种读取。
过时映射/回退路径
IO Exerciser 目前有两种读取注入:
Type = 0 在读取数据时注入 EIO,shard 指示要失败哪个分片。注入发送到主 OSD。
Type = 1 使读取假装缺少一个分片,这将立即触发解码的额外读取,shard 指示要失败哪个分片。注入发送到主 OSD。
就目前而言,这些将无法与直接读取一起使用,我们需要修改 IO Exerciser,使其发送到他们想要注入的分片,以及修改注入本身,以便可以在同步读取路径上调用它们。
缺失对象
我们可以添加一个 Type = 2 注入,类似于 Type = 0,但它返回我们的新错误代码,而不是返回 EIO。我们需要在 objecter 中设置一个钩子来告诉它在同一 epoch 中重新开始发送直接读取,或者强制在此之后进入一个新的 epoch。这将允许我们行使客户端的回退路径。
我们可能需要增强 —testrecovery 标志,使其类似于 —testrecovery=“read:2”,以确保我们能够专门使用此新注入。
读/写排序
可以添加一个新的 ConcurrentWriteRead Op,它断言之后读取的值要么完全是原始数据,要么完全是新写入的数据,并抛出不匹配。可以保留统计信息并输出我们获得了两种结果中的哪一种,以确保在未设置排序标志时,我们能覆盖两种结果的发生/断言排序错误。
除了这个 OP 之外,可能还需要一个新的序列来测试各种小型/中型/大型写入/读取,以行使此功能。
内核级测试
此领域需要更多考虑。目前未被 ceph_test_rados_io_sequence 涵盖。需要调查是否存在与 IO exerciser 可以使用的内核空间 librados 等效的东西。
如果没有,那么回退到专门测试 kRBD 可能需要一个不同的脚本,该脚本可以行使不同的对象大小和过时映射响应。
杂项
可以直接在 IO 本身设置平衡读取标志,以对每个 io 进行细粒度控制,决定是否使用平衡读取。IO exerciser 可以在执行读取时决定是否要测试平衡读取。ReadOp 可以采用一个可选参数来覆盖此设置,如果我们需要序列中的某个操作始终或从不平衡,并且可以在交互模式下的 ReadOp(失败和未失败)中添加一个可选参数,以防您希望指定该值。
ceph_test_rados
ceph_test_rados 是一个现有的工具,在查找问题时对于优化 EC 的开发非常有用。此领域没有计划增强功能,但在开发过程中将大量使用此工具来检查回归。