spdk探祕-----reduce介紹

SPDK的reduce塊壓縮方案基於使用ssd存儲的壓縮塊,如果不是ssd磁盤也沒必要使用壓縮功能。壓縮過程會產生元數據,元數據也需要持久化保存。該元數據用於記錄邏輯空間到ssd盤上存儲壓縮數據的映射。數據壓縮功能對外體現爲一個壓縮的塊設備bdev,該bdev和一般的bdev用法並無二致,用戶的io通過這個壓縮bdev後數據會被壓縮,然後寫入後端存儲設備中。

壓縮bdev的後存儲必須是支持精簡配置的存儲設備,如果不支持精簡配置即使經過壓縮效果也不會很明顯

後端存儲設備的大小必須適合最壞的情況,即沒有數據可以壓縮。在這種情況下,後備存儲設備的大小將與壓縮塊設備相同。塊設備後端一般是使用虛化的存儲池,存儲池內資源實現共享,資源池可以是傳統的san形式或者分佈式資源池,該數據壓縮算法爲了保證原子性不會覆蓋寫數據,會不停的向後追加寫。這種技術已經普遍應用於存儲中。另外在更新關聯的元數據之前,需要一些額外的後端存儲來臨時存儲要進行寫入的數據

爲了獲得最佳的NVMe性能,從後端存儲設備將以4KB爲粒度進行分配、讀取和寫入數據。這些4KB的單元稱爲“後端IO單元”。它們的索引從0到N-1,索引稱爲“後端IO單元索引”。

壓縮塊設備bdev以chunk爲單位壓縮和解壓數據,chunk是至少兩個後端IO單元的倍數。每個chunk內後端IO單元數決定了塊大小,這是在創建壓縮塊設備時指定。一個塊消耗的後端IO單元數量介於1和塊中總共的io單元數之間。例如,一個16KB的chunk可能消耗1、2、3或4個後端IO單元。消耗IO單元的數量取決於chunk能夠被壓縮的程度。chunk和其相關聯的磁盤塊的映射關係存儲在元數據中。每個chunk映射由N個64位值組成,其中N是chunk中支持IO單元的最大數量。每個64位值對應於一個後端IO單元索引。特殊值(例如2^64-1)表明後端存儲io單元沒有壓縮。分配的chunk映射數等於壓縮塊設備的大小除以它的chunk大小,再加上一些額外的chunk映射數。這些額外的chunk映射用於確保寫操作的原子性,一開始,所有的chunk映射表示“空閒chunk映射列表”。

最後,壓縮塊設備的邏輯視圖由“邏輯映射”表示。邏輯映射是將壓縮塊設備中的塊偏移量映射到相應的chunk映射。邏輯映射中的每個條目都是一個64位值,表示關聯的chunk映射。如果沒有關聯的chunk映射,則使用一個特殊值(UINT64_MAX)。通過將字節偏移量除以chunk大小來獲得索引來確定映射,該索引用作chunk映射項數組的數組索引。一開始,邏輯映射中的所有條目都沒有關聯的塊映射。注意,雖然對後端存儲設備的訪問以4KB單元爲粒度,但是邏輯視圖可能允許4KB或512字節的訪問。

爲了說明這個算法,我們將使用一個實際的例子在一個非常小的規模。

壓縮塊設備的大小爲64KB,chunk大小爲16KB。這將實現下列目標:

1、後端存儲由一個80KB的精簡配置邏輯卷組成。這相當於64KB的壓縮塊設備在最壞的壓縮情況下,還需要額外的16KB來處理額外的寫操作。

2、“空閒IO單元列表”將由索引0到19(包括)組成。這些代表了後端存儲器中的20個4KB 的IO單元。

3、“chunk映射”的大小爲32字節。每個塊有4個IO單元(16KB / 4KB),每個IO單元索引有8B (64b)。

4、5個chunk映射佔用160B的內存。這對應於壓縮塊設備中的4個chunk的4個chunk映射(64KB / 16KB),加上一個額外的塊映射,用於覆蓋現有塊。

5、“空閒chunk映射鏈表”將由索引0到4(包括)組成。它們表示分配的5個chunk映射。

6、“邏輯映射”將佔用32B的內存中。這對應於壓縮塊設備中chunk的4個條目,每個條目8B (64b)。

在這些示例中,值“X”表示上面描述的特殊值(2^64-1)。

初始狀態:

Write 16KB at Offset 32KB

1、在邏輯映射中找到對應的索引。偏移量32KB除以chunk大小(16KB)等於2。

