首页 > 技术文章 > daos 对 spdk 的使用

Heoric 2020-11-23 14:22 原文

第一章 简介

什么是SPDK

存储性能开发工具包(SPDK)提供了一组工具和库,用于编写高性能,可伸缩的用户模式存储应用程序。它通过使用一些关键技术实现了高性能:

  • 将所有必需的驱动程序移动到用户空间,这样可以避免系统调用并启用应用程序的零拷贝访问。
  • 轮询硬件用于完成而不是依赖中断,这降低了总延迟和延迟差异。
  • 避免I / O路径中的所有锁,而是依赖于消息传递

第二章 概念

用户空间的驱动程序

通常,驱动程序在内核空间中运行。SPDK包含的驱动程序设计为在用户空间中运行,但它们仍然直接与它们控制的硬件设备连接。

为了让SPDK控制设备,它必须首先指示操作系统放弃控制。这通常被称为解除设备内核驱动程序的绑定,而Linux则通过写入sysfs中的文件来完成。然后,SPDK将驱动程序重新绑定到与Linux捆绑的两个特殊设备驱动程序之一 - uiovfio这两个驱动程序是“虚拟”驱动程序,因为它们主要向操作系统指示设备有绑定的驱动程序,因此它不会自动尝试重新绑定默认驱动程序。它们实际上并没有以任何方式初始化硬件,也不了解它是什么类型的设备。

中断

SPDK轮询设备以进行完成,而不是等待中断。

  1. 实际上,在用户空间进程中将中断路由到处理程序对于大多数硬件设计来说是不可行的

  2. 中断引入软件抖动并且由于强制上下文而具有显着的开销开关。SPDK中的操作几乎是普遍异步的,允许用户在完成时提供回调。

线程

nvme 设备暴露了多个队列用来向硬件提交请求,可以不需要调度而单独访问队列。因此软件可以从多个并行线程向设备发送请求而无需锁。但是内核驱动程序的设计必须为处理来自系统上不同进程的I/O,并且这些进程的线程会随时间变化。大多数内核驱动程序选择将硬件队列映射到CPU的cores,然后在提交请求时,它们会查找当前线程正在运行的核心的正确硬件队列,通常,他们会需要获取队列周围的锁或临时禁用中断以防止在同一核上运行的线程抢占,这个开销可能很大。

SPDK驱动程序选择将硬件队列直接暴露给应用程序,并要求一次只能从一个线程访问硬件队列。实际上,应用程序为每个线程分配一个硬件队列(而不是内核驱动程序中每个核心一个硬件队列)。这保证了线程可以提交请求,而不必与系统中的其他线程执行任何类型的协调(即锁定)。

来自用户空间的DMA

NVMe设备使用直接内存访问(DMA),将数据传输到系统内存,或从系统内存传输数据。

直接内存访问(DMA)方式是一种完全由硬件执行I/O交换的工作方式。DMA控制器从CPU完全接管对总线的控制。数据交换不经过CPU,而直接在内存和I/O设备之间进行。DMA控制器采用以下三种方式:

  1. 停止CPU访问内存:当外设要求传送一批数据时,由DMA控制器发一个信号给CPU。DMA控制器获得总线控制权后,开始进行数据传送。一批数据传送完毕后,DMA控制器通知CPU可以使用内存,并把总线控制权交还给CPU。

  2. 周期挪用:当I/O设备没有 DMA请求时,CPU按程序要求访问内存:一旦 I/O设备有DMA请求,则I/O设备挪用一个或几个周期。

  3. DMA与CPU交替访内:一个CPU周期可分为2个周期,一个专供DMA控制器访内,另一个专供CPU访内。不需要总线使用权的申请、建立和归还过程。

SPDK依靠DPDK来分配固定内存

以便知道物理地址

IOMMU支持

许多平台包含一个称为I / O内存管理单元(IOMMU)的额外硬件。IOMMU很像常规MMU,除了它为外围设备(即PCI总线)提供虚拟化地址空间。MMU了解系统上每个进程的虚拟到物理映射,因此IOMMU将特定设备与这些映射之一相关联,然后允许用户分配任意总线地址到他们的过程中的虚拟地址。然后,通过将总线地址转换为虚拟地址,然后将虚拟地址转换为物理地址。

