OSD 最新进展
自从上次命名发布 Argonaut 以来,已经过去了几个月,我们一直在忙碌!好吧,回想起来,大部分时间都花在了寻找一个以“b”开头的头足类动物的名字上,但一旦完成,我们仍然有几周的时间来致力于技术改进。特别是 OSD 方面,出现了一些新的有趣发展。
OSD 内部结构概述 ¶
让我们从一些背景知识开始,为那些不熟悉 Ceph 内部结构的人。Ceph 对象存储中的对象被放置到池中,每个池由一些放置组 (PG) 组成。池“bar”中的对象“foo”将按如下方式映射到一组 osd:

首先,映射将 foo 散列到 0x3F4AE323 并将“bar”映射到其池 ID:3。下一个映射将此映射到 PG 3.23(池 3 中的 pg 23)通过取 0x3F4AE323 mod 256(池“bar”中的 PG 数量)。然后,此 pgid 通过 CRUSH 映射到 osd [24, 3, 12]。osd 24 是主节点;3 和 12 是副本。PG 在 ceph-osd 设计中扮演着几个关键角色。首先,它们是放置的单位。如果我们直接按每个对象计算放置,集群中的更改可能会要求我们重新计算每个对象的每个位置!这样,我们只需要在集群更改时按 PG 基础重新运行 CRUSH。其次,对对象的写入按 PG 基础进行排序。每个 PG 包含该 PG 中对象的有序日志。最后,恢复按 PG 基础进行。通过比较它们的 PG 日志,两个 osd 可以就哪些对象需要恢复到哪个 OSD 达成一致。
Scrub ¶
撇开这些不谈,让我们来谈谈一些保持集群数据诚信的工作。事实证明,数据冗余在您在最后一次读取后几个月才注意到损坏的对象时,并不是特别有用,那时最后的副本也可能变得不可读。为了解决这个问题,Ceph 长期以来都包含一项“scrub”功能,该功能在低 IO 期间,会按顺序选择 PG 并比较跨副本的内容。不幸的是,我们的实现存在两个缺点。第一个是,我们比较了每个 PG 中包含的对象集以及对象元数据,但没有比较对象内容。在即将发布的 Bobtail 版本中,我们将对对象内容进行哈希处理,并在扫描时比较跨副本的哈希值以检测损坏的副本。
第二个缺点是,为了简单起见,我们基本上一次性 scrub 整个 PG。高效执行 scrub 的难题在于,比较主节点和副本的内容只有在扫描在同一版本时才有用!在写入进行中时进行 scrub 可能会导致 scrub 在版本 200 时扫描副本,并在版本 197 时扫描主节点,因为副本恰好比主节点领先一点。确保版本匹配的一个简单方法是简单地停止整个 PG 上的写入并等待它们刷新后再扫描主节点和副本存储。事实上,Argonaut 的方法更复杂——我们扫描主节点和副本集合,而无需停止写入,然后停止写入以重新扫描任何在同时发生更改的对象。但是,对于大型 PG,最后一步可能需要很长时间,因此需要更好的方法。进入 ChunkyScrub!在 Bobtail 中,我们按块 scrub PG,仅暂停当前正在 scrub 的对象的写入。这样,没有对象会被阻止写入很长时间。
OSD 内部结构重构 ¶
ceph-osd 守护进程的内部结构也进行了一些重构。如上所述,PG 作为对象操作的序列化单位。这反映在代码中:OSD 负责的每个 PG 映射到一个 PG 对象。OSD 对象的主要职责是将来自客户端和其他 OSD 的消息传递给适当的 PG 对象。一个令人高兴的结果是,只要对象位于不同的 PG 中,同一 OSD 上的不同对象的操作可以独立(并且并行!)处理。然而,有一个令人烦恼的细节往往会阻止我们充分利用这种并行机会:那就是令人讨厌的 OSDMap。
OSDMap 对于上述图中的“CRUSH MAGIC”箭头起作用是必需的。CRUSH 真正需要两个输入:pgid 和集群的描述。两者结合确定了 PG 将驻留在哪些 OSD 上。OSDMap 编码了这种描述。集群中的更改(例如 osd 的死亡)被编码到由 ceph-mon 集群生成的新 OSDMap 中,并发送到 OSD。这些地图被赋予连续的 epoch 编号。本质上,OSD 内的每个决策都取决于 OSDMap 的内容。进一步使情况复杂化的是,OSDMap 更新不会同时到达所有 OSD。ceph-mon 集群将地图发送到几个 OSD,然后 OSD 将新地图传播到其他 OSD,因为它们发现具有旧地图的 OSD。每个 OSD-OSD(包括常规心跳)或 OSD-客户端消息都包含发送方的当前 OSDMap epoch,允许接收方响应发送方缺少的任何地图。那么,我们如何处理在其他线程忙于为各种 PG 请求客户端时到达的 OSDMap 更新呢?
最初,OSD 在更新全局地图时会暂停负责处理 PG 请求的线程(包括客户端 IO)。这是一种有用的简化,因为每个 PG 可能需要更新本地状态,并且与正在进行的操作协调更新会很复杂。然而,这也是一种有些代价高昂的简化,因为在地图切换期间暂停所有 IO 往往代价很高。Bobtail 包含 OSD 处理 PG 消息方式的重构。首先,PG 内部代码经过重构,尽可能减少对全局 OSD 状态的依赖。其次,每个 PG 都有自己的“当前”OSDMap epoch 概念,该概念不同于其他 PG 和 OSD 作为整体。每个 PG 的内部地图状态在处理消息之前更新到当前的 OSDMap epoch。因此,OSD 可以更新其 OSDMap 相关状态,而无需打扰 PG 线程,然后在准备好后以原子方式发布新的地图 epoch 以供 PG 线程使用。所有这些的最终结果是,OSD 应该更有效地处理地图更改。这可能看起来并不多,但当集群在 OSD 故障期间承受重负载时,地图更改往往会快速发生——这正是您不希望产生额外开销的时候!
Filestore 性能 ¶
另一个获得全新涂层的领域是后端 io 系统同步设计。ceph-osd 守护进程使用标准文件系统,如 xfs 或 btrfs 作为其后端存储。但是,正如您可能想象的那样,在抽象数据存储上以事务方式工作比直接在文件系统之上工作要简单得多(特别是考虑到 xfs、btrfs 和 ext4 之间的差异)。因此,ceph-osd 守护进程通过 FileStore 与文件系统通信,FileStore 在用户的基础文件系统之上提供统一的事务接口,以对象和平面集合的形式呈现。
日志对于提供这些事务保证至关重要。在 xfs(btrfs 有所不同)中,FileStore 首先将每个事务写入日志,然后再将其应用于文件系统。每个写入必须通过
- OSD 操作线程(负责处理客户端请求)
- FileStore 日志线程(负责将写入附加到日志)
- FileStore 工作队列(负责将写入应用于后端文件系统)
- Messenger(负责管理节点间通信)以供客户端回复。
如果我们要避免破坏性能,就必须最大限度地提高此管道的吞吐量并最大限度地减少延迟。为了解决这个问题,我们拿了一个带有 192GB 内存的新服务器,并开始在隔离的 ramdisk 上对 FileStore 模块进行基准测试。我们能够以大约 6k iops 的速率推送小写入。如果我们将运行在约 150iop 旋转磁盘上,这很好。如果我们将运行在 20k+ iop ssd 上,则要差得多。所以,我们开始工作。对我们的 Mutex 对象进行工具化,以计算在每个锁上花费的时间,产生了一些有希望的“问题锁”,其中每一个,为了简单起见,都保护了几个不相关的结构。通过对这些结构的更细粒度的同步进行代码重构,并禁用内存中日志记录,我们将 iops 提高到约 22k。在下一个版本中,我们将继续攻击 OSD 守护进程上层中存在的延迟和吞吐量瓶颈。
恢复 QOS ¶
Ceph 的一个不错的特性是自我修复。OSD 10 的死亡最终会触发生成一个新的 OSDMap,将 OSD 10 标记为已关闭并退出,这反过来会触发曾经驻留在 OSD 10 上的任何 PG 重新平衡到一组新的 OSD。当然,不可避免的是,将 OSD 10 的 PG 恢复到新的 OSD 必须涉及将对象从 OSD 10 的 PG 的幸存副本复制到新的 OSD。使用 Argonaut 的默认设置,这看起来是一系列从幸存副本到新副本的 1MB 传输。那么,这些大型传输如何与您的集群上 RBD 运行的 VM 生成的延迟敏感 4k 写入的爆发相互作用?
Argonaut 已经具有一些您可能熟悉的功能,用于限制恢复对客户端工作负载的影响。最显着的是,“osd recovery max active”限制了单个 OSD 将启动的并发恢复操作的数量。不幸的是,这只会限制单个 OSD 上启动的数量。例如,它不会阻止 20 个 OSD 同时将“osd recovery max active”对象推送到您刚刚添加到集群中的单个 OSD!这就是 Bobtail 的新“osd max backfills”可配置项发挥作用的地方。“osd max backfills”定义了允许从或到单个 OSD 恢复的 PG 数量的限制。
Argonaut 还包含一种简单的机制,用于在 OSD 上对消息进行优先级排序。每个消息都标记有一个数值优先级。消息的处理顺序首先按优先级,然后按到达时间。因此,所有优先级为 128 的消息都将在任何优先级为 63 的消息之前处理。这对于 OSD 的某些部分很有用。例如,副本向主节点发送的回复,表明副本已持久化客户端操作,会被赋予高优先级,以减少客户端操作的延迟,因为它们处理起来很快。但是,如果我们使用这种机制赋予客户端消息比恢复消息更高的优先级,任何大量的客户端 I/O 都会导致恢复被饿死。你真的不希望这样,因为 PG 在没有重新复制其数据的情况下持续时间越长,第二次或第三次 OSD 故障就越有可能将其完全淘汰!
Bobtail 引入了一种更灵活的消息优先级方案。可以发送消息,使得优先级为 40 的消息能够以优先级为 20 的消息的两倍速率通过,但不会饿死它们。这已被利用来允许以低于客户端 I/O 消息的优先级发送恢复消息,而不会导致饥饿。作为一项不错的奖励,考虑尝试读取尚未恢复到主节点的对象。主节点必须先完成该对象的恢复操作,然后才能提供读取服务。现在,我们可以赋予该恢复操作与客户端 I/O 相同的优先级,以便它能够绕过排队在其他 osd 上的任何较低优先级的恢复操作!未来在这方面的工作将侧重于减少在 PG 的主 OSD 上协调 PG 的恢复操作的负担。这种负担仍然会影响进入该 OSD 的客户端 I/O。
所以,这些是 OSD 中的一些新发展。而这仅仅是 OSD!RBD 正在获得分层和回写缓存!CephFS 正在获得大量的稳定性和性能增强!如果一切顺利,我们的下一个版本将会有更多的感叹号!也就是说,如果我们有时间在想出一个以“c”开头的头足类动物名称之后…
