1 背景
Curve(https://github.com/opencurve/…)是网易数帆自主设计研发的高性能、易运维、全场景反对的云原生软件定义存储系统,旨满足 Ceph 自身架构难以撑持的一些场景的需要,于 2020 年 7 月正式开源。以后由 CurveBS 和 CurveFS 两个子项目形成,别离提供分布式块存储和分布式文件存储两种能力。其中 CurveBS 曾经成为开源云原生数据库 PolarDB for PostgreSQL 的分布式共享存储底座,撑持其存算拆散架构。
在 CurveBS 的设计中,数据服务器 ChunkServer 数据一致性采纳基于 raft 的分布式一致性协定去实现的。
典型的基于 raft 一致性的写 Op 实现如下图所示:
以常见的三正本为例,其大抵流程如下:
- 首先 client 发送写 op(步骤 1),写 op 达到 Leader 后(如果没有 Leader,先会进行 Leader 选举,写 Op 总是先发送给 Leader),Leader 首先会接管写 Op,生成 WAL(write ahead log),将 WAL 长久化到本地存储引擎(步骤 2),并同时并行将 WAL 通过日志发送 rpc 发送给两个 Follower(步骤 3)。
- 两个 Follower 在收到 Leader 的日志申请后,将收到的日志长久化到本地存储引擎(步骤 4)后,向 Leader 返回日志写入胜利(步骤 5)。
- 一般来说,Leader 日志总是会先实现落盘,此时再收到其余一个 Follower 的日志胜利的回复后,即达成了大多数条件,就开始将写 Op 提交到状态机,并将写 Op 写入本地存储引擎(步骤 6)。
- 实现上述步骤后,即示意写 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 端打快照的过程,如下图所示:
打快照过程的几个关键点:
- 打快照这一过程是进 StateMachine 与读写 Op 的 Apply 在 StateMachine 排队执行的;
- 快照所蕴含的 last_applied_index 在调用 StateMachine 执行保留快照之前,就曾经保留了,也就是说执行快照的时候肯定能够保障保留的 last_applied_index 曾经被 StateMachine 执行过 Apply 了;
- 而如果批改 StatusMachine 的写 Op Apply 去掉 O_DSYNC,即不 sync,那么就会存在可能快照在 truncate 到 last_applied_index,写 Op 的 Apply 还没真正 sync 到磁盘,这是咱们须要解决的问题;
3 解决方案
解决方案有两个:
3.1 计划一
- 既然打快照须要保障 last_applied_index 为止 apply 的写 Op 必须 Sync 过,那么最简略的形式,就是在执行打快照时,执行一次 Sync。这里有 3 种形式,第一是对全盘进行一次 FsSync。第二种形式,既然咱们的打快照过程须要保留以后 copyset 中的所有 chunk 文件到快照元数据中,那么咱们人造就有以后快照的所有文件名列表,那么咱们能够在打快照时,对所有文件进行一次逐个 Sync。第三种形式,鉴于一个复制组的 chunk 数量可能很多,而写过的 chunk 数量可能不会很多,那么能够在 datastore 执行写 op 时,保留须要 sync 的 chunkid 列表,那么在打快照时,只有 sync 上述列表中的 chunk 就能够了。
- 鉴于上述 3 种 sync 形式可能比拟耗时,而且咱们的打快照过程目前在状态机中是“同步”的执行的,即打快照过程会阻塞 IO,那么能够思考将打快照过程改为异步执行,同时这一批改也可缩小打快照时对 IO 抖动的影响。
3.2 计划二
计划二则更为简单,既然去掉 O_DSYNC 写之后,咱们目前不能保障 last_applied_index 为止的写 Op 都被 Sync 了,那么思考将 ApplyIndex 拆分称为两个,即 last_applied_index 和 last_synced_index。具体做法如下:
- 将 last_applied_index 拆分成两个 last_applied_index 和 last_synced_index,其中 last_applied_index 意义不变,减少 last_synced_index,在执行一次全盘 FsSync 之后,将 last_applied_index 赋值给 last_synced_index;
- 在前述打快照步骤中,将打快照前保留 last_applied_index 到快照元数据变更为 last_synced_index,这样即可保障在打快照时,快照蕴含的数据肯定被 sync 过了;
- 咱们须要一个后盾线程定期去执行 FsSync,通过定时器,定期执行 Sync Task。执行过程可能是这样的:首先后盾 sync 线程遍历所有的状态机,拿到以后的所有 last_applied_index,执行 FsSync,而后将上述 last_applied_index 赋值给对于状态机的 last_synced_index;
3.3 两种计划的优缺点:
- 计划一改变较为简单,只须要改变 Curve 代码,不须要动 braft 的代码,对 braft 框架是非侵入式的;计划二则较为简单,须要改变 braft 代码;
- 从快照执行性能来看,计划一会使得原有快照变慢,因为原有快照时同步的,因而最好在这次批改中改成异步执行快照;当然计划二也能够优化原有快照为异步,从而缩小对 IO 的影响;
3.4 采取的计划:
- 采纳计划一实现形式,起因是对 braft 的非侵入式批改,对于代码的稳定性和对后续的兼容性都有益处。
- 至于对 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 交换群: