文章目录

  • 术语表(方便大家理解下文)
  • 1 背景
    • 1.1 Block Interface SSD 的问题
      • 1.1.1 性能问题
      • 1.1.2 空间放大问题
    • 1.2 业界尝试过的解决方案
      • 1.2.1 Stream SSDs
      • 1.2.2 Open-Channel SSDs
  • 2 ZNS 架构
    • 2.1 ZNS storage 模型演进
      • 2.1.1 zone storage model
      • 2.1.2 Evolving to ZNS
    • 2.2 ZNS 的架构实现
      • 2.2.1 ZNS 实现过程中的一些PR
      • 2.2.2 Zenfs 的设计实现
        • 2.2.2.1 为什么zenfs如此看重rocksdb(LSM-tree 架构)
        • 2.2.2.2 zenfs 详细设计
  • 3 性能测试
    • 3.1 环境搭建
    • 3.2 原始设备的性能
    • 3.3 应用测试性能
  • 4 参考

最近在做ZNS-ssd 相关的测试,因为 rocksdb 社区有一个 Hans 提的 Zenfs的PR,引起了很多大佬的讨论。虽然还是没有合入主线(其仅仅是作为了一个文件系统的backend,而且需要特殊的硬件支持,并且合入需要单独维护几千行代码,对于社区来说代价还是有点高,大家建议Hans 单独拉一个子项目,作为rocksdb的plug),但是在讨论过程中大家对Zenfs的接入还是比较欢迎的。

为了能深入了解zenfs的实现原理,能够在未来紧跟存储硬件的趋势,那ZNS-SSD 则是首先需要了解的。

本文将基于ZNS-SSD 论文 : ZNS: Avoiding the Block Interface Tax for Flash-based SSDs 的介绍 以及 实际测试过程中对 libzbd 和 zenfs 代码的了解 来深入分析ZNS-ssd的 实现机制。

术语表(方便大家理解下文)

ZNS: zoned namespace
page: ssd内部的最小的写单位,一般为4K
block: ssd内部的最小的擦除单位,一般为32K,在其之上还有plane, die, package
OP: over providing,ssd内部过度供应的空间,是各个厂商为GC预留的空间,对用户不可见
NMOS:浮栅晶体管, 现在的大多数 NAND ssd的底层存储单元;对应的3D XPoint的底层存储单元是PCM(相变内存)
OC-SSD: open channel ssd
WP : zone 内部用来调度写入的 write pointer,表示当前zone 可写的下一个LBA 地址

1 背景

ZNS-SSD 出现的起源肯定是因为传统的block interface ssd 也就是 Flash-ssd 有一些无法很好解决的问题。

1.1 Block Interface SSD 的问题

1.1.1 性能问题

Flash-SSD 底层大多数是NMOS 存储单元,为了保证NMOS 存储单元的0/1可以稳态体现,写之前需要先擦除,更底层的原理可以参考 从NMOS 和 PCM 底层存储单元 来看NAND和3D XPoint的本质区别。因为这个问题,管理 Flash-SSD 的 FTL (Flash-Transfer-Layer) 需要做很多的取舍。为了尽可能高效得擦除存储单元,FTL 将擦除的最小粒度设置成了block 32K(不同的SSD应该是不一样的),而写入则是按照 page 4K 粒度进行。

除此之外,FTL 更多的工作是 管理 LBAs(logical block address, 将操作系统的块驱动层下发的请求的逻辑偏移地址转化为Flash-ssd的物理地址)、回收过期/删除的数据、调度用户的随机写等。因为 NMOS 的写特性, 随着 SSD 逐渐接近满态,FTL查找可用空闲 page 或者 GC 过期 block 都会让整个 SSD 的性能不可预估。可能 SSD 刚开始的时候拥有较高的吞吐和较低的长尾,而随着写入的增加, 吞吐会有一定程度的下降,长尾则更是不可预期。

1.1.2 空间放大问题

因为FTL的GC需求,因为当想要擦除一个block的时候,这个block中的部分page还是有未过期的数据的,这一些数据需要移动到新的page中。则就需要一部分空闲来作为GC过程中的未过期数据的存储,而这一些空间则不会体现在SSD实际的可用容量中。而且这一些空间占用总容量的比例在不同厂商生产的SSD下最高能够达到28%,也就是实际1T的SSD,用户可用的也就720G,这一些OP(over providing)导致的空间放大会体现在用户的成本之上,无形中增加了用户的存储成本。

关于GC引入的空间放大,用下图举个例子:

这是我们现在的SSD内部block 和 page的分布,可以看到3 种不同的文件(用颜色代替了属于不同文件的page)的page是随机分布在整个SSD的 块空间中。
每一个小方块,代表的是一个ssd 的page, 4个小方块属于一个block,这个block也是擦除的最小粒度。最下面的则是OP空间,SSD厂商专门为GC 预留的空间,对用户不可见。

当我们删除其中一个文件的时候,比如我们删除 FileB,那么SSD内部属于File C的 这一些 page 就是过期的,需要被清理。
但是,我们想要擦除这一些过期较多的block的时候需要将其中未过期的page 移动到 OP 空间内。

当我们将可用的block中的page移动到OP空间之后,整个block 就可以被GC,从而能够用作其他数据的存储,下图中之前移动有效page到op空间之后的两个block后续就可以被擦除掉。

可以看到GC的整个过程需要涉及到数据的迁移和OP空间的占用,而且频繁的GC还会导致NMOC的寿命不断降低,因为NMOC的的两个氧化层在频繁的写/擦除之下会不断得被消耗。

1.2 业界尝试过的解决方案

1.2.1 Stream SSDs

