NVMe Multi-stream

1. 原理介绍

1.1. flash 写操作

在介绍 multi-stream 之前,首先简单介绍一下 flash 写操作的特性。

  • SSD 中写操作(write)的单元为 page,page 的大小通常为 2 ~ 64 KB
  • NAND flash 在对 page 写操作之前,必须对 page 执行擦除操作(erase),但是擦除操作的单元为 block,一个 block 通常包含 32 ~ 256 个 page


由于擦除操作相对耗时,因而在对某个 page 进行修改操作时,通常将修改的数据直接写入一个新的已经擦除过的 page,而将原来的旧的 page 设置为 invalid 状态,此时该 page 的修改操作就算完成了,之后 SSD FTL (Flash Translation Layer) 会执行垃圾回收(garbage collection)算法,回收处于 invalid 状态的 page。

FTL 需要对 invalid page 执行擦除操作以回收这些 invalid page,而擦除操作的单位为 block,一个 block 中包含多个 page,其中既包含 invalid page,同时也包含 valid page。因而当 FTL 需要回收 block A 中的 invalid page 时,就必须先将 block A 中的 valid page 先拷贝到其他新的 block 例如 block B 中,并将 block A 中的 valid page 设置为 invalid 状态,此时 block A 中的所有 page 均为 invalid 状态,FTL 可以安全地对 block A 执行擦除操作。


在以上操作过程中,FTL 需要对回收的 block 中的 valid page 进行额外的拷贝操作,从而使得设备实际执行的IO数量大于用户提交的IO数量,这一特性称为 写放大(Write Amplification)。

SSD 使用 WAF (Write Amplification Factor) 参数描述这一特性,该参数的值为

WAF = Amount of writes committed to flash / Amount of writes that arrived from the host

由于 FTL 需要对 valid page 进行额外的拷贝操作,WAF 参数的值通常大于1。(当设备支持 compression 特性时,WAF 参数的值是有可能小于1的。)

WAF 参数会影响 SSD 的使用寿命以及性能:

  • WAF 参数会影响 SSD 的寿命,WAF 的值越大,SSD 的实际有效寿命越短;
  • WAF 参数还会影响 SSD 读写操作的性能,垃圾回收操作通常在后台运行,因而当负载较轻时,垃圾回收操作一般不会对读写操作造成影响;然而当负载较重时,写操作需要等待垃圾回收算法释放free page,从而降低写性能;同时读操作会与垃圾回收操作并行运行而竞争资源,从而增大读延时。

1.2. multi-stream 原理

以上问题的根源在于两种不同生命周期(lifetime)的数据存储在同一个block中。

假设当前存在两种生命周期的数据,hot data 与 cold data,其中 hot data 相对 cold data 会更为频繁地进行更新。


在传统的 SSD FTL 实现中,操作系统向 SSD 提交写操作请求后,FTL 会将操作系统提交的数据依次写入可用的 block 中,例如上图中操作系统提交写入H1、C1、H2、C2、C3、H3、H4、C4时,FTL 将这些数据依次写到 block 1、block 2 中(假设一个 block 包含 4 个 page)。

之后操作系统提交对H1、H2、H3、H4进行修改时,必须将修改后的数据写到 block 3 中,并将 block 1 中的 H1、H2 page,block 2 中的 H3、H4 page 设置为 invalid,之后当 garbage collection 需要回收 block 1、block 2 时,就必须对其中的 C1、C2、C3、C4 page 执行额外的拷贝。


multi-stream 特性则是将不同更新频率的数据写到不同的 block 中,从而尽可能地减小 garbage collection 中引入的额外的数据拷贝操作,从而提高 SSD 的有效生命周期,并提升写性能。

当SSD FTL 支持 multi-stream 特性时,操作系统可以将写入的数据与某个 stream 相绑定,例如将 hot data 与 stream x 绑定,将 cold data 与 stream y 绑定,此时 FTL 在受理操作系统提交的写请求时,将不同 stream 的数据分别写到不同的 block,例如将 stream x (hot data)全部写到 block 1,将 stream y(cold data)全部写到 block 2。

之后操作系统提交对 H1、H2、H3、H4 进行修改时,FTL 将修改后的数据写到 block 3 中,此时 block 1 中不包含任何 valid page,之后 garbage collection 就可以直接对 block 1 执行擦除操作,而不会带来额外的拷贝操作。

1.3. multi-stream 实现

multi-stream 的本质是针对 flash 特殊的写特性,提供一种机制,使得不同更新频率的数据写到不同的 block 中。multi-stream 机制中使用 stream 抽象不同更新频率的数据,并使用 stream id 标识不同的 stream。

SSD 设备本身掌握的信息很少,其对数据的更新频率基本没有概念,只有数据的生产者即上层软件(包括用户程序、操作系统)才了解数据的更新频率,因而上层软件负责写入的数据与 stream id 的映射,接口协议负责将数据与 stream id 传递给 SSD 设备,而 SSD FTL 只负责将不同 stream id 的数据写入不同的 block。

以下依次介绍 SSD 接口协议、SSD 设备驱动、文件系统、应用程序如何适配 multi-stream 机制。

1.3.1. 接口协议

SSD 设备支持多种接口协议,目前 SCSI(T10 (SCSI) standard)与 NVMe 1.3 已经正式支持 multi-stream 特性。

下面以 NVMe 1.3 为例介绍其对 multi-stream 特性的支持。


SSD 的 FTL 负责操作系统使用的 LBA (Logical Block Address) 与 SSD 内部使用的 physical address 之间的映射。

传统的不支持 multi-stream 的 SSD FTL 通常只维护一个 log structure,它只会根据操作系统提交的 IO 请求的先后顺序,将这些 IO 请求依次存储到可用的存储空间中。

而支持 multi-stream 的 SSD FTL 则会维护多个 log structure,其中为每个 stream 维护一个单独的 log structure,FTL 以 Stream Granularity Size (SGS) 为单位分配存储空间,即 FTL 一开始会为每个 stream 预先分配 SGS 大小的存储块,之后该 stream 的数据都会存储到这一预分配的 SGS 大小的存储块中。当这一存储块的空间用尽时,FTL 则再次分配一个 SGS 大小的存储块。stream 的 log structure 会维护该 stream 分配的所有存储块,而正是所有的这些 SGS 大小的存储块构成了一个 stream。


NVMe 1.3 标准中以 directive 的形式支持 multi-stream,directive 机制用于实现 SSD 设备与上层软件之间的信息沟通,stream 只是 directive 的一个子集,目前 directive 也只实现 stream 这一个子集。

其中与 stream 相关的命令有

  1. 上层软件可以向 SSD 设备发送 identify 命令,设备在接收到该命令之后在回复的 identify data structure 中描述该设备支持的相关特性,其中的 OACS (Optional Admin Command Support) filed 的 bit 5 描述该设备是否支持 directive 特性;即当 SSD 设备支持 directive 特性时,其在接收到 identify command 时,在返回的 identify data structure 中,必须设置 OACS 的 bit 5,以表明该设备支持 directive 特性。

  2. 对于支持 directive 特性的 SSD 设备,用户必须显式调用 directive send command 命令执行 enable stream 操作,以开启 multi-stream 特性。

  3. 之后在 write command 的 Directive Type 字段设置为 Streams (01h),同时 Directive Specific 字段设置为对应的 stream id,即可以将该 write command 写入的数据与该 stream id 描述的 stream 相绑定。

1.3.2. 应用程序

应用程序是数据的主要生产者,因而我们自然会想到在应用层实现写入数据与 stream id 的映射。

例如 Linux 4.13.6 中已经实现 fcntl() 对 multi-stream 特性的支持,用户程序可以通过 fcntl() 系统调用将特定 inode / file 与特定的 stream id 进行绑定,这样不同文件的数据,或者同一文件但不同进程生产的数据就会在 SSD 中分开存储。

write stream patch for Linux 4.13.6

1.3.3. 文件系统

在应用层实现写入数据与 stream id 的绑定,可以最为准确地描述写入数据的特性,然而这一实现需要显式地修改应用程序的代码。

除了应用程序之外,文件系统也是数据的生产者,文件系统需要维护各种元数据(例如文件的 inode 信息等),此外日志型文件系统还需要写入日志数据,因而文件系统可以将不同类型的元数据与日志数据与 stream id 进行绑定,从而实现这些数据的分开存储。

例如 Samsung 提出的 Fstream based Ext4 文件系统中, 为 journal、inode、directory、inode/block bitmap and group descriptor 等元数据分别绑定不同的 stream id

此外还支持根据文件的名称或后缀,将文件数据绑定不同的stream id,实现垂直优化。

FStream: Managing Flash Streams in the File System

1.3.4. 设备驱动

SSD 设备驱动也可以实现写入数据与 stream id 的绑定,例如现有实现 AutoStream 中通过统计最近一段时间内各个 LBA 的更新次数等数据,推测各个 LBA 中存储的数据的更新频率,从而实现 hot data 与 cold data 的分开存储。

AutoStream: Automatic Stream Management for Multi-streamed SSDs

2. Fstream 原型实现

Fstream 是三星提出的一个 multistream based Ext4,其在 Ext4 文件系统的基础上,在文件系统这一层将各种元数据与文件数据映射到不同的 stream id,从而使这些数据在 SSD 上分开存储。

由于相关代码未开源,因而本文作者实现了一个简单的原型。目前 Linux mainline (从 4.13.6 开始) 已经支持 multi-stream 特性,因而我们在 mainline 4.13.6 内核版本的基础上实现这一原型。

2.1. Ext 4 元数据

Ext 4 文件系统中的元数据包括

  • superblock
  • group descriptors
  • inode bitmap
  • data block bitmap
  • inode table
  • directory
  • journal


将这些元数据的更新频率分为 4 个级别,即

stream data
journal stream journal
inode stream inode table
directory stream directory
misc stream superblock, group descriptors, inode/block bitmap

2.2. write hint

/*
 * Write life time hint values.
 */
enum rw_hint {
        WRITE_LIFE_NOT_SET      = 0, 
        WRITE_LIFE_NONE         = RWH_WRITE_LIFE_NONE,
        WRITE_LIFE_SHORT        = RWH_WRITE_LIFE_SHORT,
        WRITE_LIFE_MEDIUM       = RWH_WRITE_LIFE_MEDIUM,
        WRITE_LIFE_LONG         = RWH_WRITE_LIFE_LONG,
        WRITE_LIFE_EXTREME      = RWH_WRITE_LIFE_EXTREME,
};

使用 write hint 描述数据的更新频率,即按照数据更新的相对频率,将数据的更新频率分为 4 个级别:short、medium、long与extrem。

当SSD设备支持的 stream 的数量达到或超过 4 时

  • short 对应 stream id 1
  • medium 对应 stream id 2
  • long 对应 stream id 3
  • extrem 对应 stream id 4

当用户未显式指定数据的更新频率时,其最终默认使用 stream id 0

2.3. write hint of file data

用户程序可以通过 fcntl() 设置特定文件的 write hint

struct inode {
+       enum rw_hint        i_write_hint;

inode 中增加 i_write_hint 字段,用户程序调用 fcntl() 时,即将用户设置的 write hint 保存在文件对应的 inode 的 i_write_hint 字段

2.4. write hint of metadata

文件系统使用 block buffer 作为接口向generic block layer提交元数据以及用户数据的IO请求,因而在 struct buffer_head 中增加一个字段描述该 block buffer 缓存的数据对应的 write hint

struct buffer_head {
+       enum rw_hint b_write_hint;
};


该字段的初始值为 WRITE_LIFE_NOT_SET,即默认使用 stream id 0

struct buffer_head *alloc_buffer_head(gfp_t gfp_flags)
        struct buffer_head *ret = kmem_cache_zalloc(bh_cachep, gfp_flags);
        if (ret) {
+               ret->b_write_hint = WRITE_LIFE_NOT_SET;


在将磁盘上存储的元数据拷贝到内存中时,设置对应的 buffer head 的 b_write_hint 字段,将 EXT 4 文件系统的各种元数据分别映射到之前描述的 4 个级别

data write hint of buffer head stream id
user data WRITE_LIFE_NOT_SET 0
journal JBD2_WRITE_LIFE 1
inode table EXT4_WRITE_LIFE_INODE 2
superblock EXT4_WRITE_LIFE_MISC 3
group descriptors EXT4_WRITE_LIFE_MISC 3
inode bitmap EXT4_WRITE_LIFE_MISC 3
block bitmap EXT4_WRITE_LIFE_MISC 3
directory EXT4_WRITE_LIFE_DIR 4
#define JBD2_WRITE_LIFE         WRITE_LIFE_SHORT        /* stream for journaling */
enum ext4_write_hint {
    EXT4_WRITE_LIFE_INODE       = WRITE_LIFE_MEDIUM,    /* stream for inode table */
    EXT4_WRITE_LIFE_MISC        = WRITE_LIFE_LONG,      /* stream for superblock, inode bitmap, block bitmap and group descriptors */
    EXT4_WRITE_LIFE_DIR         = WRITE_LIFE_EXTREME,   /* stream for directory */
};

2.5. pass write hint to generic block layer

Ext 4 文件系统会调用 block_write_full_page() 将 file data 写到SSD,此时 file data 对应的 page cache 的 buffer head 的 write hint 为默认的 WRITE_LIFE_NOT_SET,因而对于 file data 实际使用 inode 中存储的 write hint

用户程序可以通过 fcntl() 设置特定文件的 write hint,当用户未显式设置时,file data 实际使用 stream id 0

int __block_write_full_page(struct inode *inode, struct page *page,
            get_block_t *get_block, struct writeback_control *wbc,
            bh_end_io_t *handler)
{
+   enum rw_hint write_hint = bh->b_write_hint == WRITE_LIFE_NOT_SET ? inode->i_write_hint : bh->b_write_hint;      

+   submit_bh_wbc(REQ_OP_WRITE, write_flags, bh, write_hint, wbc);

Ext 4 文件系统会调用 submit_bh()/block_write_full_page() 将 metadata 回写到 SSD 中,此时使用的 write hint 即为设置的各个 metadata 对应的write hint

 int submit_bh(int op, int op_flags, struct buffer_head *bh)
 {
+       return submit_bh_wbc(op, op_flags, bh, bh->b_write_hint, NULL);
 }

最终将 write hint 保存到 bio 中,即将 write hint 传递给 generic block layer

static int submit_bh_wbc(int op, int op_flags, struct buffer_head *bh, 
                         enum rw_hint write_hint, struct writeback_control *wbc)
{
        bio->bi_write_hint = write_hint;
        ...
        submit_bio(bio);
        return 0;

2.6. write hint in generic block layer

generic block layer 中对 multi-stream 的适配有

  • 只有同一个 stream 内的 bio 之间才可以合并
  • 将 bio 的 write hint 传递给 request
void blk_init_request_from_bio(struct request *req, struct bio *bio)
{
    req->write_hint = bio->bi_write_hint;
}

2.7. write hint in NVMe layer

最终 write hint 传递到 NVMe layer,并保存在 request 的 write_hint 字段

此时调用 nvme_assign_write_stream()将 write hint 映射为对应的 stream id,并将 stream id 保存在 write command 的 directive specific 字段,之后将该 write command 交给SSD设备处理,之后的处理均由 SSD 设备上的 FTL 实现。

static inline blk_status_t nvme_setup_rw(struct nvme_ns *ns, 
                struct request *req, struct nvme_command *cmnd)
{
        if (req_op(req) == REQ_OP_WRITE && ctrl->nr_streams)
                nvme_assign_write_stream(ctrl, req, &control, &dsmgmt);
        ...
        cmnd->rw.control = cpu_to_le16(control);
        cmnd->rw.dsmgmt = cpu_to_le32(dsmgmt);
        return 0;
}

/*
 * Check if 'req' has a write hint associated with it. If it does, assign
 * a valid namespace stream to the write.
 */
static void nvme_assign_write_stream(struct nvme_ctrl *ctrl,
                                     struct request *req, u16 *control,
                                     u32 *dsmgmt)
{
        enum rw_hint streamid = req->write_hint;

        if (streamid == WRITE_LIFE_NOT_SET || streamid == WRITE_LIFE_NONE)
                streamid = 0;
        else {
                streamid--;
                if (WARN_ON_ONCE(streamid > ctrl->nr_streams))
                        return;

                *control |= NVME_RW_DTYPE_STREAMS;
                *dsmgmt |= streamid << 16;
        }

        if (streamid < ARRAY_SIZE(req->q->write_hints))
                req->q->write_hints[streamid] += blk_rq_bytes(req) >> 9;
}

3. Samsung PM963 multi stream test

3.1. prepare

三星在内部测试中使用 PM963 480GB SSD 进行测试

SSD: Samsung PM963 480GB, with the allocation granularity 1 of 1.1GB from “FStream: Managing Flash Streams in the File System”

我们自己的测试环境中使用的 SSD 为 PM963 1920GB (MZQLW1T9HMJP-00003)。

三星在网上公布的资料均显示 PM963 支持 multi-stream 特性,然而 NVMe 从 1.3 标准开始添加对 multi-stream 的支持,而 PM963 只支持 1.2 标准。

同时测试发现我们使用的 PM963 不支持 NVMe 1.3 中规定的 directive 相关的命令

  1. 其 OACS 字段的 bit 5 为 0,即不支持 directive 特性。
  2. 不支持 directive send 命令,在手动发送该命令时,返回结果显示设备不支持该命令。

尽管如此,我们考虑到 PM963 在 NVMe 1.3 之前发布,因而可能使用非标准的方法实现 multi-stream 特性,我们还是希望使用 fio 对 multi-stream 特性开启前后,设备的读写性能进行一次测试。

3.2. fio测试

测试环境

  1. AliOS Linux 4.9.93 with multi-stream enabled
  2. Samsung PM963 1920GB (MZQLW1T9HMJP-00003)
  3. fio-2.18

3.2.1. prepare

在每次测试前

  1. 执行 nvme --format /dev/nvme8n1 -s 1,对全盘执行擦除操作
  2. 执行 mkfs.ext4 .dev.nvme8n1,文件系统格式化
  3. 使用 fio 创建一个 1500GB 的文件,使SSD设备的占用率达到 90%,这样可以加重之后的测试中垃圾回收算法的负载

3.2.2. fio IO test

  • 6 randread thread, block size 4KB, aio depth 64
  • 4 sequential write thread, block size 128KB, aio depth 1
  • run 2 hours

其中 4 个写线程具有不同的数据更新频率,分别为 1x, 2x, 3x, 10x。(我们使用 fio 的 thinktime 参数来实现不同的数据更新频率。)

测试分为两次进行

  1. 第一次测试中不使用 multi-stream 特性
  2. 第二次测试中开启 multi-stream 特性,使用 fio 的 fadvise_stream 参数为四个写线程绑定不同的 stream ID,其中
write thread data lifetime stream ID
1 10x 1
2 3x 2
3 2x 3
4 1x 4

注意 该测试中我们没有对我们实现的 Fstream 原型进行测试,而是直接使用 mainline 4.13.6,在用户层通过 fcntl() syscall 实现不同更新频率的用户数据与stream ID的映射。即该测试中我们只对用户进程产生的文件数据分流存储,而没有对文件系统的元数据进行分流存储。

3.2.3. fio 测试结果

test write throughput read latency read average latency
multi-stream disabled 617 MB/s 82.9 K 772 us
multi-stream enabled 616 MB/s 82.8 K 773 us

test_result

上图描述两次测试过程中,设备的实时 write throughput,其中

  • 在一开始设备的写性能达到峰值,这是因为设备经过全盘擦除后,free page数量充足,该现象符合预期
  • 之后设备的写性能出现断崖式的下降,我们分析这是因为随着写操作的进行,设备的free page数量不断减小,当下降到某一阈值时用户进程的写操作必须等待垃圾回收算法释放free page,此时设备的写性能出现下降
  • 之后设备的写性能一直处于相对稳定的状态

从以上现象我们可以确保测试过程中垃圾回收算法一直处于相对较重的负载当中。

从以上图表我们可以发现,在 multi-stream 特性开启前后,设备的读写性能并没有出现明显变化。

3.3. 测试结果分析

目前使用 NVMe 1.3 标准的方法不能开启 PM963 的 multi-stream 特性,即测试环境中使用的 PM963 SSD 不支持 directive 相关的命令,同时在 write command 中“强行”添加 stream id 时,写性能也没有明显的提升效果。

之后我们咨询熟悉硬件的同学,了解到当前公司使用的 PM963 均不支持 multi-stream 特性,而支持该特性的 SSD 要等到明年才能上线。

此外当前测试环境也没有 SAS SSD 设备(SCSI 也支持 multi-stream 特性),因而当前尚不能对 multi-stream 特性的实际效果进行测试。

4. 总结与讨论

4.1. multi-stream 适用场景分析

multi-stream 的原理是尽可能将相同更新频率的数据存储到同一个 block,从而在回收 block 时减小数据的拷贝操作,但是该机制的性能提升效果会受以下因素的影响。

4.1.1. 适用于高负载的应用场景

这里的高负载具有两重含义,即

  1. SSD 的空间占用率达到一定程度
  2. SSD 的写负载较重

multi-stream 只有在 SSD 的空间占用率达到一定程度时才会体现性能提升的效果。当使用一块全新的 SSD 时,每次写操作时总是有充足的 free block 可写,因而就不会运行 garbage collection, multi-stream 也就不会发挥作用。

在 SSD 的空间占用率达到一定程度的基础上,当同时写负载较重时,multi-stream 才能提升性能。这是因为 multi-stream 主要提升 garbage collection 算法的性能,而该算法通常在后台运行;当写负载较轻时,garbage collection 的需求较小,此时带来的性能提升效果不明显;只有当设备可用空间较小,同时写负载较重时,garbage collection 算法的负载才会较重,此时 multi-stream 才会带来明显的性能提升效果。

但值得注意的是,以上描述的是只有在写负载较重时才可以体现 multi-stream 对写操作的性能提升,而无论写负载的高低,multi-stream 机制都可以降低 WAF 参数,从而提高 SSD 的有效生命周期。

4.1.2. 更适用于小块数据的随机写操作

由于 multi-stream 的原理,其更加适用于数据库这类小块数据的随机写入操作,因为小块数据更容易与其他不同类型的数据存储在同一个 block 中,同时其随机写操作也更容易使同一个 block 同时包含 valid page 与 invalid page,因而也就给 multi-stream 更多的提升性能的空间。

例如上图为三星发表的论文中 Fstream 的性能测试结果,其中 Fileserver 测试程序主要执行大块数据的顺序写操作,Cassandra 测试程序主要执行小块数据的随机写操作,测试结果显示,multi-stream 机制对于小块数据的写操作具有更为明显的性能提升。

4.2. stream id 分配机制

stream id 的分配机制会直接影响 multi-stream 的效果,当同属于一个 stream 的所有数据具有完全相同的更新频率时,multi-stream 能带来最大的性能提升,即上层程序需要准确地判断数据的更新频率,并依此为其分配合适的stream id。

4.2.1. exclusive stream

一种最简单同时最有效的方案就是为每一类数据都分配一个单独的 stream id,即这一类数据会独占一个 stream,此时该 stream 的 block 中只存储一类数据,那么自然可以将该 stream 在垃圾回收时带来的额外的数据拷贝操作降到最低,从而具有最好的性能效果。

这种方案同时也是一种理想的方案,现实中 SSD 设备支持的 stream 的个数是有限的,因而不可能为每一类数据都分配一个单独的 stream,因而当将两种数据分配到同一个 stream 中时,这两种数据的更新频率势必存在差异,因而无法达到最为理想的性能提升效果。

需要注意的是,三星的论文中描述的性能测试,即是为每个更新频率的数据分配单独的 stream,因而论文中描述的性能提升是理想环境下的最大性能提升效果。

4.2.2. shared stream

既然 stream 的数量是有限的,那么就必须提供某种机制,按照数据的更新频率,将数据绑定到合适的 stream 上。

当前 Linux mainline 中提供的机制是,抽象 short、medium、long、extrem 这四个级别描述数据的更新频率,该抽象只是描述数据更新频率的相对值,而非绝对值。

之后 SSD 设备驱动需要将这四个级别映射到对应的 stream id,例如 short 映射到 stream 1,extrem 映射到 stream 4,依此类推。


该机制可能存在的问题有

  1. 该机制最多只使用 4 个 stream,当设备支持的 stream 个数超过 4 个时,stream 资源不能有效利用,及其 scalability 不好。
  2. 该机制中开发者必须依靠自己对数据更新频率的理解,将数据绑定到不同的 stream 上,然而不同开发者对数据更新频率的理解是不同的,例如同时映射为 short 的两类数据,其更新频率可能存在很大的差异。

4.2.3. stream of user data

目前存在两种途径为 user file data 绑定 stream

  1. 应用程序调用 fcntl() 手动设置用户数据的 write hint,由于应用程序自身对数据更新频率具有最为准确的理解,因而这种方法设置用户数据的 write hint,最为准确,同时也最为灵活,但是需要显式地修改应用程序的代码。此外由于应用程序的开发者与内核开发者对数据更新频率的理解不同,同为 short write hint 的 file data 与 metadata 其实际的更新频率可能存在较大的差异,从而影响 multi-stream 的性能。
  2. 取消用户态设置 write hint 的接口,由文件系统依据文件的名称、后缀信息推测文件数据的更新频率,从而自动为文件数据绑定对应的 stream,该方法不需要应用程序显式地修改代码,但是由于文件系统掌握的文件的相关信息是非常有限的,通过文件名称推测文件数据更新频率的方法,其准确性与灵活性都存在很大的限制。

因而上层实现需要根据不同的应用场景,选择上述的其中一种方法,或是将这两者相结合。

4.2.4. seperated stream of user/kernel

由于应用程序的开发者与内核开发者对数据更新频率的理解可能存在不同,同为 short write hint 的 file data 与 metadata 其实际的更新频率可能存在较大的差异,从而影响 multi-stream 的性能。

因而另一种方案是,为内核与用户态程序提供相隔离的 stream id,例如

  • 仍然使用数据的更新频率的相对值作为 write hint,例如将 file data 与 metadata 的更新频率分为 short、medium、long、extrem 多个级别
  • 当 SSD 设备一共支持 N 个 stream 时
  • 内核预留其中的 M 个 stream id,所有内核态提交的数据(包括 Fstream 的 metadata),按照其 write hint,映射为这 M 个 stream
  • 用户态使用剩余的 (N - M) 个 stream id,用户态使用 fcntl()/fadvise() 接口设置文件的 write hint,之后文件系统提交该文件写入的数据时,将该文件映射到这 (N - M) 个 stream

该方案可以保证 file data 与 metadata 在 SSD 上绝对是分开存储的。

5. 后记

NVMe SSD 设备无疑将成为未来服务器的主流存储。SSD 设备中内置的 FTL 程序使得 SSD 设备兼容为传统的 HDD 设备设计的上层软件栈,从而在早期为 SSD 设备快速打开市场。然而随着 flash 存储技术以及接口协议(例如 NVMe)的不断发展,SSD 设备的硬件性能不断提升,OS与FTL之间的割裂感反而成为了性能的瓶颈。

例如本文提到的不同更新频率的数据存储这一问题,数据的生产者是上层的软件栈,包括用户程序与操作系统,而负责数据存储的则是SSD设备中内置的FTL。由于目前的实现中上层软件栈与FTL之间的沟通非常有限,因而造成了以下的局面,即上层软件栈对数据的特性最为了解,但是无法控制数据存储在哪个physical block上,而FTL根本不知道数据的特性,因而也就不能对数据进行差异化的存储。也就是说IO stack当中本应该通力协作的两部分,如今越来越显现出两者相割裂所带来的性能瓶颈。

而在近些年我们也看到社区所作出的努力。为了解决这一问题,就必须加强OS与FTL之间的沟通。本文提到的 multi-stream 是一个思路,即通过接口协议加强OS与FTL之间的信息交互。此外 Open-Channel 架构则是另外一个思路,其干脆将FTL的部分功能上移到OS中,从而使OS与FTL的联系更为紧密。两者的实现不同,但解决的都是同一个问题,即减小OS与FTL之间的割裂感,从而进一步提升SSD设备的性能。

6. 附录

1. NVMe 1.3

2. FStream: Managing Flash Streams in the File System

3. AutoStream: Automatic Stream Management for Multi-streamed SSDs

4. write stream patch

multi-stream 特性由 Jens Axboe 于 2017 年添加到 Linux 4.13.6,适配内容主要包括

  • 令 SCSI 与 NVMe 驱动支持 multi-stream 特性
  • 对应用层提供 fcntl() 接口,实现文件数据与 stream 的映射
  • 令文件系统、通用块设备层适配 multi-stream 特性,即将 fcntl() 中绑定的 stream id 传递到 SCSI / NVMe 驱动