spdk探祕-----基本框架及bdev範例分析

Spdk框架介紹

存儲性能開發工具包(SPDK)提供了一組工具和庫,用於編寫高性能,可伸縮的用戶模式存儲應用程序。它通過使用一些關鍵技術實現了高性能:

(1)、將所有必需的驅動程序移動到用戶空間,這樣可以避免系統調用並啓用應用程序的零拷貝訪問。

(2)、輪詢硬件用於完成而不是依賴中斷,這降低了總延遲和延遲差異。

(3)、避免I / O路徑中的加鎖方式來進行線程間的通信,而是依賴於消息傳遞。

SPDK的基石是用戶空間,輪詢模式,異步,無鎖NVMe驅動程序。這提供了從用戶空間應用程序直接到SSD的零拷貝,高度並行訪問。

SPDK提供了一個完整的塊堆棧作爲用戶空間庫,它執行許多與操作系統中的塊堆棧相同的操作。這包括統一不同存儲設備之間的接口,排隊以處理諸如內存不足或I / O掛起以及邏輯卷管理等情況。最後,SPDK提供基於這些組件構建的NVMe-oFiSCSIvhost服務器,這些服務器能夠通過網絡或其他進程提供磁盤。NVMe-oF和iSCSI的標準Linux內核啓動器與這些目標以及帶有vhost的QEMU互操作。

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路徑上使用自己獨立的資源就可以避免資源競爭,從而去除鎖的機制。

Spdk的主要構件如下圖:

 

驅動(Drivers)

NVMe Driver:SPDK的基礎組件,這個高優化無鎖的驅動有着高擴展性、高效性和高性能的特點。 

Intel QuickData Technology:也稱爲Intel I/O Acceleration Technology(Inter IOAT,英特爾I/O加速技術),這是一種基於Xeon處理器平臺上的copy offload引擎。通過提供用戶空間訪問,減少了DMA數據移動的閾值,允許對小尺寸I/O或NTB的更好利用。

NVMe over Fabrics(NVMe-oF)initiator:從程序員的角度來看,本地SPDK NVMe驅動和NVMe-oF啓動器共享一套共同的API命令。這意味着,例如本地/遠程複製將十分容易實現。

Storage Services(存儲設備)

Block device abstration layer(bdev):這種通用的塊設備抽象是連接到各種不同設備驅動和塊設備的存儲協議的粘合劑。並且還在塊層中提供靈活的API,用於額外的用戶功能,如磁盤陣列、壓縮、去冗等等。

Blobstore:爲SPDK實現一個高精簡的文件式語義(非POSIX)。這可以爲數據庫、容器、虛擬機或其他不依賴於大部分POSIX文件系統功能集(比如用戶訪問控制)的工作負載提供高性能基礎。

Blobstore Block Device:由SPDK Blobstore分配的塊設備,是虛擬機或數據庫可以與之交互的虛擬設備。這些設備得到SPDK基礎架構的優勢,意味着零拷貝和令人難以置信的可擴展性。

Logical Volume:類似於內核軟件棧中的邏輯卷管理,SPDK通過Blobstore的支持,同樣帶來了用戶態邏輯卷的支持,包括更高級的按需分配、快照、克隆等功能。

Ceph RADOS Block Device(RBD):使Ceph成爲SPDK的後端設備,比如這可能允許Ceph用作另一個存儲層。‍‍‍

Linux Asynchrounous I/O(AIO):允許SPDK與內核設備(比如機械硬盤)交互。

存儲協議(Storage Protocols)

iSCSI target:建立了通過以太網的塊流量規範,大約是內核LIO效率的兩倍。現在的版本默認使用內核TCP/IP協議棧,後期會加入對用戶態TCP/IP協議棧的集成。

NVMe-oF target:實現了NVMe-oF規範。將本地的高速設備通過網絡暴露出來,結合SPDK通用塊層和高效用戶態驅動,實現跨網絡環境下的豐富特性和高性能。支持的網絡不限於RDMA一種,FC,TCP等作爲Fabrics的不同實現,會陸續得到支持。

vhost target:KVM/QEMU的功能利用了SPDK NVMe驅動,使得訪客虛擬機訪問存儲設備時延遲更低,使得I/O密集型工作負載的整體CPU負載減低,支持不同的設備類型供虛擬機訪問,比如SCSI, Block, NVMe塊設備。