这种类型的ssd 通过 SSD主控 来对写入的请求进行标记,请求进入到流式SSD内部的时候根据预定义好的流式ssd标记来将带有不同标记的输入command 分配存储到不同的擦除块之内。这样,能够极大得节省因为FTL 调度GC导致的性能以及ssd寿命的损失。但是这种流式SSD 对主控也就是 host的要求比较高,需要host能够快速的区分接收到的写入指令,如果主控不能很好得区分写入指令的stream hint 类型,那仍然就像block interface 的ssd一样,GC需要由FTL进行调度。

也就是stream ssd想要保证自己有足够的资源和效率来区分写入的command,那就需要消耗更多的内存(保存未区分的状态) 以及性能更好的主控芯片,然而,对这一些资源的需求并不能降低SSD本身的成本。

而具体stream ssds 的性能能有多好,是不是能弥补成本提升所带来的损失,按照现在的业界硬件存储发展来看,应该是没戏了,不过ZNS的论文中做了测试,后续会提到。

1.2.2 Open-Channel SSDs

这种类型的SSD之前业界还是有很多探索的,而且百度已经将一种 OC- SSD SDF型号 大规模部署在了自己的基础架构的集群里 。OC-SSD 它允许SSD主控和SSD共同管理 一个 连续的LAB 块的集合,并且对外暴露的操作指令是擦除块对齐的。这种按照擦除块对齐的访问模型能够消除 SSD 内部GC的代价,并且降低对OP空间的消耗(不需要预留一部分空间用作擦出块内部的正常数据page的迁移)以及 DRAM的消耗(DRAM 需要保存LBA,因为OC是按照擦除块对齐的,擦除块相比于page大一些,所以需要的LBA 空间就少一些)。

总的来说 OC-ssd 能够提供以下几个特性:

  1. 将SSD内部的可以并行操作的特性暴露给了用户。每一个chunk 可以看作是一个channel,也就是一块独立的存储区域,类似/dev/sda这种像是传统SSD的块设备。一个SDF 的OC设备最多能够提供43个channel,那它这一个ssd 就能作为/dev/sda1 — /dev/sda43 这么多个块设备供用户使用。
  2. 提供了非对称的I/O接口。即读取的最小单元(page,在SDF 中page 是8K)和写入的最小单元(erase block, SDF 中是 2M)可以不同,在 SDF 中写入是擦除块对齐的,所以能够最大程度得减少甚至消除GC引入的写放大。
  3. erase 操作作为了一个 设备指令,并且暴露给了用户。erase操作在传统的 block interface 的ssd中成本非常高,相比于读写操作来说延时非常长,在SDF 中erase 一个2M 的block 延时甚至能够达到 3ms。所以,即使是在 open-channel 内部去调度erase操作,也会引入较高的延时。OC聪明的一点是将调度 erase 指令的权限暴露给了用户,用户自己去选择什么时候调度OC内部的erase,自己去考虑怎么解决 erase 延时的问题。
  4. 为 OC-ssd 设计的一个短链路的I/O栈。os 的 block layer 的请求调度是极为复杂的,并且 os的传统I/O栈成为很多高性能硬件存储的瓶颈(NVM-PMEM 这样的存储硬件完全不用os 的 块层存储栈),所以 在SDF 的实现中,为了降低对OC 的访问延时,by-pass了大部分的I/O栈,上次应用(比如leveldb 的sst)的访问通过 ioctl 直接与底层的 driver 进行交互,这样能够极大得降低访问延时。在SDF 中,leveldb 的 sst 访问延时能够降低到 2-4us,这个量级 对于 走PCIe+NVMe 的 ssd 来说确实很低了。

所以OC-ssd 能够提升SSD的性能 以及 存储单元的寿命。但是,对LBA的管理 以及 erase block的回收和坏块管理就都得由host来做了。并且,host 需要兼容不同的SSD内部实现机制,并且对暴露统一的访问模式。想要让OC-ssd 通用,就需要不断得对host 部分的软件进行维护,维护成本较为高昂。并且OC还提供了自己的I/O栈,想要使用OC的高性能特性,就无法走操作系统的I/O栈,这对存储软件来说,还得自己实现一个块驱动层来 在OS内部管理 OC-ssd的存储空间才行,对于不同内核驱动/块层实现细节的 存储研发工程师来说接入成本实在是过于高昂。

贴一个百度架构 和高校合作 LevelDB + SDF(OC-SSD)的架构图,对OC-SSD 的大体使用形态就一目了然了:

可以看到 不论是 Stream-SSD 还是 OC-SSD 都能在一定程度缓解 甚至 解决 block-interface ssd的问题,但是要不就是硬件成本太高,要不就是接入成本太高,这对于做存储系统以及存储引擎的公司来说都是需要投入较长的时间以及人力成本才能看到收益。

所以,ZNS社区 很聪明得抓住了存储软件研发人员的 “痛点”,并吸取了前两者在设计上的优点以及解决了前两者的痛点,从而展现出了一个全新的、更容易被存储系统研发人员接受的 新硬件形态。

2 ZNS 架构

2.1 ZNS storage 模型演进

先大体看一下 ZNS-ssd 的内部数据分布:

ZNS-ssd 像 OC 那种 channel,将整个SSD内部的存储区域分为多个zone 空间,每一段zone空间管理一段LBA(物理page和逻辑地址之间的映射),不同的zone 空间之间的数据可以是独立的。最主要的是,每一个zone 内部的写入 只允许顺序写,可以随机读。因为 zone 内部的顺序写特性,基本可以消除 SSD GC的开销。为了保证zone 内部的顺序写,在ZNS内部想要覆盖写一段LBA地址的话需要先reset(清理当前地址的数据),才能重新顺序写这一段逻辑地址空间。