消息传递和并发

SPDK的主要目标之一是通过添加硬件进行线性扩展。这可能意味着实践中的许多事情。例如,从一个SSD移动到两个SSD应该是每秒I / O数量的两倍。或者将CPU核心数量增加一倍应该可以使计算量增加一倍。或者甚至将NIC数量加倍也应该使网络吞吐量翻倍。为实现此目的,必须设计软件使得执行线程尽可能彼此独立。在实践中,这意味着避免软件锁甚至原子指令。

传统上,软件通过将一些共享数据放到堆上,用锁保护它,然后让所有执行线程仅在需要访问共享数据时获得锁来实现并发。这个模型有很多很棒的属性:

  • 将单线程程序转换为多线程程序相对容易,因为您不必从单线程版本更改数据模型。您只需在数据周围添加锁定即可。
  • 您可以将程序编写为从上到下阅读的同步,命令性语句列表。
  • 您的线程可以在后台由操作系统调度程序中断并进入休眠状态,从而实现CPU资源的高效时间共享。

不幸的是,随着线程数量的增加,对共享数据周围锁定的争用也会发生。更细粒度的锁定有所帮助,但也大大增加了程序的复杂性。即使这样,超过一定数量的高度争用的锁,线程将花费大部分时间来尝试获取锁,并且程序将不会受益于任何额外的CPU核。

消息传递

SPDK完全采用不同的方法。SPDK通常会将该数据分配给单个线程,而不是将共享数据放在所有线程获取锁定后访问的全局位置。当其他线程想要访问数据时,它们会向拥有的线程传递一条消息,以代表它们执行操作。

SPDK中的消息通常由函数指针和指向某个上下文的指针组成,并使用无锁环在线程之间传递。消息传递通常比大多数软件开发人员的直觉导致他们相信的速度快得多,这主要是由于缓存效应。如果单个核心始终访问相同的数据(代表所有其他核心),则该数据更可能位于更靠近该核心的缓存中。通常最有效的方法是让每个核心工作在一个相对较小的数据集中,然后将其放在本地缓存中,然后在完成后将一条小消息传递给下一个核心。

在更极端的情况下,即使是消息传递也可能成本过高,将为每个线程创建一个数据副本。然后该线程将只引用其本地副本。为了改变数据,线程将向每个其他线程发送一条消息,告诉它们在本地副本上执行更新。当数据不经常变化,但可能经常被读取,并且经常被用于I/O路径时,这是非常理想的。这当然是为了计算效率而交换内存大小,因此它的使用仅限于最关键的代码路径。

消息传递基础结构

首先,spdk_thread是执行线程的抽象,spdk_poller是一个应该在给定线程上定期调用的函数的抽象。
在用户希望与SPDK一起使用的每个系统线程上,它们必须首先调用spdk_thread_create()。
该库还定义了另外两个抽象:spdk_io_device和spdk_io_channel

抽象地说,我们将设备概括为spdk_io_device,将特定于线程的队列概括spdk_io_channel。然而,随着时间的推移,这种模式出现在很多地方,与我们最初选择的名字不太匹配。在今天的代码中,spdk_io_device是任何指针,其唯一性仅取决于其内存地址,spdk_io_channel是与特定spdk_io_device相关联的每线程上下文。

线程抽象提供了将消息发送到任何其他线程、逐个向所有线程发送消息、以及向那些拥有io_channel的给定io_device的所有线程发送消息的功能。

事件框架

C语言的局限性

消息传递是有效的,但它会导致异步代码。不幸的是,异步代码在C中是一个挑战。它通常通过传递操作完成时调用的函数指针来实现。这会削减代码,因此不容易遵循,特别是通过逻辑分支。最好的解决方案是使用支持futures和promises的语言。但是,SPDK是一个低级库,需要非常广泛的兼容性和可移植性,所以我们选择留在普通的旧C中。

NAND Flash SSD内部

将I / O提交到NVMe设备

使用Vhost-user进行虚拟化I / O

第三章 用户指南

Blobstore程序员指南

介绍