2、邏輯映射中的第2項是“X”。這意味着這16KB還沒有被寫入。

3、在內存中分配一個16KB的緩衝區

4、將傳入的16KB數據壓縮到這個分配的緩衝區中

5、假設該數據壓縮到6KB。這需要2個4KB的後備IO單元。

6、從空閒的IO單元列表中分配2個io單元(0和1)。始終使用空閒的IO單元列表中編號最低的條目。

7、將6KB的數據寫入備份IO單元0和1。

8、從空閒chunk映射列表中分配一個chunk映射(0)。

9、將(0,1,X, X)寫入chunk映射。這表示只有2個後備IO單元用於存儲16KB的數據。

10、將chunk映射索引寫入邏輯映射中的條目2。

Write 4KB at Offset 8KB

1、在邏輯映射中找到對應的索引。偏移量8KB除以chunk大小爲0。

2、邏輯映射中的第0項是“X”。這意味着這16KB還沒有被寫入。

3、寫操作不是針對整個16KB塊,我們一樣也要爲源數據分配一個16KB塊大小的緩衝區。

4、將傳入的4KB數據複製到這個16KB緩衝區的8KB偏移量。將剩餘的緩衝區歸零。

5、分配一個16KB的目標緩衝區。

6、將16KB的源數據緩衝區壓縮到16KB的目標緩衝區

7、假設該數據壓縮到3KB。這需要1個 4KB的IO單元。

8、從空閒的IO單元列表中分配1個(2)。

9、將3KB的數據寫入IO單元。

10、從空閒chunk映射列表中分配一個塊映射(1)。

11、寫(X,X,2, X)到chunk映射。

12、將chunk映射索引寫入邏輯映射中的條目0。

Read 16KB at Offset 16KB

1、偏移量16KB映射到邏輯映射中的索引1。

2、邏輯映射中的條目1是“X”。這意味着這16KB還沒有被寫入。

3、由於沒有數據被寫入這個chunk,所以返回所有的0來滿足讀取的I/O。

Write 4KB at Offset 4KB

1、偏移量4KB映射到邏輯映射中的索引0。

2、邏輯映射中的條目0是“1”。因爲本次寫沒有覆蓋整個chunk,所以執行讀改寫操作。

3、塊映射1僅有一個IO單元(2)。分配一個16KB的緩衝區並將塊2讀入其中。請注意,分配的是16KB而不是4KB,這樣我們就可以重用這個緩衝區來保存稍後將寫入磁盤的壓縮數據。

4、爲此塊的未壓縮數據分配16KB緩衝區。將壓縮數據緩衝區中的數據解壓縮到此緩衝區中。

5、將傳入的4KB數據複製到未壓縮數據緩衝區的4KB偏移量。

6、將16KB的未壓縮數據緩衝區壓縮到壓縮數據緩衝區中。

7、假設這個數據壓縮到5KB。這需要2個4KB的IO單元。

8、從空閒IO單元列表中分配塊3和4。

9、將5KB的數據寫入塊3和塊4。

10、從空閒塊映射列表中分配chunk映射2。

11、將(3、4、X、X)寫入chunk映射2。注意,此時邏輯映射不引用chunk映射。如果此時出現電源故障,則此chunk的先前數據仍然完全有效。

12、將chunk映射2寫入邏輯映射中的條目0。

13、chunk映射1返回到空閒chunk映射列表。

14、空閒IO單元2返回到空閒IO單元列表。

跨多個chunk的情況

跨越chunk邊界的操作在邏輯上被分割爲多個操作,每個操作都與單個chunk關聯。

示例:20KB寫入,偏移量爲4KB

在這種情況下,寫操作被分割爲一個位於4KB偏移位置的12KB寫操作(隻影響邏輯映射中的chunk 0)和一個位於16KB偏移位置的8KB寫操作(隻影響邏輯映射中的chunk 1)。每個寫操作都使用上述算法獨立處理。直到兩個操作都完成了,纔會完成20KB的寫操作。

Unmap

對整chunk的取消映射操作是通過從邏輯映射中刪除chunk映射條目(如果有的話)來實現的。chunk映射返回到空閒chunk映射列表,與chunk映射關聯的任何IO單元返回到空閒IO單元列表。

隻影響chunk的一部分的取消映射操作可以被視爲向chunk的那個區域寫入0。如果通過多個操作取消映射整個chunk,則可以通過未壓縮的全等於零進行檢測。發生這種情況時,可能會從邏輯映射中刪除chunk映射條目。

在未映射整個chunk之後,對chunk的後續讀將返回0。這類似於上面的“以16KB偏移量讀取16KB”示例。