同时一个完整的 ZNS设备是支持完整的存储栈,包括底层的块驱动到上层的文件系统都是已经实现好的。这个完整的存储栈就是相比于OC-ssd的优势,能够对外屏蔽复杂的接入功能,紧抓用户的使用习惯,用户软件可以做到基本零成本接入。

当然,仅限于底层是顺序写的存储应用。rocksdb / ceph的bluestore 等。

整个存储栈的演进是一个过程,并不是直接就推出了ZNS的设计,下文将会详细介绍,从ZNS 的演进过程我们也能看到ZNS 可应用场景的一些限制。

2.1.1 zone storage model

最开始的时候西数推出了zone 形态的存储模型,也就是上图中SSD内部的存储空间按照zone 进行了逻辑划分,而且只允许顺序写,覆盖写的前提是reset 之前的LBA。

这个时候的zone storage model 内部实现 以及一些约束可以从以下几个方面来看:

1 Per-zone state machine
如果想要确定一个zone 是否能够写,需要通过一系列状态进行判断,下图是zone 的内部状态变化。

这里关注的主要是几个重要的外部状态:

  • Empty : 所有zone 未使用前的开始状态。
  • Open : 内部有两种变迁状态,想要写入的话先从 Empty转化为Open 状态,随后该zone 就是可写的。
  • Full:持续写入,达到一个zone 空间的上限之后会进入Full 状态。
  • Closed: 如果 zone ssd 有最大zone 个数限制,当处于Open的zone 个数达到了limit 限制,还想Open新的zone, 这一些限制中的某一个zone 需要切换到Close状态。Closed 状态的zone还是可写的,但是需要先进入Open 状态才写。

2 write pointer ,后续简称为WP

write pointer 表示一个可写的zone 内部 可写的下一个LBA地址,当前zone可写的状态是Empty和 Open。当zone 的状态是Full 的时候,任何从当前WP 开始 的写指令都会失败,当 一个 zone 调度了 reset 指令,就会清理当前zone 上所有的数据,并将当前zone 的状态置为 Empty,同时WP 的LBA 也会移动到zone 最开始的 LBA。

下图的WP 指向的是当前writable zone 中的下一个可写的LBA地址,在WP 指向的LBA 之前的LBA都是已经写过数据的了。

3 writable zone capacity

这个特性是 zone storage 以及 后续的 ZNS 为了兼容 行业规范(论文中说的是SMR HDD 的 power-of-two zone size industry norm,没有找到这个规范的出处 )推出的。个人绝对最重要的特性还是直观得告诉用户当前zone 的可用容量,论文中这样图解释的比较清楚:

一个zone 会被分为是那个区域

  • Written LBAs
  • Unwritten LBAs
  • Unmapped LBAs

这样,zone size 就是三个容量之和;而 zone capacity 则是 written + Unwritten 之和,也就是 WP 可用的LBAs 的容量。对于 Unmmapped LBAs 因为没有建立这一部分空间的逻辑地址和物理地址的映射,那肯定就不可用了。

有了zone capacity,就能很好得控制 LBA 映射表的大小,不像interface ssd一样,一开始就对整个存储空间建立一个 LBA 映射表,如果磁盘足够大,那对内存的消耗就比较大了;有了Capacity, 那当capacity 快要被消耗光的时候再建立LBA,就能少一部分的内存消耗了。

4 active zone limit

这是对于 NAND 存储介质的磁盘的一种限制。如果存储介质是NAND,则因为电源异常的时候需要通过 附加电源电容器确保将 其内部NMOS 存储单元的电子归位(奇偶校验),当前服务器附加电容器因为个数有限,内部的电容只能支持部分zone 空间的奇偶校验,所以 zone storage 为了保证数据一致性,会限制最大同时可写(处于 Open 和 Closed 状态的zone)的 zone 的个数。然而这个限制 在机械硬盘的存储介质(磁存储)下就不会有了,因为不需要额外的 附加电源电容器。

现在的ZN540 zns-ssd 最大允许 14个 active zones,当然实际的性能并不会说因为有active zone limit 个数的限制而下降,当前的active zones 还是能够发挥出磁盘本身的性能上限的。

西数的SSD 性能还是略差一些,单GB价格相近的NVMe ssd相比于 Intel 同等价格的写性能差不少,单读性能又比较接近,可能与SSD 主控性能关系比较大吧。。。

zone storage 的思想还是可借鉴的。

2.1.2 Evolving to ZNS

下文将介绍 对于 ZNS-ssd 的推出 可能影响其性能的一些 硬件限制和主控软件的设计。

1 硬件限制

  • Zone sizing

    首先传统的interface ssd 的擦除单位会比一般的写请求的最小粒度大不少。所以 ZNS-ssd 会考虑再擦除效率和写入自由度之间的平衡,在保证最大性能的前提下选择合适的 擦除块大小。ZN540 的擦除块是2M。

  • Mapping table

    这个之前说过,就是传统的block interface 的ssd 会对整个存储空间的物理地址建立对应的逻辑地址映射,也就是 LBAs 和 物理地址的映射表。当然,对整个SSD 进行这样的映射是为了得最大程度得提升GC效率, GC的时候查找空闲块的话只需要访问内存中的这一张映射表就能知道哪一些 LBAs 对应的 物理块是空闲的。存在的问题也很明显,就是平均1TB 的磁盘需要1G的 内存空间来存储这个映射表。如果磁盘以及容量都增加的话,LBA tables 消耗的内存对于用户来说就很难接受了。

    这个问题,ZNS中因为每一个 zone space 都只能顺序写,这样对于ZNS 设备来说,只需要维护擦除块这种极为粗粒度的映射表了,能够极大得节省对内存空间的消耗。

2 主控软件的设计

这里主要是ZNS-ssd 为了让自己的易用性更强,提供的一些外部应用可接入的接口。

  • Host- Side FTL (HFTL)

    HFTL 的主要作用是 在 外部应用如果调度inplace-upate/随机写 则通过HFTL 转化为 ZNS-ssd 支持的 写语义。毕竟,inplace-update 的应用还是占据了市场的很大一部分(数据库存储领域的 mysql),对于这种应用 ZNS-ssd 不能说不支持,只能说相比于 LSM-tree 来说这种性能没有那么好罢了。

    HFTL 这里就像是传统的 ssd FTL,调度GC 并且 管理 LBA 映射表,同时也要管理 ZNS 设备用到的CPU和内存资源。如果用户调度的随机写场景 ,那ZNS 设备的运行机制就像是传统的磁盘设备了。

    现在已经支持 HFLT 管理的映射机制包括:dm-zone (SMR-HDD), dm-zap(ZNS-SSD,到目前为止还在开发中),pblk 和 SPDK’FTL 都显示了 HFTL 的可行性和适用性。

  • File System

    为了支持更多的应用来访问ZNS 设备,文件系统会作为一个更高层的存储接口,就像是POSIX API 一样为应用提供文件语义。现有的文件系统包括(f2fs, btrfs, zfs) 都能够支持顺序写模式,但是这一些文件系统的元数据的更新并非是顺序写,比如 log-structured write (文件系统的WAL),supperblock 的更新等 都不是顺序写。所以想要为ZNS 做一个更上层的应用,并且调度ZNS 内部的 各个zone 的状态 使用现有的这一些文件系统其实都不是很友好(过于复杂,且改造成本太高),虽然在前期的 zone storage 演进的过程中也在f2fs 这种文件系统上做了适配,但也仅限于 在ZAC/ZBC 这样的设备中使用(hdd),不适用于ZNS;所以后续ZNS 单独设计了适配于ZNS 的文件系统来为应用提供文件语义。

  • End-To-End Data Placement

    端到端的数据存储。传统的磁盘I/O栈 需要通过 内核文件系统,通用块层,I/O调度层,块设备驱动层 这样的一系列I/O子系统才能达到磁盘的主控。这一些漫长的链路无形中无拖慢数据的存储效率,间接降低磁盘吞吐,增加了请求的延时。然而 ZNS 的推出,ZNS 内部的设计本身能够极大的降低SSD 内部的写放大,提升写吞吐,同时少了很多的指令和对LBA的管理操作(erase block的粒度大,减少了 对LBA的需求),ZNS 做了一个非常大胆的挑战,开发了支持文件系统语义的端到端的访问形态的文件系统 Zenfs ,数据的存储绕开像原本 block interface 一样需要庞大的I/O栈,直接与ZNS-SSD 进行交互。

    有很多应用广泛的系统一直在 端到端的数据存储上进行探索,而且这一些系统能够支持顺序写模式,对于ZNS 的推出也是极大的支持,包括但不限于:LSM-tree 存储引擎 Rocksdb, 基于cache 的存储 CacheLib, 对象存储 Ceph 的seastore。后续会给出Zenfs 作为Rocksdb 的一个文件系统 backend 的性能测试数据。

2.2 ZNS 的架构实现

先看看 支持zone 存储的 SMR HDD 以及 支持 zonefs 的 nvme ssd 的整个存储栈形态

其中对于ceph 这样的应用来说 bluestore或者Seastore 这样的后端引擎是直接管理裸设备的,所以不需要文件系统支持。当然如果需要,也可以通过一个内核支持的小型文件系统zonefs 来进行数据访问。

但是这个小型文件系统过去简单,它将每一个zone空间暴露为一个文件,使用LBA0 来存储superblock,并没有复杂的inode/dentry 这种元数据的管理机制,在这个文件系统上创建/删除/重命名都是不允许的。针对数据的写还是类似zone storage的要求,即通过一个WP来进行写,如果这个zone 空间对应的文件被写满了,则WP 无法写入直到 对这个zone 执行了reset ,才会将WP 重新移动到LBA0,从而可写。

它对于Rocksdb 来说功能还不足,而且Rocksdb 只需要一个文件 用户态的 backend,虽然如是说,但是如何在rocksdb调度写的时候分配一个最优的zone,如何选择合适的时机删除sst文件(重置zone空间)都需要精心的设计在里面。

后来,Hans 主导设计了 Zenfs 来作为Rocksdb 的Backend 来进行端到端的请求调度,且选择最优的数据存储方式 并且在降低写放大(LSM-tree)、SSD 的磨损均衡、降低读长尾 等都做了较多的探索。

大体架构如下:

接下来我们仔细看看 ZNS 内部的一些实现的特性 以及 Zenfs 的详细设计实现。

2.2.1 ZNS 实现过程中的一些PR

  1. ZNS的特性 需要内核支持,所以开发了ZBD(zoned block device) 内核子系统 来提供通用的块层访问接口。除了支持内核通过ZBD 访问ZNS之外,还提供了用户API ioctl进行一些基础数据的访问,包括:当前环境 zone 设备的枚举,展示已有的zone的信息 ,管理某一个具体的zone(比如reset)。

  2. 在 FIO 内部支持了 对 ZBD的压测。

  3. 在近期,为了更友好得评估ZNS-ssd的性能,在ZBD 上支持了暴露 per zone capacity 和 active zones limit。

  4. Zenfs 的设计 并 作为 rocksdb 的一个文件系统backend。

这里是对应修改代码行数的概览:

从代码行数上来看,可以说是非常得轻量了。

2.2.2 Zenfs 的设计实现

2.2.2.1 为什么zenfs如此看重rocksdb(LSM-tree 架构)

之所以ZNS 社区对Rocksdb 这么看重,代码行数上的贡献上可以说 在Rocksdb 上投入的精力远超其他方面。

从LSM-tree原理上,我们可以看到几点:

  1. LSM-tree 的写入是append 顺序写,这适配 ZNS 的 zone 架构来说简直再合适不过。
  2. LSM-tree 的compaction 也是顺序写一批数据,然后再集中删除,这也符合 ZNS 的空间回收方式(每一个zone 状态是Full的时候,想要重新写,只有reset了)。在好的配置下相当于 SSD内部的GC 和 rocksdb的compaction 完美结合了。
  3. LSM-tree on 传统 ssd 的痛点比较明显。读方面:LSM-tree分层软件架构对读性能不友好(长尾较为严重),再加上ssd 的FTL GC会间接 让长尾不可预估;引以为傲的顺序写优势也因为SSD 内部的FTL 频繁GC 导致写性能抖动且相比于空载时的下降。这一些痛点在ZNS 下都能够被很好的避免甚至完全解决。

抛开 LSM-tree 本身on ssd 的劣势 之外,Rocksdb 则有一些自身特有的优势,值得 ZNS 社区持续投入:

  1. k/v 存储领域里应用广泛,适合用于高速存储介质(NVMe-ssd)
  2. 开源 且 拥有活跃的社区,社区也在持续跟进新的存储技术。包括:io_uring / spdk 等
  3. 可插拔的存储后端设计,实现一个fs backend,移植就非常容易(将zenfs 编译到rocksdb 代码中就可以看出来)。
2.2.2.2 zenfs 详细设计

先看一下总体Zenfs的系统架构概览,这个图是论文中的图,更简洁一些:

因为它要作为Rocksdb 的fs backend,负责和zoned block devcei 进行交互,那其继承自FileSystemWrapper类的基本接口肯定是都实现了。

主要的组件如下几个:

  1. Journaling and Data.

    Zenfs 定义了两种类型的zones: journal 和 data. 代码中ZonedBlockDevice 类管理的也就是两个vector , meta_zonesio_zones,下文统一称为Journal ZonesData Zones

    其中 Journal Zones 用来管理文件系统的元数据,包括异常时恢复文件系统的一致性状态,维护文件系统的superblock 以及 映射 wal 和 数据文件到 zone中。

    Data zones 则主要用于保存 sst 这样的数据文件。

  2. Extents.

    Rocksdb 的数据文件会被映射 写入到一个extents 集合中 std::vector<ZoneExtent*> extents_。其中一个extent 时一个变长但block对齐的连续LBA地址,而且会拿着一个标识当前sst的信息 顺序写入到一段zone空间中,会用ZoneFile这个数据结构标识文件以及属于这个文件的extents 数组。每一个 zone空间能够存储多个extents,但是一个extent 不会跨越多个zone而存在。

    Extent 的分配和释放都是一个内存数据结构来管理的,当一个文件变比或者这个extent的数据要持久化到磁盘 调用Fsync/Append时,内存的数据结构也会对应持久化到journal_zone之中。而且内存中这个数据结构会持续跟踪extents的分配情况,当一个zone内的所有extents 所属的文件都被删除,这个zone就可以被reset了,方便后续的 reuse。

  3. Superblock.

    Superblock 主要用来初始化Zenfs 或者 从磁盘异常恢复Zenfs 的状态。Superblock 会通过unique id, 魔数和用户选项 来标识属于当前磁盘的Zenfs。这个唯一标识 是 UUID(unique identifier),允许用户识别对应磁盘上的文件系统,即当磁盘的盘符重启或者外部插拔发生变化的时候仍然能够识别到这上面的文件系统。

  4. Journal.

    Journal的主要工作是维护 superblock 和 WAL 以及 sst 和 存储于zone中的extents的映射。
    Journal的数据主要存储在上图中的 Journal Zones中,也就是 代码中的 meat_zones,而且journal zone 是位于整个存储设备上的前三个永远不会offline的 zone ZENFS_META_ZONES。其中任何时刻,总会有一个zone是处于active的, 也就是必须可写的,不然这两个zone 被closed 的话就无法跟踪元数据了的更新了。

    其中最开始的那个active zone 会有一个header,包含:sequence number(每当有一个journal zone被初始化的时候都会自增),superblock 数据结构,当前journal 状态的一个snapshot。初始化的时候,header被持久化完成,整个zone剩下的capacity就可以开始接受新的data 数据更新了。

我们从一个 ZNS 磁盘初始化一个Zenfs的过程需要执行:

.plugin/zenfs/util/zenfs mkfs --zbd=$DEV --aux_path=/tmp/$AUXPATH --finish_threshold=10 --force

注意:在 $DEV 的设备名称只能是 nvme0n1 或者 nvme1n这种,不能加 /dev/nvme0n1,zenfs 会自己去环境中找 nvme0n1,不需要用户指定路径。

1 Zenfs::MkFS 所有的meta zone都会reset,并且在第一个meta zone上创建一个zenfs文件系统,执行如下内容

  1. 写一个superblock 的数据结构,包括sequence 的初始化 并持久化
  2. 初始化一个空的snapshot,并持久化。