Blobstore是一种持久的电源故障安全块分配器,旨在用作支持更高级别存储服务的本地存储系统,通常代替传统的文件系统。这些更高级别的服务可以是本地数据库或键/值存储(MySQL,RocksDB),它们可以是专用设备(SAN,NAS)或分布式存储系统(例如Ceph,Cassandra)。但是,它并非设计为通用文件系统,并且故意不符合POSIX标准。为了避免混淆,我们避免引用文件或对象,而使用术语“blob”。Blobstore旨在允许对名为“blobs”的块设备上的块组进行异步、非缓存、并行读取和写入。Blob通常很大,至少数百千字节为单位,并且总是底层块大小的倍数。

工作原理

抽象

Blobstore定义了存储抽象的层次结构,如下所示。

Logical Block

逻辑块由磁盘本身公开,磁盘编号从0到N,其中N是磁盘中的块数。逻辑块通常是512B或4KiB。

Page

页面定义为在Blobstore创建时定义的固定数量的逻辑块。组成页面的逻辑块始终是连续的。页面也从磁盘的开头编号,使得第一页的块是第0页,第二页是第1页,等等。页面的大小通常是4KiB,因此在实践中这是8或1个逻辑块。SSD必须能够执行至少页面大小的原子读取和写入。

Cluster

群集是在Blobstore创建时定义的固定页数。组成群集的页面始终是连续的。群集也从磁盘的开头编号,其中群集0是第一个群集页面,群集1是第二组页面等。群集通常是1MiB大小,或256页

Blob

blob是一个有序的集群列表。应用程序操纵(创建,调整大小,删除等)Blob,并在电源故障和重新启动时保持不变。应用程序使用Blobstore提供的标识符来访问特定blob。通过指定从blob开头的偏移量,以页为单位读取和写入Blob。应用程序还可以以键/值对的形式存储元数据,每个blob我们将其称为xattrs(扩展属性)

Blobstore

已由基于Blobstore的应用程序初始化的SSD称为“Blobstore”。Blobstore拥有整个底层设备,该设备由私有Blobstore元数据区域和由应用程序管理的blob集合组成

image-20200608143528655

块设备层编程指南

介绍

块设备是支持以固定大小的块读取和写入数据的存储设备。这些块通常为512或4096字节。设备可以是软件中的逻辑构造,或者对应于诸如NVMe SSD的物理设备。
块设备层由单个通用库lib/bdev和多个可选模块(作为单独的库)组成,这些模块实现了各种类型的块设备。通用库的公共头文件是bdev.h,它是与任何类型的块设备交互所需的全部API。本指南将介绍如何使用该API与bdev进行交互。
除了为所有块设备提供通用抽象之外,bdev层还提供了许多有用的功能:

  • 自动排队I / O请求以响应队列满或内存不足的情况
  • 即使在I / O流量发生时,也可以热删除支持。
  • 带宽和延迟等I / O统计信息
  • 设备重置支持和I / O超时跟踪

第四章 程序员指南

对dpdk 的依赖

#include <rte_config.h>    // 一堆宏定义
#include <rte_cycles.h>    // rdts
#include <rte_malloc.h>    // malloc
#include <rte_mempool.h>   // mempool
#include <rte_memzone.h>   // 连续物理内存,(物理地址)
#include <rte_version.h>   // 版本号  
#include <rte_eal.h>       // EAL Configuration API
#include <rte_bus.h>       // DPDK device bus interface
#include <rte_pci.h>       // RTE PCI Library
#include <rte_bus_pci.h>   //  RTE PCI Bus Interface
#include <rte_dev.h>       //  RTE PMD Driver Registration Interface
#include <rte_common.h>
#include <rte_log.h>
#include <rte_errno.h>
#include <rte_vfio.h>       // setup/release device
#include <rte_memory.h>
#include <rte_eal_memconfig.h>  // eal 内存配置
#include <rte_alarm.h>          // eal 提供 alarm 
#include <rte_lcore.h>         // Logical core  / physical sockets / thread
#include <rte_bus_vdev.h>      // RTE virtual bus API
#include <rte_compressdev.h>   //  RTE Compression Device APIs
#include <rte_comp.h>          // RTE definitions for Data Compression Service
#include <rte_crypto.h>        // RTE Cryptography Common Definitions
#include <rte_cryptodev.h>
#include <rte_cryptodev_pmd.h>
#include <rte_ethdev.h>     // The Ethernet Device API  / The application-oriented Ethernet API / The driver-oriented Ethernet API
#include <rte_string_fns.h>  //String-related functions as replacement for libc equivalents
#include <rte_vhost.h>
#include <rte_ether.h>	// Ethernet Helpers in RTE

//   rte_ring.h

DAOS 用到的接口

  1. spdk_dma_malloc
  2. spdk_dma_free
  3. spdk_dma_zmalloc
  4. spdk_thread_lib_fini
  5. spdk_thread_create
  6. spdk_thread_exit
  7. spdk_thread_send_msg
  8. spdk_thread_poll
  9. spdk_set_thread
  10. spdk_unaffinitize_thread
  11. spdk_blob_opts_init
  12. spdk_blob_io_write
  13. spdk_blob_io_read
  14. spdk_blob_io_unmap
  15. spdk_blob_close
  16. spdk_bs_init
  17. spdk_bs_load
  18. spdk_bs_create_blob_ext
  19. spdk_bs_delete_blob
  20. spdk_bs_open_blob
  21. spdk_bs_get_cluster_size
  22. spdk_bs_get_io_unit_size
  23. spdk_bs_free_io_channel
  24. spdk_bs_unload
  25. spdk_bs_get_bstype
  26. spdk_bs_alloc_io_channel
  27. spdk_bs_free_io_channel
  28. spdk_bs_opts_init
  29. spdk_put_io_channel
  30. spdk_bdev_io_get_nvme_status
  31. spdk_bdev_desc_get_bdev
  32. spdk_bdev_free_io
  33. spdk_bdev_nvme_admin_passthru
  34. spdk_bdev_io_type_supported
  35. spdk_bdev_get_io_stat
  36. spdk_bdev_get_name
  37. spdk_bdev_close
  38. spdk_bdev_get_io_channel
  39. spdk_bdev_get_product_name
  40. spdk_bdev_create_bs_dev
  41. spdk_bdev_first
  42. spdk_bdev_next
  43. spdk_bdev_open
  44. spdk_bdev_finish
  45. spdk_bdev_initialize
  46. spdk_pci_addr_parse
  47. spdk_pci_addr_compare
  48. spdk_pci_addr_fmt
  49. spdk_pci_device_get_socket_id
  50. spdk_conf_find_section
  51. spdk_conf_section_get_nmval
  52. spdk_conf_first_section
  53. spdk_conf_set_as_default
  54. spdk_conf_allocate
  55. spdk_conf_read
  56. spdk_conf_free
  57. spdk_env_opts_init
  58. spdk_env_init
  59. spdk_env_fini
  60. spdk_free
  61. spdk_copy_engine_finish
  62. spdk_copy_engine_initialize
  63. spdk_nvme_ctrlr_cmd_get_log_page
  64. spdk_nvme_ctrlr_process_admin_completions
  65. spdk_nvme_ctrlr_free_cmb_io_buffer
  66. spdk_nvme_ctrlr_get_first_active_ns
  67. spdk_nvme_ctrlr_get_next_active_ns
  68. spdk_nvme_ctrlr_get_num_ns
  69. spdk_nvme_ctrlr_get_ns
  70. spdk_nvme_ctrlr_get_data
  71. spdk_nvme_ctrlr_get_pci_device
  72. spdk_nvme_ctrlr_format
  73. spdk_nvme_ctrlr_update_firmware
  74. spdk_nvme_ctrlr_alloc_io_qpair
  75. spdk_nvme_ctrlr_alloc_cmb_io_buffer
  76. spdk_nvme_ctrlr_free_io_qpair
  77. spdk_nvme_ns_cmd_write
  78. spdk_nvme_ns_get_size
  79. spdk_nvme_ns_is_active
  80. spdk_nvme_ns_get_id
  81. spdk_nvme_spdk_zmallocqpair_process_completions
  82. spdk_nvme_cpl_is_error
  83. spdk_nvme_transport_id_parse
  84. spdk_nvme_detach
  85. spdk_nvme_attach_cb
  86. spdk_nvme_probe_cb
  87. spdk_nvme_remove_cb