Write Zeroes Operations

寫零操作的處理方式與unmap操作類似。如果寫零操作覆蓋整個chunk,我們可以在邏輯映射中完全刪除chunk的條目。然後對該chunk的後續讀返回0。

Restart

使用libreduce的應用程序重新啓動時,它將重新加載壓縮卷。

當壓縮卷被重新加載時,通過遍歷邏輯映射重新構建空閒chunk映射列表和空閒IO單元列表。邏輯映射將只指向有效的chunk映射,並且有效chunk映射將只指向有效的IO單元。任何未引用的chunk映射和IO單元都進入各自的空閒列表。

這確保瞭如果系統在寫操作的中間崩潰——即在chunk映射更新期間或之後,但在它被寫入邏輯映射之前可以保證數據一致性。

Chunk上的併發處理

實現必須小心處理同一chunk上的重疊操作。例如,操作1向chunk A寫入一些數據,同時操作2也向chunk A寫入一些數據。在這種情況下,操作2應該在操作1完成後纔開始。

精簡卷

後端存儲必須是精簡卷以實現壓縮。這個算法將總是使用在後端存儲設備上距離偏移量0最近的IO單元。這確保了即使後備存儲設備的大小可能與壓縮卷的大小類似,後端存儲設備的存儲空間實際上不會分配,直到真正需要IO單元

壓縮源碼分析

首先是創建壓縮塊設備,壓縮塊設備是在bdev的設備上在套了一層殼,壓縮bdev的後端可能是ssd或者其他bdev設備例如iscsidev等。

創建壓縮bdev的大概流程

create_compress_bdev--à vbdev_init_reduce--à spdk_reduce_vol_init--àspdk_reduce_vol_init--à_comp_reduce_writev--à _init_write_path_cpl--à_init_write_super_cpl--àvbdev_reduce_init_cb--àvbdev_compress_claim--àspdk_bdev_register

這裏只列出了主要的函數,需要重點說明下在vbdev_compress_claim中對封裝的comp_bdev塊設備設置執行函數fn_table等,如果對內核通用塊層熟悉的話很容易就發現這個vbdev_compress_claim函數的功能非常類似內核通用塊層如何註冊塊設備的操作。不熟悉也不要緊,這些都是套路,按照固定套路來就可以了。

數據壓縮

先看下數據壓縮的主要函數流程:

vbdev_compress_submit_request--à_comp_bdev_io_submit--àspdk_reduce_vol_writev--à_start_writev_request--à_reduce_vol_compress_chunk--à_write_compress_done-à_reduce_vol_write_chunk--à_issue_backing_ops--à_comp_reduce_writev

在_reduce_vol_compress_chunk函數裏面真正開始壓縮,該函數也是調用了之前後端塊設備上註冊的壓縮函數。

static void
_reduce_vol_compress_chunk(struct spdk_reduce_vol_request *req, reduce_request_fn next_fn)
{
	struct spdk_reduce_vol *vol = req->vol;

	req->backing_cb_args.cb_fn = next_fn;
	req->backing_cb_args.cb_arg = req;
	req->comp_buf_iov[0].iov_base = req->comp_buf;
	req->comp_buf_iov[0].iov_len = vol->params.chunk_size;
	req->decomp_buf_iov[0].iov_base = req->decomp_buf;
	req->decomp_buf_iov[0].iov_len = vol->params.chunk_size;
	vol->backing_dev->compress(vol->backing_dev,
				   req->decomp_buf_iov, 1, req->comp_buf_iov, 1,
				   &req->backing_cb_args);
}

static void
_comp_reduce_compress(struct spdk_reduce_backing_dev *dev,
		      struct iovec *src_iovs, int src_iovcnt,
		      struct iovec *dst_iovs, int dst_iovcnt,
		      struct spdk_reduce_vol_cb_args *cb_arg)
{
	int rc;

	rc = _compress_operation(dev, src_iovs, src_iovcnt, dst_iovs, dst_iovcnt, true, cb_arg);
	if (rc) {
		SPDK_ERRLOG("with compress operation code %d (%s)\n", rc, spdk_strerror(-rc));
		cb_arg->cb_fn(cb_arg->cb_arg, rc);
	}
}

調用壓縮接口,真正處理壓縮、解壓縮是DPDK的compressdev。總的來說spdk的壓縮算法算是中規中矩,壓縮後的數據按照4K對齊。現在市面上的很多存儲產品中的壓縮是非對齊存放,壓縮後的數據長度是多長就佔用多少物理空間。

 

 

 

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