2 Zenfs::Mount 从磁盘 Recovery 一个已经存在的zenfs 的几个步骤如下:

  1. 现在是三个journal zones,最开始的时候需要先读取三个 journal zones 的第一个LBA内容,从而确定每一个zone 的sequence,其中seq 最大的是当前的active zone(拥有最全的元数据新的zone)。
  2. 读取active zone的header 内容,并且初始化 superblock 和 jourace state。
  3. 对 journal 的更新都会同步到到 header 的snapshot 中。

这两步操作基本就构建好了一个完整的Zenfs 状态,后续就会持续接受用户的写入。

写入过程中 sst 的数据存储是通过 保存着extent 并由extent持久化到对应的zone空间中,那 如何选择一个Data zone 来作为存储当前文件数据的呢? 因为不同的zone 在实际接受数据存储时其 capacity 的容量是变化的。如果一个sst 文件的存储是跨zone的,那最后对一个zone的 reset 还需要考虑这个文件 是否被删除。

Best-Effort Alogthrim for Zone Selection

Zenfs 这里开发了 Best-effort 算法来选择zone 作为 rocksdb sst 文件的存储。Rocksdb 通过对 WAL 和 不同 level 的 sst 设置不同的 write_hint 来表示这一些文件的生命周期。

选择哪一种 write_hint, 则通过如下逻辑进行:

Env::WriteLifeTimeHint ColumnFamilyData::CalculateSSTWriteHint(int level) {if (initial_cf_options_.compaction_style != kCompactionStyleLevel) {return Env::WLTH_NOT_SET;}if (level == 0) {return Env::WLTH_MEDIUM;}int base_level = current_->storage_info()->base_level();// L1: medium, L2: long, ...if (level - base_level >= 2) {return Env::WLTH_EXTREME;} else if (level < base_level) {// There is no restriction which prevents level passed in to be smaller// than base_level.return Env::WLTH_MEDIUM;}return static_cast<Env::WriteLifeTimeHint>(level - base_level +static_cast<int>(Env::WLTH_MEDIUM));
}

也就是从当前总层数开始,倒数两层的sst 文件拥有最长的生命周期,level0 拥有 WITH_MEDIUM 的生命周期,WAL 则拥有最短的生命周期WITH_SHORT

回到Zenfs 选择zone 的过程,总的来说就是让 life_time 小的文件尽量存放在和它 life_time接近的zone中,这样更大概率得统一对整个zone 进行 reset :

(1) 对于新的写入,直接分配一个新的zone

(2) 优先从 active zones 中进行分配,如果能够找到合适的zone,则直接Reset 这个zone,并作为当前文件的存储。合适的zone 的条件是:如果当前文件的lifetime 比 active zone 中最老的数据 还小,则当前zone 比较合适作为当前文件的存储;如果有多个active zone满足这个条件,则选择一个最近比较的active zone。

(3) 如果从active zone中没有找到合适的zone,那直接分配一个新的zone。当然,分配的过程也就意味着判断active zone个数有没有超过 max_nr_active_io_zones_ ,超过了则需要关闭一个 active zone,然后才能分配一个新的zone。

逻辑如下:

Zone *ZonedBlockDevice::AllocateZone(Env::WriteLifeTimeHint file_lifetime) {...// best effort 算法的逻辑for (const auto z : io_zones) {if ((!z->open_for_write_) && (z->used_capacity_ > 0) && !z->IsFull()) {// 主要就是拿着当前 zone 的lifetime 和当前文件的file_lifetime (也就是write_hint)进行对比// 如果文件的life_time小,则当前zone 满足存储需求。unsigned int diff = GetLifeTimeDiff(z->lifetime_, file_lifetime);if (diff <= best_diff) {allocated_zone = z;best_diff = diff;}}}...// 如果从没有为当前文件找到找到合适的zone,那就得分配一个新的了if (best_diff >= LIFETIME_DIFF_NOT_GOOD) {/* If we at the active io zone limit, finish an open zone(if available) with* least capacity left */if (active_io_zones_.load() == max_nr_active_io_zones_ &&finish_victim != nullptr) {s = finish_victim->Finish();if (!s.ok()) {Debug(logger_, "Failed finishing zone");}active_io_zones_--;}if (active_io_zones_.load() < max_nr_active_io_zones_) {for (const auto z : io_zones) {if ((!z->open_for_write_) && z->IsEmpty()) {z->lifetime_ = file_lifetime;allocated_zone = z;active_io_zones_++;new_zone = 1;break;}}}}...
}

AllocateZone 完成之后就可以 更新当前文件在 分配的zone 中的extent(主要存放偏移地址和length),通过IOStatus Zone::Append 进行文件数据的实际写入了。

总的来说,Zenfs 通过 Best-effort 算法,根据 Rocksdb 配置的write_hint_存储 data文件和zone 接近的生命周期 来加速过期zone的回收,极大得减少了 ZNS 的空间放大问题,根据论文中的数据,说能够保持空间放大在10% 左右(可以说是整个LSM-tree + SSD 的空间放大,数据没问题的话已经很了不起了)。

当然,想要有这样的测试数据,需要对rocksdb 的参数配置进行调整,可以通过执行 Zenfs下的一个脚本来达到这个目的: ./zenfs/tests/get_good_db_bench_params_for_zenfs.sh nvme2n1 可以获取到官方推荐的一个配置,建议让 target_file_size 和 zone 配置的大小对齐。

Zenfs 也有active_zone_limits 的限制,即我们在AllocateZone 函数中可以看到,分配一个新的zone 的话如果当前active zone 的个数达到了max_nr_active_io_zones_ ,需要先关闭之前的一个zone才行,也就是在 Rocksdb 中也会有 active zone个数的限制。当然这方面 Zenfs 也做了对应的测试,发现 active zone 的个数小于6的话 会对写性能有影响, 但是达到12的话后面再增加对写性能没有太大的影响。

3 性能测试

3.1 环境搭建

1 需要有一块西数提供的 ZNS-ssd

2 内核版本大多数功能是 5.9 才支持的,建议直接升级内核到5.12

3 依赖的安装包:linux-util (blkzone) ,libzbd (负责和内核的ZBD 通过ioctl 进行交互),nvme-cli (查看/升级 ZNS 固件), Cmake, gflags

4 Zenfs 和 Rocksdb 的编译 直接参考:https://github.com/westerndigitalcorporation/zenfs/tree/master/

3.2 原始设备的性能

在测试 ZNS-ssd 和拥有 7% op空间 以及 28% op空间的 block ssd 的稳态吞吐和延时的对比,测试是对拥有相同存储介质对外暴露不同存储接口 硬件进行的。

从数据中可以得到如下结论

  1. Op 空间的增加 有助于最大写吞吐的提升 以及 平均延时的降低
  2. Block SSD 在写入吞吐不断增加时,后续因为底层的GC 无法达到target 的目标吞吐。对于op 空间越大的ssd,其target 吞吐能更接近实际 吞吐一些。
  3. ZNS-ssd 的吞吐和延时不受 GC的影响,其实际吞吐和延时的增加都会随着写入target 的增加而线性增加,而且不需要额外的OP空间,可以说是性能表现极佳了。

测试 ZNS 的 fio脚本如下:

并发写多个zone:

[global]
filename=/dev/nvme3n1
group_reporting
bs=2M
rw=randread
norandommap
randrepeat=0
max_open_zones=14
ioengine=libaio
buffered=0
direct=1
runtime=60
time_based
#io_size set to 4 zones for each job (1077 * 2)
io_size=2154m
#size set for 2 zones (2 * 2g)
size=4g
numjobs=8
iodepth=1
zonemode=zbd
[randomzonetest1]
offset=0
[randomzonetest2]
offset=4g
[randomzonetest3]
offset=8g
[randomzonetest4]
offset=12g
[randomzonetest5]
offset=16g
[randomzonetest6]
offset=20g
[randomzonetest7]
offset=24g
[randomzonetest8]
offset=28g
[randomzonetest9]
offset=32g
[randomzonetest10]
offset=36g
[randomzonetest11]
offset=40g
[randomzonetest12]
offset=44g

并发压测一个zone

 [global]
filename=/dev/nvme3n1
zonemode=zbd
max_open_zones=14
direct=1
ioengine=libaio
numjobs=8
iodepth=4
group_reporting
size=100G
time_based
runtime=60s[seqwrite]
rw=write # or write
bs=4K

3.3 应用测试性能

这里主要是对Rocksdb 的测试,整个测试是在AMD Epyc 7302P CPU 和 128G 的内存上跑的。

对比了xfs on block ssd , f2fs on block ssd , f2fs on zns, zenfs 这四种 场景。Key: 20B, value :800b,带压缩。构造数据集是先 fillrandom 3.8 billion的key,再overwrite。

大体的吞吐结果如下:

可以看到fill random 时的性能,大家差异并不大,Zenfs 相比于xfs 也就高了10% 左右。但是, 当进行overwrite 有大量compaction进行的时候,Zenfs的调度策略就很有优势了,其本省的数据文件和相同生命周期的zone一起调度删除 以及 ZNS 底层的zone的回收和compaction的结合,使得整个on Zenfs 的rocksdb 写放大在原本compaction策略不变的情况下降到了最低,写吞吐提升了一倍多。

测试了写性能,还得再看看读性能。在之前的写数据集的基础上调度了读,底层数据集大于内存5倍以上。

在数据集的基础上,分别跑 readrandom, readwhilewriting with 20M/s rate limiter, readwhilewriting with no write limits.

可以看到readrandom 场景,ZNS 读吞吐并不比其他的文件系统差,同时如果有写入且加上rate limiter,读性能也能够和其他测试保持接近。

如果不限制ratelimiter,保持读吞吐的同时 写吞吐也能够极大得提升。

再看看对应的读延时:

很明显 on ZNS 的ssd 读长尾得到了明显的缓解,因为 zenfs 以及 F2FS on ZNS 都是端到端的数据访问, 不会再走内核庞大的I/O调度了逻辑,在有写入的读场景,因为传统的block-ssd 的主控调度无法分开调度读写,所以读长尾会因为写入的增加而增加。但这一些对ZNS-ssd 来说都没有这样的问题,所以ZNS 在读上因为其本身硬件设计的特性保证了读吞吐并不受影响的同时读长尾相比于传统的block ssd 显著降低。

当然,这一些测试数据的细节还在本地环境复现中,后续会做一个补充。

不过总的来说,ZNS ssd 能够从工业界的角度解决 传统SSD 本身的内部实现上的问题(GC引起的写放大,需要消耗过多内存保存LBAs和物理地址的映射,需要OP空间进行GC),并且能够在以前解决方案的基础上进一步提升易用性,这符合当今高速发展的硬件和其特有的软件栈相结合 来发挥高性能硬件本省性能的趋势。不论是 PMEM 的硬件存储栈 还是 SPDK的软件存储栈都希望将 前人(os)的部分 on 新硬件的冗余设计抛开,从而达到降本增效的目的。

4 参考

[1. ZNS: Avoiding the Block Interface Tax for Flash-based SSDs]

[2. An Efficient Design and Implementation of LSM-Tree based Key-Value Store on Open-Channel SSD]

3. https://github.com/westerndigitalcorporation/zenfs

4. ZNS-SSD

