[部分轉自 https://www.sdnlab.com/21087.html】
轉自 https://www.cnblogs.com/vlhn/p/7727141.html
https://blog.csdn.net/weixin_37097605/article/details/101488760
SPDK 應用編程框架
SPDK (Storage performance development kit, http://spdk.io) 是由Intel發起、用於加速使用NVMe SSD作為后端存儲的應用軟件加速庫。該軟件庫的核心是用戶態、異步、輪詢方式的NVMe驅動。較之內核(諸如Linux Kernel) 的NVMe驅動,它可以大幅度降低NVMe command的延遲 (Latency) ,同時提高單CPU核的IOPS,從而形成一套高性價比的解決方案,例如使用SPDK的vhost解決方案可以應用於HCI (Hyper Converged Infrastructure) 加速虛擬機中的NVMe I/O。
為了實現上述目標,僅僅提供用戶態NVMe驅動的一些操作函數或源語是不夠的。如果在某些應用場景中使用不當,不僅不能發揮出用戶態NVMe驅動的高性能,甚至會導致程序錯誤。雖然NVMe的底層函數有一些說明,但為了更好地發揮出底層NVMe的性能,SPDK提供了一套編程框架 (SPDK Application Framework),用於指導軟件開發人員基於SPDK的用戶態NVMe驅動以及用戶態塊設備層 (User space Bdev) 構造高效的存儲應用。用戶有兩種選擇:
- (1) 直接使用SPDK應用編程框架實現應用的邏輯;
- (2) 使用SPDK編程框架的思想,改造應用的編程邏輯,以更好的適配SPDK的用戶態NVMe驅動。
總體而言,SPDK的應用框架可以分為以下幾部分:
- (1) 對CPU core和線程的管理;
- (2) 線程間的高效通信;
- (3) I/O的的處理模型以及數據路徑(data path)的無鎖化機制。
CPU core和線程的管理
SPDK一大宗旨是使用最少的CPU核和線程來完成最多的任務。為此,SPDK在初始化程序時(目前調用spdk_app_start函數)限定使用綁定CPU的哪些核,可以在配置文件或命名行中配置,例如在命令行中使用-c 0x5是指使用core0 和core2來啟動程序。通過CPU核綁定函數的親和性可以限制住CPU的使用,並且在每個核上運行一個thread,該thread在SPDK中被稱為Reactor (如Figure 1所示)。目前SPDK的環境庫 (ENV) 缺省仍舊使用了DPDK的EAL庫來進行管理。總而言之,Reactor thread執行一個函數 (_spdk_reactor_run), 該函數的主體包含一個while (1) {} 功能的函數,直到Reactor的state被改變,例如受到 (spdk_app_stop 的調用)。為了高效,上述循環中也會有一些相應的機制讓出CPU資源 (諸如sleep)。這樣的機制大多時候會導致CPU使用100%的情況,這點和DPDK比較類似。
換言之,假設一個使用SPDK編程框架的應用運用了兩個CPU core,那么每個core上就會啟動一個Reactor thread。如此一來,用戶怎么執行自己的函數呢?為了解決該問題,SPDK提供了一個Poller的機制,即用戶定義函數的分裝。SPDK提供的Poller分為兩種:
- (1) 基於定時器的Poller;
- (2) 非定時器的Poller。
SPDK的Reactor thread對應的數據結構(struct spdk_reactor) 有相應的列表來維護Poller的機制。例如,一個鏈表維護定時器的Poller,一個鏈表維護非定時器的Poller,並且提供Poller的注冊和銷毀函數。在Reactor的while循環中,它會不停的check這些Poller的狀態,進行相應的調用,用戶的函數也因此可以進行相應的調用。由於單個CPU上只有一個Reactor thread,所以同一個Reactor thread 中不需要一些鎖的機制來保護資源。當然,位於不同CPU的core上的thread還是需要通信必要。為了解決該問題,SPDK封裝了線程間異步傳遞消息 (Async Messaging Passing) 的方式。
線程間的高效通信
SPDK放棄使用傳統的加鎖方式來進行線程間的通信,因為這種方案比較低效。為了使同一個thread只執行自己所管理的資源,SPDK提供了Event (事件調用) 機制。該機制的本質是每個Reactor對應的數據結構 (struct spdk_reactor) 維護了一個Event事件的ring (環)。這個環是多生產者和單消費者 (MPSC: Multiple producer Single Consumer) 的模型,即每個Reactor thread可以接收來自任何其他Reactor thread (包括當前的Reactor Thread) 的事件消息進行處理。目前SPDK中Event ring的缺省實現依賴於DPDK的機制,應該有線性鎖的機制,但是相較於線程間采用鎖的機制進行同步要高效得多。
毫無疑問,Event ring處理的同時也在進行Reactor的函數 (_spdk_reactor_run) 處理。每個Event事件的數據結構 (struct spdk_event) 其實包括了需要執行的函數、加上相應的參數以及要執行的core。簡單而言,一個Reactor A 向另外一個Reactor B通信,其實就是需要Reactor B執行函數F(X) (X是相應的參數)。
基於上述機制,SPDK就實現了一套比較高效的線程間通信機制。具體例子可以參照SPDK NVMe-oF target內部的一些實現,主要代碼位於 (lib/nvmf) 目錄。
I/O處理模型以及數據路徑的無鎖化
SPDK主要的I/O 處理模型是Run-to-completion,指運行直到全部完成。上述內容中提及,使用SPDK應用框架時,一個CPU core只擁有一個thread,該thread可以執行很多Poller (包括定時和非定時器)。Run-to-completion的宗旨是讓一個線程最好執行完所有的任務。顯而易見,SPDK的編程框架滿足了該需要。如果不使用SPDK應用編程框架,則需要編程者自己注意這個事項。例如,使用SPDK用戶態NVMe驅動訪問相應的I/O QPair進行讀寫操作,SPDK 提供了異步讀寫的函數 (spdk_nvme_ns_cmd_read),同時檢查是否完成的函數 (spdk_nvme_qpair_process_completions)。這些函數的調用應由一個線程完成,不應該跨線程處理。
SPDK 的I/O 路徑也采用無鎖化機制。當多個thread操作同意SPDK 用戶態block device (bdev) 時,SPDK會提供一個I/O channel的概念 (即thread和device的一個mapping關系)。不同的thread 操作同一個device應該擁有不同的I/O channel,每個I/O channel在I/O路徑上使用自己獨立的資源就可以避免資源競爭,從而去除鎖的機制。
【翻譯自 https://spdk.io/doc/concurrency.html
SPDK的消息傳遞和並發性
SPDK的初始目標是隨着硬件的增加,性能也能得到線性增長。比如說由一個SSD增加到2個SSD時,每秒執行的IO應該是原來兩倍;或者CPU數量增倍時,計算能力也增倍;或者NIC數量增倍時,網絡的throughput也增倍。為了實現這個,軟件上設計必須讓線程的執行盡量的相互獨立,這意味着避免加鎖甚至原子操作。
傳統的軟件實現並發性是通過將共享的數據放到heap上,然后通過鎖來保護,當線程需要訪問該共享數據時,要先去獲取鎖。這種方法的特點是:
- It's relatively easy to convert single-threaded programs to multi-threaded programs because you don't have to change the data model from the single-threaded version. You just add a lock around the data.
- You can write your program as a synchronous, imperative list of statements that you read from top to bottom.
- Your threads can be interrupted and put to sleep by the operating system scheduler behind the scenes, allowing for efficient time-sharing of CPU resources.
但是當線程增多時,相互之間對鎖的競爭會導致線程花很多時間去獲取鎖,使得程序沒法受益於增加的CPU核。
SPDK采用不同的方法。SPDK經常只把數據assign給一個線程。當其他線程要獲取這個數據時,傳遞一個消息給數據的owner線程讓其代替完成操作。
spdk_msg_fn fn;
void *arg;
};
SPDK中的消息通常包含 一個函數指針,一個context指針,通過lockless ring.在線程間傳遞。由於caching effects,消息的傳遞往往比想象的快,因為如果一個core一直在訪問同樣 的數據(代替其他core操作),那么數據更可能放在離這個core更近的cache。 It's often most efficient to have each core work on a relatively small set of data sitting in its local cache and then hand off a small message to the next core when done.
在更極端的情況,若message傳遞的花費太高,每個線程都保留有一份數據的副本,線程只訪問自己擁有的副本數據,若要修改,那么發消息通知其他線程更新數據。這種方法適用於數據的修改不頻繁但是經常讀的情況。and is often employed in the I/O path. This of course trades memory size for computational efficiency, so it's use is limited to only the most critical code paths.
消息傳遞infrastructure
SPDK提供了幾層消息傳遞infrastructure。
最基礎的庫 沒有采用消息傳遞,只是列舉了函數何時被調用的規則。(例如NVMe Driver)。
大部分的庫 用SPDK的 thread abstraction,在libspdk_thread.a中。其提供了基本的消息傳遞框架並定義了一些關鍵primitives。
struct
spdk_thread {
// Tail queue declarations. 隊列頭 TAILQ_HEAD(, spdk_io_channel) io_channels; //描述前一個和下一個元素的結構體 TAILQ_ENTRY(spdk_thread) tailq; char *name;
struct spdk_cpuset *cpumask;
uint64_t tsc_last;
struct spdk_thread_stats stats;
/*
* Contains pollers actively running on this thread. Pollers * are run round-robin. The thread takes one poller from the head * of the ring, executes it, then puts it back at the tail of * the ring. */ TAILQ_HEAD(active_pollers_head, spdk_poller) active_pollers;
/**
* Contains pollers running on this thread with a periodic timer. */ TAILQ_HEAD(timer_pollers_head, spdk_poller) timer_pollers;
struct spdk_ring *messages;
/*
singly-linked list:This structure contains a single pointer to the first element on the list. SLIST_HEAD(HEADNAME, TYPE) head;
*/
SLIST_HEAD(, spdk_msg) msg_cache; size_t msg_cache_count;
/* User context allocated at the end */
uint8_t ctx[0]; } |
在指定的線程上周期性地被調用:
struct
spdk_poller {
TAILQ_ENTRY(spdk_poller) tailq;
/* Current state of the poller; should only be accessed from the poller's thread. */
enum spdk_poller_state state;
uint64_t period_ticks;
uint64_t next_run_tick; spdk_poller_fn fn; void *arg; }; |
struct
io_device {
void *io_device; char *name; spdk_io_channel_create_cb create_cb; spdk_io_channel_destroy_cb destroy_cb; spdk_io_device_unregister_cb unregister_cb; struct spdk_thread *unregister_thread; uint32_t ctx_size; uint32_t for_each_count; TAILQ_ENTRY(io_device) tailq;
uint32_t refcnt;
bool unregistered;
}; |
/**
* \brief Represents a per-thread channel for accessing an I/O device. * * An I/O device may be a physical entity (i.e. NVMe controller) or a software * entity (i.e. a blobstore). * * This structure is not part of the API - all accesses should be done through * spdk_io_channel function calls. */ struct spdk_io_channel { struct spdk_thread *thread; struct io_device *dev; uint32_t ref; uint32_t destroy_ref; TAILQ_ENTRY(spdk_io_channel) tailq; spdk_io_channel_destroy_cb destroy_cb;
/*
* Modules will allocate extra memory off the end of this structure * to store references to hardware-specific references (i.e. NVMe queue * pairs, or references to child device spdk_io_channels (i.e. * virtual bdevs). */ }; |
使用時先調用 spdk_allocate_thread()??(沒這個函數)This function takes three function pointers - one that will be called to pass a message to this thread, one that will be called to request that a poller be started on this thread, and finally one to request that a poller be stopped. The implementation of these functions is not provided by this library. Many applications already have facilities for passing messages, so to ease integration with existing code bases we've left the implementation up to the user.
In today's code spdk_io_device is any pointer, whose uniqueness is predicated only on its memory address, and spdk_io_channel is the per-thread context associated with a particular spdk_io_device.
event framework event.h
event框架是可選的?大部分其他SPDK模塊可以不依賴於SPDK event庫???
To accomplish cross-thread communication while minimizing synchronization overhead, the framework provides message passing in the form of events.
組成部分:reactors, events, and pollers
event
The event framework spawns one thread per core (reactor) and connects the threads with lockless queues. Messages (events) can then be passed between the threads.
每個CPU上跑一個 event loop thread(這些thread稱為reactor)。這些線程處理 incoming events from a queue。每個event包括一個捆綁的函數指針和其參數,給一個指定cpu core。
spdk_event_allocate() 創建event;
spdk_event_call() 傳event給指定的core,並執行event。
reactor
每個reactor有一個 lock-free的隊列來放傳給該core的event。每個core的線程可以插event到任何其他core的隊列中。每個core上的reactor loop檢查incoming events並以first-in first-out的順序執行。Event functions should never block and should preferably execute very quickly, since they are called directly from the event loop on the destination core.
struct spdk_reactor {
/* Logical core number for this reactor. */ uint32_t lcore;
/* Lightweight threads running on this reactor */
TAILQ_HEAD(, spdk_lw_thread) threads;
/* Poller for get the rusage for the reactor. */
struct spdk_poller *rusage_poller;
/* The last known rusage values */
struct rusage rusage;
struct spdk_ring *events;
} __attribute__((aligned(64))); |
poller
poller和event一樣是帶參的函數,可以被捆綁、執行。但是不像event,poller是一直在所注冊的線程上重復地執行,直到被注銷。The reactor event loop intersperses calls to the pollers with other event processing. Pollers are intended to poll hardware as a replacement for interrupts. Normally, pollers are executed on every iteration of the main event loop.
】
常見問題
01 SPDK每年發布幾個版本? 發布版本號是怎么樣的?
A: 4個版本。發布版本采用年份加月份的方式:YY.MM (其中MM 屬於集合{1,4,7,11})。每年一共發布4個版本,分別在1月、4月、7月和11月發布。SPDK即將發布的版本是18.04。
02 SPDK開源項目和DPDK項目是什么關系?
A:SPDK 最早項目代號WaikikiBeach,全稱DPDK For Storage,2015年開源以后改為SPDK。SPDK 提供了一套環境抽象化庫 (位於lib/env目錄),主要用於管理SPDK存儲應用所使用的CPU資源、內存和PCIe等設備資源,其中DPDK是SPDK缺省的環境庫。每次當SPDK發布新版本的使用時會使用最新發布的DPDK的穩定版本。例如,SPDK 18.04會使用DPDK18.02版本。
03 SPDK的一些典型使用場景是什么?
A:目前而言,SPDK並不是一個存儲應用的通用適配解決方案。因為把內核驅動放到用戶態,所以導致需要在用戶態實施一套基於用戶態軟件驅動的完整I/O棧。文件系統毫無疑問是其中的一個重要話題,而內核的文件系統 (例如linux EXT4和btrfs等) 已經無法直接使用。雖然目前SPDK 提供了非常簡單的“文件系統”blobfs/blostore, 但並不支持POSIX接口。為此,如果要將使用文件系統的應用直接遷移到SPDK的用戶態“文件系統”上,還需要做一些代碼移植工作。例如,不使用POSIX接口,轉而采用類似於AIO的異步讀寫方式。SPDK社區一直朝着該方向努力,現如今SPDK在以下應用場景中的使用比較好:(1) 提供塊設備接口的后端存儲應用,諸如iSCSI Target和 NVMe-oF Target;(2) 對虛擬機中I/O (virtio) 的加速,主要支持Linux系統下的QEMU/KVM作為hypervisor 管理虛擬機的場景,使用vhost交互協議實現基於共享內存通道的高效vhost用戶態target (例如vhost SCSI/blk/NVMe target),從而加速虛擬機中virto SCSI/blk和kernel Native NVMe協議的I/O 驅動。其主要原理是減少了VM中斷等事件的數目(例如interrupt,和VM_EXIT),並縮短了host OS中的I/O棧;(3) SPDK加速數據庫存儲引擎。通過實現了RocksDB中的抽象文件類,SPDK的blobfs/blobstore目前可以和Rocksdb集成,用於在NVMe SSD上加速實現RocksDB引擎的使用。該操作的實質是bypass kernel文件系統將完全使用基於SPDK的用戶態I/O stack。此外,參照SPDK對Rocksdb的支持,亦可以用SPDK blobfs/blobstore 整合其他的數據庫存儲引擎。
SPDK源碼解讀1