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=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

集群配置:

机器rolesdisk
server1client,mds,chunkserverssd/hdd * 18
server2mds,chunkserverssd/hdd * 18
server3mds,chunkserverssd/hdd * 18

4.1 HDD比照测试后果

场景优化前优化后
单卷4K 随机写IOPS=5928, BW=23.2MiB/s, lat=21587.15usecIOPS=6253, BW=24.4MiB/s, lat=20465.94usec
单卷512K程序写IOPS=550, BW=275MiB/s,lat=232.30msecIOPS=472, BW=236MiB/s,lat=271.14msec
单卷4K单深度随机写IOPS=928, BW=3713KiB/s, lat=1074.32usecIOPS=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.93usecIOPS=1202, BW=4811KiB/s, lat=106426.74usec
单卷单深度4K随机写IOPS=21, BW=85.8KiB/s,lat=46.63msecIOPS=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.95usecbw=178920KB/s, iops=44729,lat=2860.37usec
单卷512k程序写bw=140847KB/s, iops=275,lat=465.08msecbw=193975KB/s, iops=378,lat=337.72msec
单卷单深度4k随机写bw=3247.3KB/s, iops=811,lat=1228.62usecbw=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交换群: