​

 

简介

​存储性能开发套件 (SPDK)​​ 是 ​​GitHub​​ 托管的一套开源工具和库,可帮助开发人员创建高性能和可扩展存储应用。 本教程将重点介绍 SPDK 提供的用户空间 NVMe 驱动程序,并展示如何在英特尔® 架构平台上运行 Hello World 示例 

软硬件配置


CPU 和芯片组



英特尔® 至强® 处理器 E5-2697 v2 @ 2.7 GHz

  • 每路的物理内核数量: 12(24 个逻辑内核)
  • 插座数量: 2
  • 芯片组: 英特尔® C610(C1 步进)
  • 系统总线: 9.6 GT/秒 QPI


          内存



          内存大小: 8 GB (8X8 GB) DDR3 1866

          品牌/型号: Samsung – M393B1G73BH0*



          存储



          ​英特尔® 固态盘 DC P3700 系列​



          操作系统



          CentOS* 7.2.1511,包含内核 3.10.0
           


          为何需要使用用户空间 NVMe 驱动程序?

          一直以来,存储设备的速度都远低于计算机系统的其他组件,比如 RAM 和 CPU。 这意味着操作系统和 CPU 需借助中断才能与磁盘进行交互:

          1. 向操作系统提出从磁盘读取数据的请求。
          2. 驱动程序处理请求,并与硬件通信。
          3. 磁盘片开始运转。
          4. 针在磁盘片内移动,开始读取数据。
          5. 数据被读取并拷贝至缓冲区。
          6. 生成中断,通知 CPU 数据已就绪。
          7. 最后从缓冲区读取数据。

          这种中断模式会产生开销;但它的延迟一直远低于基于磁盘的存储设备,因此不失为一种有效方式。 随着固态盘 (SSD) 等全新存储设备以及 ​​3D XPoint™​​ 存储等下一代存储技术的推出,使存储速度远高于磁盘,并将瓶颈从硬件(比如磁盘)移回至了软件(比如中断 + 内核),如图 1 所示:

          【转】SPDK 助力加速 NVMe 硬盘_驱动程序

          图 1.固态盘 (SSD) 和 3D XPoint™ 存储的速度远高于磁盘。 硬件不再存在瓶颈。

          读写数据时,用户空间 NVMe 驱动程序轮询存储设备,因此不再需要使用中断。 此外更重要的是,NVMe 驱动程序在用户空间中运行,这意味着应用能够直接与 NVMe 设备交互,无需通过内核。 它会产生开销,因为在与内核交互的过程中需要保存和恢复状态。  NVMe 采用无锁设计,不必使用 CPU 周期同步线程之间的数据,而且这种无锁方法还支持并行 IO 命令执行。

          相比于使用 Linux 内核的方法,SPDK 用户空间 NVMe 驱动程序可将总体延迟降低 10 倍:

          【转】SPDK 助力加速 NVMe 硬盘_命名空间_02

          SPDK 能够使 8 块 NVMe 固态盘达到饱和,从而通过单个 CPU 内核提供超过 350 万次 IOPs:

          【转】SPDK 助力加速 NVMe 硬盘_驱动程序_03

          前提条件和构建 SPDK

          SPDK 支持 Fedora*、CentOS*、Ubuntu*、Debian* 和 FreeBSD*。 如欲获取完整的先决软件包列表,请访问​​此处​​。

          构建 SPDK 之前,必须首先安装​​数据平面开发套件 (DPDK)​​,因为 SPDK 依赖 DPDK 中的内存管理和队列功能。 DPDK 是比较成熟的库,常用于网络数据包处理,而且经过高度优化,能够以较低的延迟管理内存和队列数据。

          使用以下命令从 GitHub 中克隆 SPDK 源代码:

          ​git clone https:​​​​/​​​​/​​​​github.com​​​​/​​​​spdk​​​​/​​​​spdk.git​

          构建 DPDK(面向 Linux*):

          ​1​

          ​cd /path/to/build/spdk​

          ​2​

           

          ​3​

          ​wget http://fast.dpdk.org/rel/dpdk-16.07.tar.xz​

          ​4​

           

          ​5​

          ​tar xf dpdk-16.07.tar.xz​

          ​6​

           

          ​7​

          ​cd dpdk-16.07 && make install T=x86_64-native-linuxapp-gcc DESTDIR=.​

          构建 SPDK(面向 Linux*):

          在 SPDK 文件夹中构建 DPDK 后,需要将目录恢复成 SPDK,并将 DPDK 位置传输至 make,以构建 SPDK:

          ​1​

          ​cd /path/to/build/spdk​

          ​2​

           

          ​3​

          ​make DPDK_DIR=./dpdk-16.07/x86_64-native-linuxapp-gcc​

          运行 SPDK 应用之前设置系统

          以下命令可设置 hugepage 并解除内核驱动程序对 NVMe 和 I/OAT 设备的绑定:

          ​sudo scripts/setup.sh​

          使用 hugepage 对性能至关重要,因为相比于默认的 4KiB 页面大小,它们的大小只有 2MiB,并能降低转换后备缓冲区 (TLB) 缺失的几率。 TLB 是 CPU 的一个组件,负责将虚拟地址转换成物理内存地址,因此使用较大的页面 (hugepage) 有助于高效使用 TLB。

          借助‘Hello World’开始使用 SPDK

          SPDK 包含许多​​示例​​和实用​​文档​​。可帮助您快速入门。 接下来我们通过示例介绍如何将‘Hello World’保存至 NVMe 设备,然后将其读取至缓冲区。

          跳至代码之前还有一点值得注意,即如何组织 NVMe 设备结构,并提供高级示例解释如何利用 NVMe 驱动程序检测 NVMe 设备并读写数据。

          组织 NVMe 设备(亦称作 NVMe 控制器)结构时需要考虑以下几点:

          • 系统可以有一台或多台 NVMe 设备。
          • 每台 NVMe 设备包含多个命名空间(可以仅为一个)。
          • 每个命名空间包含多个逻辑块地址 (LBA)。

          本示例将经历以下几个步骤:

          设置

          1. 初始化 DPDK 环境抽象层 (EAL)。 -c 为支持内核运行的位掩码,-n 为面向 master 的内核 ID,--proc-type 为安装 hugetlbfs 的目录。
          1static char *ealargs[] = {

          2         "hello_world",

          3         "-c 0x1",

          4         "-n 4",

          5         "--proc-type=auto",

          6};

          7rte_eal_init(sizeof(ealargs) / sizeof(ealargs[0]), ealargs);
          1.  
          2. 创建请求缓冲池,用于 SPDK 内部保存各 I/O 请求数据:
          1request_mempool = rte_mempool_create("nvme_request", 8192,

          2          spdk_nvme_request_size(), 128, 0,

          3          NULL, NULL, NULL, NULL,

          4          SOCKET_ID_ANY, 0);
          1.  
          2. 寻找适用于 NVMe 设备的系统:
          rc = spdk_nvme_probe(NULL, probe_cb, attach_cb, NULL);

          1. 枚举 NVMe 设备,向 SPDK 返回布尔值,表示是否连接设备:
          01static bool

          02probe_cb(void *cb_ctx, struct spdk_pci_device *dev, struct spdk_nvme_ctrlr_opts *opts)

          03{

          04     printf("Attaching to %04x:%02x:%02x.%02x\n",

          05             spdk_pci_device_get_domain(dev),

          06             spdk_pci_device_get_bus(dev),

          07             spdk_pci_device_get_dev(dev),

          08             spdk_pci_device_get_func(dev));

          09 

          10     return true;

          11}

          1. 设备已连接;现在可以请求命名空间数量信息:
          01static void

          02attach_cb(void *cb_ctx, struct spdk_pci_device *dev, struct spdk_nvme_ctrlr *ctrlr,

          03      const struct spdk_nvme_ctrlr_opts *opts)

          04{

          05    int nsid, num_ns;

          06    const struct spdk_nvme_ctrlr_data *cdata = spdk_nvme_ctrlr_get_data(ctrlr);

          07 

          08    printf("Attached to %04x:%02x:%02x.%02x\n",

          09           spdk_pci_device_get_domain(dev),

          10           spdk_pci_device_get_bus(dev),

          11           spdk_pci_device_get_dev(dev),

          12           spdk_pci_device_get_func(dev));

          13 

          14    snprintf(entry->name, sizeof(entry->name), "%-20.20s (%-20.20s)", cdata->mn, cdata->sn);

          15 

          16    num_ns = spdk_nvme_ctrlr_get_num_ns(ctrlr);

          17    printf("Using controller %s with %d namespaces.\n", entry->name, num_ns);

          18    for (nsid = 1; nsid <= num_ns; nsid++) {

          19        register_ns(ctrlr, spdk_nvme_ctrlr_get_ns(ctrlr, nsid));

          20    }

          21}

          1. 枚举用户空间,以检索相关信息(比如大小):
          1static void

          2register_ns(struct spdk_nvme_ctrlr *ctrlr, struct spdk_nvme_ns *ns)

          3{

          4     printf("  Namespace ID: %d size: %juGB\n", spdk_nvme_ns_get_id(ns),

          5            spdk_nvme_ns_get_size(ns) / 1000000000);

          6}

          1. 创建 I/O 队列对,向命名空间提交读/写请求:
          ns_entry->qpair = spdk_nvme_ctrlr_alloc_io_qpair(ns_entry->ctrlr, 0);
          1. 读/写数据
          2. 为即将读/写的数据分配缓冲区:
          sequence.buf = rte_zmalloc(NULL, 0x1000, 0x1000);

          1. 将‘Hello World’拷贝至缓冲区:
          sprintf(sequence.buf, "Hello world!\n");

          1. 将写入请求提交至特定命名空间,从而提供队列对、缓冲区指示器、LBA 索引、数据写入时的回调,以及应传递至回调的数据的指示器:
          1rc = spdk_nvme_ns_cmd_write(ns_entry->ns, ns_entry->qpair, sequence.buf,

          2                            0, /* LBA start */

          3                            1, /* number of LBAs */

          4                            write_complete, &sequence, 0);

          1. 同时调用写入完成回调。
          2. 将读取请求提交至特定命名空间,从而提供队列对、缓冲区指示器、LBA 索引、已读取的数据回调,以及应传递至回调的数据的指示器:
          1rc = spdk_nvme_ns_cmd_read(ns_entry->ns, ns_entry->qpair, sequence->buf,

          2                           0, /* LBA start */

          3                           1, /* number of LBAs */

          4                           read_complete, (void *)sequence, 0);

          1. 同时调用读取完成回调。
          2. 轮询表明数据读写均已完成的标记。 如果请求仍处于传输过程中,我们可以轮询特定队列对的完成情况。 尽管实际数据读写过程处于同步但 spdk_nvme_qpair_process_completions 函数会查看并返回已完成 I/O 请求的数量,并调用上述读/写完成回调:
          1while (!sequence.is_completed) {

          2       spdk_nvme_qpair_process_completions(ns_entry->qpair, 0);

          3}

          1. 退出之前需要释放队列对并进行清空:
          spdk_nvme_ctrlr_free_io_qpair(ns_entry->qpair);

          github 提供有关上述 Hello World 应用的​​完整代码示例​​,有关 SPDK NVME 驱动程序的 ​​API 文档​​请访问​​www.spdk.io​

          运行 Hello World 示例时将显示以下输出:

          【转】SPDK 助力加速 NVMe 硬盘_linux_04

          SPDK 中的其他示例

          SPDK 包含许多示例,可帮助您快速入门并了解 SPDK 的工作原理。 ​​perf 示例​​对 NVMe 硬盘进行了性能评测,以下为该示例的输出:

          【转】SPDK 助力加速 NVMe 硬盘_数据_05

          开发人员如需获取 NVMe 硬盘信息,比如特性、管理命令集属性、NVMe 命令集属性、能源管理和健康信息,可使用​​识别示例​​:

          【转】SPDK 助力加速 NVMe 硬盘_驱动程序_06

          其他实用链接

          作者

          Steven Briscoe 是英特尔(英国)公司的一名应用工程师,在软件服务事业部负责云计算工作。

          Thai Le 是英特尔公司的一名软件工程师,负责云计算和性能计算分析工作。