bdev實例分析

以examples/bdev/hell_word爲例

int main(int argc, char **argv)
{
	struct spdk_app_opts opts = {};
	int rc = 0;
	struct hello_context_t hello_context = {};

	//使用默認值初始化opts
	spdk_app_opts_init(&opts);
	opts.name = "hello_bdev";

	//這是沒有指定具體的bdev,使用默認的Malloc0
	if ((rc = spdk_app_parse_args(argc, argv, &opts, "b:", NULL, hello_bdev_parse_arg,
				      hello_bdev_usage)) != SPDK_APP_PARSE_ARGS_SUCCESS) {
		exit(rc);
	}
	if (opts.config_file == NULL) {
		SPDK_ERRLOG("configfile must be specified using -c <conffile> e.g. -c bdev.conf\n");
		exit(1);
	}
	hello_context.bdev_name = g_bdev_name;

	//通過spdk_app_start()庫會自動生成所有請求的線程,用戶的執行函數hello_start跑在該函數分配的線程上,直到應用程序通過調用spdk_app_stop()終止,或者在調用調用者提供的函數之前,在spdk_app_start()內的初始化代碼中發生錯誤情況。
	rc = spdk_app_start(&opts, hello_start, &hello_context);
	if (rc) {
		SPDK_ERRLOG("ERROR starting application\n");
	}

	//當應用程序停止時,釋放我們分配的內存
	spdk_dma_free(hello_context.buff);
	//關閉spdk子系統
	spdk_app_fini();
	return rc;
}

Hell_word的具體執行函數是在hello_start中,hello_start運行在spdk分配的線程上:

static void
hello_start(void *arg1)
{
	struct hello_context_t *hello_context = arg1;
	uint32_t blk_size, buf_align;
	int rc = 0;
	hello_context->bdev = NULL;
	hello_context->bdev_desc = NULL;

	SPDK_NOTICELOG("Successfully started the application\n");

	//根據bdev_name,這裏使用默認的Malloc0,獲取bdev
	hello_context->bdev = spdk_bdev_get_by_name(hello_context->bdev_name);
	if (hello_context->bdev == NULL) {
		SPDK_ERRLOG("Could not find the bdev: %s\n", hello_context->bdev_name);
		spdk_app_stop(-1);
		return;
	}

	//通過調用spdk_bdev_Open()打開bdev函數將返回一個描述符
	SPDK_NOTICELOG("Opening the bdev %s\n", hello_context->bdev_name);
	rc = spdk_bdev_open(hello_context->bdev, true, NULL, NULL, &hello_context->bdev_desc);
	if (rc) {
		SPDK_ERRLOG("Could not open bdev: %s\n", hello_context->bdev_name);
		spdk_app_stop(-1);
		return;
	}

	SPDK_NOTICELOG("Opening io channel\n");
	// 通過描述符獲取io channel
	hello_context->bdev_io_channel = spdk_bdev_get_io_channel(hello_context->bdev_desc);
	if (hello_context->bdev_io_channel == NULL) {
		SPDK_ERRLOG("Could not create bdev I/O channel!!\n");
		spdk_bdev_close(hello_context->bdev_desc);
		spdk_app_stop(-1);
		return;
	}

	//這裏是獲取bdev的塊大小和最小內存對齊,這是使用spdk_dma_zmalloc所要求的,當然了你也可以不用spdk_dma_zmalloc申請內存使用通用的內存申請方式。
	blk_size = spdk_bdev_get_block_size(hello_context->bdev);
	buf_align = spdk_bdev_get_buf_align(hello_context->bdev);
	hello_context->buff = spdk_dma_zmalloc(blk_size, buf_align, NULL);
	if (!hello_context->buff) {
		SPDK_ERRLOG("Failed to allocate buffer\n");
		spdk_put_io_channel(hello_context->bdev_io_channel);
		spdk_bdev_close(hello_context->bdev_desc);
		spdk_app_stop(-1);
		return;
	}
//初始化buf
	snprintf(hello_context->buff, blk_size, "%s", "Hello World!\n");
    //開始寫
	hello_write(hello_context);
}

開始寫操作