ZNS : 解决传统SSD问题的高性能存储栈设计(fs-->io-->device)相关推荐

  1. ZNS 架构实现 : 解决传统SSD问题的高性能存储栈设计

    声明 主页:元存储的博客_CSDN博客 依公开知识及经验整理,如有误请留言. 个人辛苦整理,付费内容,禁止转载. 内容摘要 2.2 ZNS 的架构实现 先看看 支持zone 存储的 SMR HDD 以 ...

  2. [元带你学NVMe协议] ZNS 架构: 解决传统SSD问题的高性能存储栈设计

    2 ZNS 架构 2.1 ZNS storage 模型演进 先大体看一下 ZNS-ssd 的内部数据分布: ZNS-ssd 像 OC 那种 channel,将整个SSD内部的存储区域分为多个zone ...

  3. 仓库货位卡标识牌_仓库货架能解决传统仓储的哪些问题?

    仓库货架的运用不仅能够有效的解决传统仓储存在的现实问题,同时可以使得仓库的空间利用率得以大幅度的提升. 仓储货架,重型货架,横梁货架,穿梭车货架,钢平台,江苏海胜货架制造有限公司​www.hisonr ...

  4. 无纸化会议系统是什么?它如何解决传统模式的弊端

    无纸化会议系统是什么?它如何解决传统模式的弊端?所谓无纸化会议,使用基于移动互联网的无纸化会议交互系统.现代通讯技术.音频技术.视频技术.软件技术,通过文件的电子交换实现会议的无纸化. 大家都知道,传 ...

  5. 案例 | 光大银行如何解决传统监控痛点,打造一体化监控平台?

    "光大银行为了解决传统监控管理的痛点,从监控平台的建设和全站监控能力,大屏可视化展现和智能监控分析这四点出发,打造了新一代的一体化的统一监控管理平台."--马文杰, 中国光大银行总 ...

  6. 武汉新时标文化传媒有限公司解决传统货柜式电商哪些痛点

    传统电商初步解决了零售电商履约效率的问题,由此发展出的内容电商成为降低消费者决策心智成本的重要抓手,从图文导购.视频电商到直播电商,内容电商形态层出不穷,对内容和电商生态的商业价值影响日益显著. 哪些 ...

  7. 图解高性能服务器开发两种模式,第四章 NETTY高性能架构设计

    目录 一.NIO存在问题以及Netty的优点 二.线程模型基本介绍 三.工作原理图(传统同步阻塞式IO) 四.Reactor模式 五.单Reactor单线程 六.单Reactor多线程 七.主从REA ...

  8. Netty 高性能架构设计

    Netty 高性能架构设计 Netty 概述 原生 NIO 存在的问题 Netty 官网说明 Netty 的优点 Netty 版本说明 线程模型基本介绍 传统阻塞 I/O 服务模型 Reactor 模 ...

  9. 【转载】专访阿里陶辉:大规模分布式系统、高性能服务器设计经验分享

    关注陶辉很长时间,初次对陶辉的了解还是在我们CSDN的博客上,从2007年开始写博客,一直到现在,如果不是对技术的追求和热爱,以及热爱分享的精神,我想不是很多人能坚持下来,拥有多年大型互联网公司的从业 ...

  10. 专访阿里陶辉:大规模分布式系统、高性能服务器设计经验分享

    专访阿里陶辉:大规模分布式系统.高性能服务器设计经验分享 发表于2014-06-27 16:25|18197次阅读| 来源CSDN|55 条评论| 作者魏伟 云计算Nginx开源 摘要:先后就职于在国 ...

最新文章

  1. golang-实现自己的事件驱动
  2. MPB:基于BIOLOG的微生物群落功能分析
  3. Visual Studio找不到adb.exe错误解决
  4. new Grammar in 740 - Internal table group by
  5. codevs 2185 最长公共上升子序列--nm的一维求法
  6. 06_jQuery_内容过滤
  7. leetcode题库1--两数之和
  8. psenet的eval_ctw1500.py解析
  9. 如何创建git账户的chroot
  10. 「解决方案」高速公路综合解决方案
  11. wince 百度地图懒人包_百度地图winCE版 V10.9.2 安卓版
  12. SAP SRS 门店WEB系统激活
  13. Charles华为手机保姆级安装过程
  14. python爬取凤凰新闻网_爬取网易,搜狐,凤凰和澎湃网站评论数据,正负面情感分析...
  15. 解决微信小程序“app.json: [“workers“] 字段需为 目录“错误及worker的使用
  16. R语言C指数,如何在R软件中求一致性指数( Harrell concordance index:C-index)?
  17. SEO、UEO未来谁更重要?
  18. Android无法播放本地视频
  19. Java 微信公众号每日自动给女朋友推送问候
  20. JQ+ ES6模板字符串 + $.each(数组, function(index, 数组中的对象) {操作程序} 循环遍历添加新html结构标签

热门文章

  1. 用VC GDI+画一颗树
  2. 51单片机延时函数不起作用
  3. 中职一年级计算机学情分析,一年级学情分析.doc
  4. android编程_5个在Android平台上进行编程的最佳应用
  5. 二重积分的计算 —— 交换积分顺序(exchange the order of integration)
  6. Photoshop抠头发丝超简单方法 PS抠头发
  7. 扫码点餐系统可以免费使用了
  8. 【网络安全必看】如何提升自身WEB渗透能力?
  9. 金华职业技术学院计算机网络技术考试,金华职业技术学院2016年提前招生计算机应用技术专业测评方案...
  10. 计算机硕士毕业论文范文,计算机论文:精选计算机硕士毕业论文范文十篇.docx...