第一章 簡介
什么是SPDK
存儲性能開發工具包(SPDK)提供了一組工具和庫,用於編寫高性能,可伸縮的用戶模式存儲應用程序。它通過使用一些關鍵技術實現了高性能:
- 將所有必需的驅動程序移動到用戶空間,這樣可以避免系統調用並啟用應用程序的零拷貝訪問。
- 輪詢硬件用於完成而不是依賴中斷,這降低了總延遲和延遲差異。
- 避免I / O路徑中的所有鎖,而是依賴於消息傳遞
第二章 概念
用戶空間的驅動程序
通常,驅動程序在內核空間中運行。SPDK包含的驅動程序設計為在用戶空間中運行,但它們仍然直接與它們控制的硬件設備連接。
為了讓SPDK控制設備,它必須首先指示操作系統放棄控制。這通常被稱為解除設備內核驅動程序的綁定,而Linux則通過寫入sysfs中的文件來完成。然后,SPDK將驅動程序重新綁定到與Linux捆綁的兩個特殊設備驅動程序之一 - uio或vfio這兩個驅動程序是“虛擬”驅動程序,因為它們主要向操作系統指示設備有綁定的驅動程序,因此它不會自動嘗試重新綁定默認驅動程序。它們實際上並沒有以任何方式初始化硬件,也不了解它是什么類型的設備。
中斷
SPDK輪詢設備以進行完成,而不是等待中斷。
-
實際上,在用戶空間進程中將中斷路由到處理程序對於大多數硬件設計來說是不可行的
-
中斷引入軟件抖動並且由於強制上下文而具有顯着的開銷開關。SPDK中的操作幾乎是普遍異步的,允許用戶在完成時提供回調。
線程
nvme 設備暴露了多個隊列用來向硬件提交請求,可以不需要調度而單獨訪問隊列。因此軟件可以從多個並行線程向設備發送請求而無需鎖。但是內核驅動程序的設計必須為處理來自系統上不同進程的I/O,並且這些進程的線程會隨時間變化。大多數內核驅動程序選擇將硬件隊列映射到CPU的cores,然后在提交請求時,它們會查找當前線程正在運行的核心的正確硬件隊列,通常,他們會需要獲取隊列周圍的鎖或臨時禁用中斷以防止在同一核上運行的線程搶占,這個開銷可能很大。
SPDK驅動程序選擇將硬件隊列直接暴露給應用程序,並要求一次只能從一個線程訪問硬件隊列。實際上,應用程序為每個線程分配一個硬件隊列(而不是內核驅動程序中每個核心一個硬件隊列)。這保證了線程可以提交請求,而不必與系統中的其他線程執行任何類型的協調(即鎖定)。
來自用戶空間的DMA
NVMe設備使用直接內存訪問(DMA),將數據傳輸到系統內存,或從系統內存傳輸數據。
直接內存訪問(DMA)方式是一種完全由硬件執行I/O交換的工作方式。DMA控制器從CPU完全接管對總線的控制。數據交換不經過CPU,而直接在內存和I/O設備之間進行。DMA控制器采用以下三種方式:
-
停止CPU訪問內存:當外設要求傳送一批數據時,由DMA控制器發一個信號給CPU。DMA控制器獲得總線控制權后,開始進行數據傳送。一批數據傳送完畢后,DMA控制器通知CPU可以使用內存,並把總線控制權交還給CPU。
-
周期挪用:當I/O設備沒有 DMA請求時,CPU按程序要求訪問內存:一旦 I/O設備有DMA請求,則I/O設備挪用一個或幾個周期。
-
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集合組成
塊設備層編程指南
介紹
塊設備是支持以固定大小的塊讀取和寫入數據的存儲設備。這些塊通常為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 用到的接口
- spdk_dma_malloc
- spdk_dma_free
- spdk_dma_zmalloc
- spdk_thread_lib_fini
- spdk_thread_create
- spdk_thread_exit
- spdk_thread_send_msg
- spdk_thread_poll
- spdk_set_thread
- spdk_unaffinitize_thread
- spdk_blob_opts_init
- spdk_blob_io_write
- spdk_blob_io_read
- spdk_blob_io_unmap
- spdk_blob_close
- spdk_bs_init
- spdk_bs_load
- spdk_bs_create_blob_ext
- spdk_bs_delete_blob
- spdk_bs_open_blob
- spdk_bs_get_cluster_size
- spdk_bs_get_io_unit_size
- spdk_bs_free_io_channel
- spdk_bs_unload
- spdk_bs_get_bstype
- spdk_bs_alloc_io_channel
- spdk_bs_free_io_channel
- spdk_bs_opts_init
- spdk_put_io_channel
- spdk_bdev_io_get_nvme_status
- spdk_bdev_desc_get_bdev
- spdk_bdev_free_io
- spdk_bdev_nvme_admin_passthru
- spdk_bdev_io_type_supported
- spdk_bdev_get_io_stat
- spdk_bdev_get_name
- spdk_bdev_close
- spdk_bdev_get_io_channel
- spdk_bdev_get_product_name
- spdk_bdev_create_bs_dev
- spdk_bdev_first
- spdk_bdev_next
- spdk_bdev_open
- spdk_bdev_finish
- spdk_bdev_initialize
- spdk_pci_addr_parse
- spdk_pci_addr_compare
- spdk_pci_addr_fmt
- spdk_pci_device_get_socket_id
- spdk_conf_find_section
- spdk_conf_section_get_nmval
- spdk_conf_first_section
- spdk_conf_set_as_default
- spdk_conf_allocate
- spdk_conf_read
- spdk_conf_free
- spdk_env_opts_init
- spdk_env_init
- spdk_env_fini
- spdk_free
- spdk_copy_engine_finish
- spdk_copy_engine_initialize
- spdk_nvme_ctrlr_cmd_get_log_page
- spdk_nvme_ctrlr_process_admin_completions
- spdk_nvme_ctrlr_free_cmb_io_buffer
- spdk_nvme_ctrlr_get_first_active_ns
- spdk_nvme_ctrlr_get_next_active_ns
- spdk_nvme_ctrlr_get_num_ns
- spdk_nvme_ctrlr_get_ns
- spdk_nvme_ctrlr_get_data
- spdk_nvme_ctrlr_get_pci_device
- spdk_nvme_ctrlr_format
- spdk_nvme_ctrlr_update_firmware
- spdk_nvme_ctrlr_alloc_io_qpair
- spdk_nvme_ctrlr_alloc_cmb_io_buffer
- spdk_nvme_ctrlr_free_io_qpair
- spdk_nvme_ns_cmd_write
- spdk_nvme_ns_get_size
- spdk_nvme_ns_is_active
- spdk_nvme_ns_get_id
- spdk_nvme_spdk_zmallocqpair_process_completions
- spdk_nvme_cpl_is_error
- spdk_nvme_transport_id_parse
- spdk_nvme_detach
- spdk_nvme_attach_cb
- spdk_nvme_probe_cb
- spdk_nvme_remove_cb
DAOS 用到的數據結構
- spdk_io_channel
- spdk_thread
- spdk_env_opts
- spdk_blob
- spdk_blob_id
- spdk_blob_opts
- spdk_blob_store
- spdk_bdev
- spdk_bdev_desc
- spdk_bdev_io_stat
- spdk_conf
- spdk_conf_section
- spdk_pci_addr
- spdk_pci_device
- spdk_bs_opts
- spdk_bs_dev
- spdk_bs_type
- spdk_nvme_ctrlr_data
- spdk_nvme_cmd
- spdk_nvme_probe
- spdk_nvme_error_information_entry
- spdk_nvme_cpl
- spdk_nvme_ns
- spdk_nvme_fw_commit_action
- spdk_nvme_status
- spdk_nvme_transport_id
- spdk_nvme_ctrlr
- spdk_nvme_qpair
- spdk_nvme_ctrlr_opts
- spdk_nvme_format
- spdk_nvme_health_information_page
- 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
2、format 格式化設備 destructive operation(通過pci地址)
3、update
4、cleanup
daos_io_server
bio_internale.h
bio_desc
1、xstream
bio_nvme_init 函數用來 初始化結構體 bio_nvme_data
bio_spdk_env_init 函數用來初始化 spdk_env_opts
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()