static void
hello_write(void *arg)
{
	struct hello_context_t *hello_context = arg;
	int rc = 0;
	uint32_t length = spdk_bdev_get_block_size(hello_context->bdev);
    //異步寫,寫完成後調用回調函數write_complete
	SPDK_NOTICELOG("Writing to the bdev\n");
	rc = spdk_bdev_write(hello_context->bdev_desc, hello_context->bdev_io_channel,
			     hello_context->buff, 0, length, write_complete, hello_context);

	if (rc == -ENOMEM) {
		SPDK_NOTICELOG("Queueing io\n");
		/* In case we cannot perform I/O now, queue I/O */
		hello_context->bdev_io_wait.bdev = hello_context->bdev;
		hello_context->bdev_io_wait.cb_fn = hello_write;
		hello_context->bdev_io_wait.cb_arg = hello_context;
		spdk_bdev_queue_io_wait(hello_context->bdev, hello_context->bdev_io_channel,
					&hello_context->bdev_io_wait);
	} else if (rc) {
		SPDK_ERRLOG("%s error while writing to bdev: %d\n", spdk_strerror(-rc), rc);
		spdk_put_io_channel(hello_context->bdev_io_channel);
		spdk_bdev_close(hello_context->bdev_desc);
		spdk_app_stop(-1);
	}
}

寫的回調函數

static void
write_complete(struct spdk_bdev_io *bdev_io, bool success, void *cb_arg)
{
	struct hello_context_t *hello_context = cb_arg;
	uint32_t length;

	//響應完成,用戶必須調用spdk_bdev_free_io()來釋放資源
	spdk_bdev_free_io(bdev_io);
    
	if (success) {
		SPDK_NOTICELOG("bdev io write completed successfully\n");
	} else {
		SPDK_ERRLOG("bdev io write error: %d\n", EIO);
		spdk_put_io_channel(hello_context->bdev_io_channel);
		spdk_bdev_close(hello_context->bdev_desc);
		spdk_app_stop(-1);
		return;
	}
   
	//初始化buf爲讀做準備
	length = spdk_bdev_get_block_size(hello_context->bdev);
	memset(hello_context->buff, 0, length);
//開始讀
	hello_read(hello_context);
}

讀操作

static void
hello_read(void *arg)
{
	struct hello_context_t *hello_context = arg;
	int rc = 0;
	uint32_t length = spdk_bdev_get_block_size(hello_context->bdev);
    //讀和寫差不多,也是異步執行,等待回調函數
	SPDK_NOTICELOG("Reading io\n");
	rc = spdk_bdev_read(hello_context->bdev_desc, hello_context->bdev_io_channel,
			    hello_context->buff, 0, length, read_complete, hello_context);

	if (rc == -ENOMEM) {
		SPDK_NOTICELOG("Queueing io\n");
		/* In case we cannot perform I/O now, queue I/O */
		hello_context->bdev_io_wait.bdev = hello_context->bdev;
		hello_context->bdev_io_wait.cb_fn = hello_read;
		hello_context->bdev_io_wait.cb_arg = hello_context;
		spdk_bdev_queue_io_wait(hello_context->bdev, hello_context->bdev_io_channel,
					&hello_context->bdev_io_wait);
	} else if (rc) {
		SPDK_ERRLOG("%s error while reading from bdev: %d\n", spdk_strerror(-rc), rc);
		spdk_put_io_channel(hello_context->bdev_io_channel);
		spdk_bdev_close(hello_context->bdev_desc);
		spdk_app_stop(-1);
	}
}

讀完成後回調函數被調用

static void
read_complete(struct spdk_bdev_io *bdev_io, bool success, void *cb_arg)
{
	struct hello_context_t *hello_context = cb_arg;

	if (success) {
		SPDK_NOTICELOG("Read string from bdev : %s\n", hello_context->buff);
	} else {
		SPDK_ERRLOG("bdev io read error\n");
	}

	//結束後關閉channel,釋放資源
	spdk_bdev_free_io(bdev_io);
	spdk_put_io_channel(hello_context->bdev_io_channel);
	spdk_bdev_close(hello_context->bdev_desc);
	SPDK_NOTICELOG("Stopping app\n");
	spdk_app_stop(success ? 0 : -1);
}

spdk用起來還是非常方便de 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章