EC 的全称是Erasure Code——纠删码,是一种编码理论,EC 介绍链接
如下是和本文档相关的一些术语:
2019 网易数帆对象存储(内部代号 NOS)团队基于 Ceph EC 开发了近线存储引擎,针对原生的 Ceph EC 作了一系列优化,主要包括:
近线存储是网易数帆使用 Ceph EC 的初次尝试,用于存储低频数据,满足了近线存储的要求;但整体上还存在两个比较严重的问题:
不能满足在线存储场景,我们在近线存储优化的基础上针对在线存储场景也做了多项优化工作,本文介绍这些优化的实践、效果以及我们的心得。
从 NOS 的对象大小统计分布情况看,我们 80% 的对象大小都在 0 ~ 128KB 范围内,小对象的高占比是我们可以做对象合并的基础前提。
而从对象大小的角度来看在线存储比近线存储区别在于:
当前 NOS 在线存储的性能指标要求:
而基于固有的近线存储架构不可能满足上述要求。由于 EC 必须要条带对齐,对 NOS 小对象来说,独占条带会严重降低有效数据利用率,所以我们的优化目标是小对象合并,通过合并对象写入,降低写 IOPS。
此方式在应用层累积对象,如 64kb 的条带,非合并方式下每个对象调用一次 write ;而在应用层累积多个对象后写入,如设置 1MB 的累积 buffer,当 buffer 写满后再批量调用 write ,按照平均大小 128KB 计算,可以将 8 个 IO 合并成 1 个,能显著降低写 IOPS 压力,如图(a) 所示。但此时一个对象仍然是分布在一个条带中,如图 (b) object-s1 - object-sn,读操作需要读取 n 个 osd。
横向搁置条带合并写方式能显著降低写 IOPS, 但读对象时仍然需要读取对象所在条带的所有磁盘(osd),小对象的 IOPS 放大严重。而如果我们将条带大小由 64KB 调整为 1MB,则单个 osd 的 data chunk 变成 128KB,而我们 80% 的对象都是小对象,即单个对象可以直接存放在单个 osd 的 chu中;如果某个 osd 故障仍然可以通过条带中的其他 osd 通过 EC 计算恢复。只是此时用于 EC 恢复的数据是其他对象,而不像横向搁置条带中,数据的恢复由同一个对象的其他块完成。如下图 © 为纵向的条带搁置策略。
例如在横向搁置中对象 object1 分布在 s1- s3 三个 osd 上,如果 s1 osd 故障,则 object-s1 的数据块可以通过 object-s2/object/s2/object-p1/object-p2 恢复。而在纵向搁置中如果 object1 所在的 osd 故障,而通过 object2/object3/p1/p2 恢复。
通过图 (b) 和 图(c) 对比:
即通过调整对象的搁置策略,合并写对象,写有 k 倍的 IOPS 提升,读有有 3 + 2 倍的 IOPS 提升;虽然理想很丰满,但现实也很骨感,如上描述的小对象合并的过程不能用作用户的在线写 API,用户的 PUT 操作都是对象级别的,需要实时响应 PUT 状态,而上述模型需要多个小对象拼凑出一个完整的条带,需要一定的时间差,而极端情况下可能根本拼凑出一个条带;为此我们需要转变思路,引入第二个优化点离线写在线读。
简言之,即我们不能根据用户的实时上传对象拼装 Ceph EC 条带,用户 PUT 上传仍然写三副本存储引擎,通过离线程序异步的扫描一定时间段之前的用户数据,执行条带拼装流程,拼装好的条带再写入 EC
NOS 的对象元数据记录在分布式 mysql 数据库中,我们通过 Producer 模块扫描已经上传到三副本的对象推送给消息队列(Kafka), Consumer 端从消息队列拉取数据执行小对象合并流程,完成对象的生命周期转换,整体的架构图如下:
条带的拼装是一个静态的过程,简单的想法就是不停的从 Kafka 中取数据来填充一个 Ceph 条带,我们在权衡条带利用率和拼装成功率和工程实现等细节后,设计了一种分级队列的拼装方法,大概思路是在内存维护一个 MergeList 数据结构(类似 tcmalloc 中内存分配的分级队列结构),对大于 4M 的对象不需要合并:
如下是执行合并的示意图:
执行小对象合并的几个权衡点:
举例条带拼装过程,如上图(条带拼装示意图 - An 1MB Stripe with small file)
如上阐述了小对象合并的概要内容,实际工程实现中还要很多细节处理;即我们拼装一个条带的过程是根据对象的大小来决定其在一个 Ceph 条带中的分布位置,此时并没有发生真实的数据读写,只有当条带拼装成功后将该条带按批处理的方式提交给 submit 线程处理,submit 线程从三副本读取内容写 Ceph EC 再执行对象的元数据信息变更,如对细节感兴趣,欢迎和我们沟通。
上节内容描述了小对象合并的概要实现,其中条带的头初始 4k 位置标记为 header,我们在上节没有说明;这部分的内容是为 Ceph Append 写方式实现垃圾回收而设计。原生的 Ceph 提供了的对象存储是 RGW,可以采用副本和 EC 模式,但粒度都是单对象级别的;即如果我们采用 Append 方式追加写到同一个 Ceph 对象,当该 Ceph 对象中部分小对象被删除形成空洞后,Ceph 在底层没有提供垃圾回收的机制;鉴于此,NOS 结合自身的业务特点设计了一套垃圾回收机制。
而结合业务层来实现 GC 需要被回收的块上记录有上层的业务信息,即上层的对象名信息,先看看我们在近线存储的中的 GC 方案:
从我们近线存储的 GC 流程中可知,GC 的过程由于反序列化获取业务层对象信息消耗了大量的 IO 和带宽,读写流量甚至比用户的读写还高,GC 的内耗注定了整个集群的负载能力差;而由于没有小对象合并写能力,header 信息也才能采取这种妥协折中的方式。
当我们引入小对象合并写特性后,发现世界突然变得不一样了,我们可以将一个条带中的 header 信息集中存储在条带的最开始的 4KB 位置处,如下图:
该方案的核心变更点:
现在当 GC 程序需要反序列化 Ceph 块上的对象信息时,只需要读取首个 osd0 上的 meta 部分,在代码实现中我们是读取了 osd0 的前 4M 内容( Ceph 块大小为 32M,条带大小为1M,一个 Ceph 块上有 32 个条带),按照 header 格式将对象信息反序列化,而这 4M 内容在 osd0 上是连续存储的,只需要一个大 IO 就可以读取完毕。
除了上层业务的一些优化改进,我们同时引入了 BlueStore 引擎。BlueStore 最早在 Jewel 版本中引入,用于取代传统的 FileStore,作为新一代高性能对象存储后端。BlueStore 将索引元数据的 DB 引擎由 LevelDB 替换为 RocksDB(RocksDB 基于 LevelDB 发展而来,并针对直接使用 SSD 作为后端存储介质的场景做了大量优化)。FileStore 因为仍然需要通过操作系统自带的本地文件系统间接管理磁盘,所以所有针对 Rados 层的对象操作,都需要预先转换为能够被本地文件系统识别、符合 POSIX 语义的文件操作,这个转换的过程及其繁琐,效率低下。
针对 FileStore 的上述缺陷,BlueStore 选择绕开本地文件系统,由自身接管裸盘设备,直接进行对象操作,不再进行对象和文件之间的转换,从而使得整个对象存储的 I/O 路径大大缩短,性能有了质的飞跃。我们对单 OSD FileStore 和 BlueStore 性能做了简单测试。
物理池已用容量为37%左右时,BlueStore与FileStore的性能对比
可见:
以下表格记录BlueStore在物理池已用容量为37%和5%时的性能对比:
可见:
上述的所有优化目前已应用于生产环境,下面就我们线上一个 8 + 4 集群给出一些 metric 数据。
写带宽
执行离线写入阶段,控制集群的整体写入速度 2GB/s 左右,而实际我们使用 rados bench 工具压测时写入峰值可达 6GB/s+
写条带延时
在【条带合并】部分我们介绍了条带合并算法,而一次条带合并的过程可能会合并成多个条带,例如一个 3.3M 的对象和一个 128KB 的对象会拼成 4 个条带(条带大小 1M),此时向 Ceph 写对象的时候是一次写 4 个条带,耗时会比单条带要长,但对每个 osd 而言其实是做了 io 合并,减小了写的 IOPS。
从下图可以看出,我们有 1 - 4 个条带合并写的情形,但条带的写入延时在 400ms 左右,4 条带的写入延时在 1600ms 左右;即对于 1M的数据写底层 12 (8 +4) 个 osd 平均耗时在 400ms,因为我们是离线写入,latency 在可接受范围内。
条带利用率
条带利用率是一个 1M 的 Ceph 条带中有效数据和条带大小的占比,利用率直接反馈了存储空间利用率情况,通过【条带合并】算法,我们的业务场景下,条带利用率平均 83%(只有小对象才需要合并写入,即对于小对象有 17%的空间浪费);对大对象我们采取独占 Ceph 块的方式,不需要合并写也没有空间的浪费。因此整体上 Ceph 的空间利用率是高于 83% 的。
首包时间
首包时间反馈 Ceph 集群处理请求返回首个字节的时间,特别是对于 IOPS 压力较大情形下的负载能力:
NEFS(三副本): 40 - 60 ms
CephFs(8 +4EC):40 - 50ms
目前从我们业务统计数据分析看 Ceph 的首包响应时间和 NEFS (小文件存储系统)持平,甚至更短,得益于我们的条带放置策略,对 80% 情形,对象小于 128KB 分布在单个 OSD 上,因此我们读取数据时也只需要读取单个 OSD 上的 chunk 数据。
平均时延
而平均延时是整个对象的下载时间,和首包响应一样大多数情况下优于三副本时间,但出现突刺现象,如下图有 300ms;突刺的产生可能源于读取分布在多个 OSD 上的相对较大对象,而当某个 OSD 异常或者 util 较高时造成 IO 长尾效应。
GC 的过程需要读取源文件写新文件,同时产生读写流量,我们对空洞率达 75%的 Ceph 块对象会执行 GC 操作,GC 的瓶颈即集群的读写瓶颈,因我们是离线写入,保障用户读不受影响的情况下可以尽量提高 GC 的速度;目前 GC 程序日峰值可清理千万+对象,日回收空间峰值 100TB+。
本文总结了网易数帆对象存储团队在推进在线 EC 存储中遇到的一些问题和对应的优化手段,包括小对象合并、离线写在线读、优雅的 GC 设计以及引入 BlueStore 引擎等。当然 NOS 在实际生产环境中的问题不止这些,还包括诸如:
对于这些问题我们也做了相应的优化和改进,欢迎和我们交流。同时鉴于笔者对存储引擎、Ceph 等知识的掌握水平有限,如有表述不当或者错误之处,欢迎指正。
作者:
网易数帆 - 对象存储(NOS)团队