写点什么

Curve 基于 Raft 的写时延优化

作者:网易数帆
  • 2022 年 4 月 26 日
  • 本文字数:3890 字

    阅读完需:约 13 分钟

Curve 基于 Raft 的写时延优化

1 背景

Curve(https://github.com/opencurve/curve )是网易数帆自主设计研发的高性能、易运维、全场景支持的云原生软件定义存储系统,旨满足 Ceph 本身架构难以支撑的一些场景的需求,于 2020 年 7 月正式开源。当前由 CurveBS 和 CurveFS 两个子项目构成,分别提供分布式块存储和分布式文件存储两种能力。其中 CurveBS 已经成为开源云原生数据库PolarDB for PostgreSQL的分布式共享存储底座,支撑其存算分离架构。



在 CurveBS 的设计中,数据服务器 ChunkServer 数据一致性采用基于 raft 的分布式一致性协议去实现的。


典型的基于 raft 一致性的写 Op 实现如下图所示:



以常见的三副本为例,其大致流程如下:


  1. 首先 client 发送写 op(步骤 1),写 op 到达 Leader 后(如果没有 Leader,先会进行 Leader 选举,写 Op 总是先发送给 Leader),Leader 首先会接收写 Op,生成 WAL(write ahead log),将 WAL 持久化到本地存储引擎(步骤 2), 并同时并行将 WAL 通过日志发送 rpc 发送给两个 Follower(步骤 3)。

  2. 两个 Follower 在收到 Leader 的日志请求后,将收到的日志持久化到本地存储引擎(步骤 4)后,向 Leader 返回日志写入成功(步骤 5)。

  3. 一般来说,Leader 日志总是会先完成落盘,此时再收到其他一个 Follower 的日志成功的回复后,即达成了大多数条件,就开始将写 Op 提交到状态机,并将写 Op 写入本地存储引擎(步骤 6)。

  4. 完成上述步骤后,即表示写 Op 已经完成,可以向 client 返回写成功(步骤 7)。在稍晚一些时间,两个 Follower 也将收到 Leader 日志提交的消息,将写 Op 应用到本地存储引擎(步骤 9)。


在目前 CurveBS 的实现中,写 Op 是在 raft apply 到本地存储引擎(datastore)时,使用了基于 O_DSYNC 打开的 sync 写的方式。实际上,在基于 raft 已经写了日志的情况下,写 Op 不需要 sync 就可以安全的向 client 端返回,从而降低写 Op 的时延,这就是本文所述的写时延的优化的原理。


其中的代码如下,在 chunkfile 的 Open 函数中使用了 O_DSYNC 的标志。


CSErrorCode CSChunkFile::Open(bool createFile) {    WriteLockGuard writeGuard(rwLock_);    string chunkFilePath = path();    // Create a new file, if the chunk file already exists, no need to create    // The existence of chunk files may be caused by two situations:    // 1. getchunk succeeded, but failed in stat or load metapage last time;    // 2. Two write requests concurrently create new chunk files    if (createFile        && !lfs_->FileExists(chunkFilePath)        && metaPage_.sn > 0) {        std::unique_ptr<char[]> buf(new char[pageSize_]);        memset(buf.get(), 0, pageSize_);        metaPage_.version = FORMAT_VERSION_V2;        metaPage_.encode(buf.get());
int rc = chunkFilePool_->GetFile(chunkFilePath, buf.get(), true); // When creating files concurrently, the previous thread may have been // created successfully, then -EEXIST will be returned here. At this // point, you can continue to open the generated file // But the current operation of the same chunk is serial, this problem // will not occur if (rc != 0 && rc != -EEXIST) { LOG(ERROR) << "Error occured when create file." << " filepath = " << chunkFilePath; return CSErrorCode::InternalError; } } int rc = lfs_->Open(chunkFilePath, O_RDWR|O_NOATIME|O_DSYNC); if (rc < 0) { LOG(ERROR) << "Error occured when opening file." << " filepath = " << chunkFilePath; return CSErrorCode::InternalError; }...}
复制代码

2 问题分析

先前之所以使用 O_DSYNC,是考虑到 raft 的快照场景下,数据如果没有落盘,一旦开始打快照,日志也被 Truncate 掉的场景下,可能会丢数据,目前修改 Apply 写不 sync 首先需要解决这个问题。


首先需要分析清楚 Curve ChunkServer 端打快照的过程,如下图所示:



打快照过程的几个关键点:


  1. 打快照这一过程是进 StateMachine 与读写 Op 的 Apply 在 StateMachine 排队执行的;

  2. 快照所包含的 last_applied_index 在调用 StateMachine 执行保存快照之前,就已经保存了,也就是说执行快照的时候一定可以保证保存的 last_applied_index 已经被 StateMachine 执行过 Apply 了;

  3. 而如果修改 StatusMachine 的写 Op Apply 去掉 O_DSYNC,即不 sync,那么就会存在可能快照在 truncate 到 last_applied_index,写 Op 的 Apply 还没真正 sync 到磁盘,这是我们需要解决的问题;

3 解决方案

解决方案有两个:

3.1 方案一

  1. 既然打快照需要保证 last_applied_index 为止 apply 的写 Op 必须 Sync 过,那么最简单的方式,就是在执行打快照时,执行一次 Sync。这里有 3 种方式,第一是对全盘进行一次 FsSync。第二种方式,既然我们的打快照过程需要保存当前 copyset 中的所有 chunk 文件到快照元数据中,那么我们天然就有当前快照的所有文件名列表,那么我们可以在打快照时,对所有文件进行一次逐一 Sync。第三种方式,鉴于一个复制组的 chunk 数量可能很多,而写过的 chunk 数量可能不会很多,那么可以在 datastore 执行写 op 时,保存需要 sync 的 chunkid 列表,那么在打快照时,只要 sync 上述列表中的 chunk 就可以了。

  2. 鉴于上述 3 种 sync 方式可能比较耗时,而且我们的打快照过程目前在状态机中是“同步”的执行的,即打快照过程会阻塞 IO,那么可以考虑将打快照过程改为异步执行,同时这一修改也可减少打快照时对 IO 抖动的影响。

3.2 方案二

方案二则更为复杂,既然去掉 O_DSYNC 写之后,我们目前不能保证 last_applied_index 为止的写 Op 都被 Sync 了,那么考虑将 ApplyIndex 拆分称为两个,即 last_applied_index 和 last_synced_index。具体做法如下:


  1. 将 last_applied_index 拆分成两个 last_applied_index 和 last_synced_index,其中 last_applied_index 意义不变,增加 last_synced_index,在执行一次全盘 FsSync 之后,将 last_applied_index 赋值给 last_synced_index;

  2. 在前述打快照步骤中,将打快照前保存 last_applied_index 到快照元数据变更为 last_synced_index,这样即可保证在打快照时,快照包含的数据一定被 sync 过了;

  3. 我们需要一个后台线程定期去执行 FsSync,通过定时器,定期执行 Sync Task。执行过程可能是这样的: 首先后台 sync 线程遍历所有的状态机,拿到当前的所有 last_applied_index,执行 FsSync,然后将上述 last_applied_index 赋值给对于状态机的 last_synced_index;

3.3 两种方案的优缺点:

  1. 方案一改动较为简单,只需要改动 Curve 代码,不需要动 braft 的代码,对 braft 框架是非侵入式的;方案二则较为复杂,需要改动 braft 代码;

  2. 从快照执行性能来看,方案一会使得原有快照变慢,由于原有快照时同步的,因此最好在这次修改中改成异步执行快照;当然方案二也可以优化原有快照为异步,从而减少对 IO 的影响;

3.4 采取的方案:

  1. 采用方案一实现方式,原因是对 braft 的非侵入式修改,对于代码的稳定性和对后续的兼容性都有好处。

  2. 至于对 chunk 的 sync 方式,采用方案一的第 3 种方式,即在 datastore 执行写 op 时,保存需要 sync 的 chunkid 列表,同时在打快照时,sync 上述列表中的 chunkid,从而保证 chunk 全部落盘。这一做法避免频繁的 FsSync 对全部所有 chunkserver 的造成 IO 的影响。此外,在执行上述 sync 时,采用批量 sync 的方式,并对 sync 的 chunkid 进行去重,进而减少实际 sync 的次数,从而减少对前台 IO 造成的影响。

4 POC

以下进行 poc 测试,测试在直接去掉 O_DSYNC 情况下,针对各种场景对 IOPS,时延等是否有优化,每组测试至少测试两次,取其中一组。


测试所用 fio 测试参数如下:


  • 4K 随机写测试单卷 IOPS:


[global]rw=randwritedirect=1iodepth=128ioengine=libaiobsrange=4k-4kruntime=300group_reportingsize=100G
[disk01]filename=/dev/nbd0
复制代码


  • 512K 顺序写测单卷带宽:


[global]rw=writedirect=1iodepth=128ioengine=libaiobsrange=512k-512kruntime=300group_reportingsize=100G  [disk01]filename=/dev/nbd0
复制代码


  • 4K 单深度随机写测试时延:


[global]rw=randwritedirect=1iodepth=1ioengine=libaiobsrange=4k-4kruntime=300group_reportingsize=100G
[disk01]filename=/dev/nbd0
复制代码


集群配置:


4.1 HDD 对比测试结果


上述测试在 RAID 卡 cache 策略 writeback 下性能有略微提高,但是提升效果并不明显,512K 顺序写场景下甚至略有下降,并且还发现在去掉 O_DSYNC 后存在 IO 剧烈抖动的现象。


我们怀疑由于 RAID 卡缓存的关系,使得性能提升不太明显,因此,我们又将 RAID 卡 cache 策略设置为 writethough 模式,继续进行测试:



在 RAID 卡 cache 策略 writethough 模式下,性能提升较为明显,单卷 4K 随机写大约有 20%左右的提升。

4.2 SSD 对比测试结果

SSD 的测试在 RAID 直通模式(JBOD)下测试,性能对比如下:



可以看到在上述场景下,测试效果有较大提升,4K 随机写场景下 IOPS 几乎提升了 100%,512K 顺序写也有较大提升,时延也有较大降低。

5 总结

上述优化适用于 Curve 块存储,基于 RAFT 分布式一致性协议,可以减少 RAFT 状态机应用到本地存储引擎的一次立即落盘,从而减少 Curve 块存储的写时延,提高 Curve 块存储的写性能。在 SSD 场景下测试,性能有较大提升。对于 HDD 场景,由于通常启用了 RAID 卡缓存的存在,效果并不明显,因此我们提供了开关,在 HDD 场景可以选择不启用该优化。


本文作者:许超杰,网易数帆资深系统开发工程师


  • Curve 技术合集:https://zhuanlan.zhihu.com/p/311590077

  • Curve 主页:http://www.opencurve.io/

  • Curve 源码:https://github.com/opencurve/curve

  • 扫码加入 Curve 交流群:



发布于: 刚刚阅读数: 2
用户头像

网易数帆

关注

专注数字化转型基础软件研发 2020.07.22 加入

源自网易杭州研究院,是网易数字经济的创新载体和技术孵化器。聚合云计算、大数据、人工智能等新型技术,聚焦研发数据智能、软件研发、基础设施与中间件等基础软件,推动数字化业务发展。

评论

发布
暂无评论
Curve 基于 Raft 的写时延优化_开源_网易数帆_InfoQ写作社区