DAOS 用到的数据结构

  1. spdk_io_channel
  2. spdk_thread
  3. spdk_env_opts
  4. spdk_blob
  5. spdk_blob_id
  6. spdk_blob_opts
  7. spdk_blob_store
  8. spdk_bdev
  9. spdk_bdev_desc
  10. spdk_bdev_io_stat
  11. spdk_conf
  12. spdk_conf_section
  13. spdk_pci_addr
  14. spdk_pci_device
  15. spdk_bs_opts
  16. spdk_bs_dev
  17. spdk_bs_type
  18. spdk_nvme_ctrlr_data
  19. spdk_nvme_cmd
  20. spdk_nvme_probe
  21. spdk_nvme_error_information_entry
  22. spdk_nvme_cpl
  23. spdk_nvme_ns
  24. spdk_nvme_fw_commit_action
  25. spdk_nvme_status
  26. spdk_nvme_transport_id
  27. spdk_nvme_ctrlr
  28. spdk_nvme_qpair
  29. spdk_nvme_ctrlr_opts
  30. spdk_nvme_format
  31. spdk_nvme_health_information_page
  32. spdk_nvme_critical_warning_state

spdk 对dpdk 的依赖

#include <rte_config.h>    // 一堆宏定义
#include <rte_cycles.h>    // rdts
#include <rte_malloc.h>    // malloc
#include <rte_mempool.h>   // mempool
#include <rte_memzone.h>   // 连续物理内存,(物理地址)
#include <rte_version.h>   // 版本号  
#include <rte_eal.h>       // EAL Configuration API
#include <rte_bus.h>       // DPDK device bus interface
#include <rte_pci.h>       // RTE PCI Library
#include <rte_bus_pci.h>   //  RTE PCI Bus Interface
#include <rte_dev.h>       //  RTE PMD Driver Registration Interface
#include <rte_common.h>
#include <rte_log.h>
#include <rte_errno.h>
#include <rte_vfio.h>       // setup/release device
#include <rte_memory.h>
#include <rte_eal_memconfig.h>  // eal 内存配置
#include <rte_alarm.h>          // eal 提供 alarm 
#include <rte_lcore.h>         // Logical core  / physical sockets / thread
#include <rte_bus_vdev.h>      // RTE virtual bus API
#include <rte_compressdev.h>   //  RTE Compression Device APIs
#include <rte_comp.h>          // RTE definitions for Data Compression Service
#include <rte_crypto.h>        // RTE Cryptography Common Definitions
#include <rte_cryptodev.h>
#include <rte_cryptodev_pmd.h>
#include <rte_ethdev.h>     // The Ethernet Device API  / The application-oriented Ethernet API / The driver-oriented Ethernet API
#include <rte_string_fns.h>  //String-related functions as replacement for libc equivalents
#include <rte_vhost.h>
#include <rte_ether.h>	// Ethernet Helpers in RTE
//   rte_ring.h

第五章 DAOS 对SPDK 的应用

daos_server

1、discover

discover

2、format 格式化设备 destructive operation(通过pci地址)

format

3、update

4、cleanup

daos_io_server

daos_io_server

bio_internale.h

bio_desc

1、xstream

bio_nvme_init 函数用来 初始化结构体 bio_nvme_data

bio_spdk_env_init 函数用来初始化 spdk_env_opts

bio_nvme_init

bio_xsctxt_alloc

2、context

bio_blob_open

bio_blob_close

。。。。

blob_msg_create

blob_msg_open

blob_msg_close

。。。

3、monitor

检查设备状态。。

4、buffer

读写操作

5、recovery

第六章 块设备的访问

struct spdk_bdev
struct spdk_bdev_desc
struct spdk_bdev_io
struct spdk_io_channel
    

    
spdk_bdev_initialize()
spdk_bdev_open()
spdk_bdev_close()
    
spdk_bdev_get_io_channel()
    
spdk_bdev_write()
spdk_bdev_write_blocks()
    
spdk_bdev_read()
spdk_bdev_read_blocks()
    
spdk_bdev_free_io()

img

推荐阅读