乐趣区

关于云原生:Curve-基于-Raft-的写时延优化

1 背景

Curve(https://github.com/opencurve/…)是网易数帆自主设计研发的高性能、易运维、全场景反对的云原生软件定义存储系统,旨满足 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=randwrite
    direct=1
    iodepth=128
    ioengine=libaio
    bsrange=4k-4k
    runtime=300
    group_reporting
    size=100G
    
    [disk01]
    filename=/dev/nbd0
  • 512K 程序写测单卷带宽:

    [global]
    rw=write
    direct=1
    iodepth=128
    ioengine=libaio
    bsrange=512k-512k
    runtime=300
    group_reporting
    size=100G
     
     
    [disk01]
    filename=/dev/nbd0
  • 4K 单深度随机写测试时延:
[global]
rw=randwrite
direct=1
iodepth=1
ioengine=libaio
bsrange=4k-4k
runtime=300
group_reporting
size=100G

[disk01]
filename=/dev/nbd0

集群配置:

机器 roles disk
server1 client,mds,chunkserver ssd/hdd * 18
server2 mds,chunkserver ssd/hdd * 18
server3 mds,chunkserver ssd/hdd * 18

4.1 HDD 比照测试后果

场景 优化前 优化后
单卷 4K 随机写 IOPS=5928, BW=23.2MiB/s, lat=21587.15usec IOPS=6253, BW=24.4MiB/s, lat=20465.94usec
单卷 512K 程序写 IOPS=550, BW=275MiB/s,lat=232.30msec IOPS=472, BW=236MiB/s,lat=271.14msec
单卷 4K 单深度随机写 IOPS=928, BW=3713KiB/s, lat=1074.32usec IOPS=936, BW=3745KiB/s, lat=1065.45usec

上述测试在 RAID 卡 cache 策略 writeback 下性能有稍微进步,然而晋升成果并不显著,512K 程序写场景下甚至略有降落,并且还发现在去掉 O_DSYNC 后存在 IO 激烈抖动的景象。

咱们狐疑因为 RAID 卡缓存的关系,使得性能晋升不太显著,因而,咱们又将 RAID 卡 cache 策略设置为 writethough 模式,持续进行测试:

场景 优化前 优化后
单卷 4K 随机写 IOPS=993, BW=3974KiB/s,lat=128827.93usec IOPS=1202, BW=4811KiB/s, lat=106426.74usec
单卷单深度 4K 随机写 IOPS=21, BW=85.8KiB/s,lat=46.63msec IOPS=38, BW=154KiB/s,lat=26021.48usec

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

4.2 SSD 比照测试后果

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

场景 优化前 优化后
单卷 4k 随机写 bw=83571KB/s, iops=20892,lat=6124.95usec bw=178920KB/s, iops=44729,lat=2860.37usec
单卷 512k 程序写 bw=140847KB/s, iops=275,lat=465.08msec bw=193975KB/s, iops=378,lat=337.72msec
单卷单深度 4k 随机写 bw=3247.3KB/s, iops=811,lat=1228.62usec bw=4308.8KB/s, iops=1077,lat=925.48usec

能够看到在上述场景下,测试成果有较大晋升,4K 随机写场景下 IOPS 简直晋升了 100%,512K 程序写也有较大晋升,时延也有较大升高。

5 总结

上述优化实用于 Curve 块存储,基于 RAFT 分布式一致性协定,能够缩小 RAFT 状态机利用到本地存储引擎的一次立刻落盘,从而缩小 Curve 块存储的写时延,进步 Curve 块存储的写性能。在 SSD 场景下测试,性能有较大晋升。对于 HDD 场景,因为通常启用了 RAID 卡缓存的存在,成果并不显著,因而咱们提供了开关,在 HDD 场景能够抉择不启用该优化。

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

  • Curve 技术合集:https://zhuanlan.zhihu.com/p/…
  • Curve 主页:http://www.opencurve.io/
  • Curve 源码:https://github.com/opencurve/…
  • 扫码退出 Curve 交换群:

退出移动版