一 簡介前端
用戶對超高併發、超大規模計算等需求推進了存儲硬件技術的不斷髮展,存儲集羣的性能愈來愈好,延時也愈來愈低,對總體IO路徑的性能要求也愈來愈高。在雲硬盤場景中,IO請求從生成到後端的存儲集羣再到返回之間的IO路徑比較複雜,虛擬化IO路徑尤爲可能成爲性能瓶頸,由於虛機內全部的IO都須要經過它下發給後端的存儲系統。咱們使用了SPDK來優化虛擬化IO路徑,提出了開源未解決的SDPK熱升級和在線遷移方案,而且在高性能雲盤場景中成功應用,取得了不錯的效果,RSSD雲硬盤最高可達120萬IOPS。本文主要分享咱們在這方面的一些經驗。後端
二 SPDK vhost的基本原理數組
SPDK(Storage Performance Development Kit )提供了一組用於編寫高性能、可伸縮、用戶態存儲應用程序的工具和庫,基本組成分爲用戶態、輪詢、異步、無鎖 NVMe 驅動,提供了從用戶空間應用程序直接訪問SSD的零拷貝、高度並行的訪問。網絡
在虛擬化IO路徑中,virtio是比較經常使用的一種半虛擬化解決方案,而virtio底層是經過vring來通訊,下面先介紹下virtio vring的基本原理,每一個virtio vring 主要包含了如下幾個部分:架構
desc table數組,該數組的大小等於設備的隊列深度,通常爲128。數組中每個元素表示一個IO請求,元素中會包含指針指向保存IO數據的內存地址、IO的長度等基本信息。通常一個IO請求對應一個desc數組元素,固然也有IO涉及到多個內存頁的,那麼就須要多個desc連成鏈表來使用,未使用的desc元素會經過自身的next指針鏈接到free_head中,造成一個鏈表,以供後續使用。併發
available數組,該數組是一個循環數組,每一項表示一個desc數組的索引,當處理IO請求時,從該數組裏拿到一個索引就能夠到desc數組裏面找到對應的IO請求了。異步
used 數組,該數組與avail相似,只不過用來表示完成的IO請求。當一個IO請求處理完成時,該請求的desc數組索引就會保存在該數組中,而前端virtio驅動獲得通知後就會掃描該數據判斷是否有請求完成,若是完成就會回收該請求對應的desc數組項以便下個IO請求使用。async
SPDK vhost的原理比較簡單,初始化時先由qemu的vhost驅動將以上virtio vring數組的信息發送給SPDK,而後SPDK經過不停的輪尋available數組來判斷是否有IO請求,有請求就處理,處理完後將索引添加到used數組中,並經過相應的eventfd通知virtio前端。函數
當SPDK收到一個IO請求時,只是指向該請求的指針,在處理時須要能直接訪問這部份內存,而指針指向的地址是qemu地址空間的,顯然不能直接使用,所以這裏須要作一些轉化。高併發
在使用SPDK時虛機要使用大頁內存,虛機在初始化時會將大頁內存的信息發送給SPDK,SPDK會解析該信息並經過mmap映射一樣的大頁內存到本身的地址空間,這樣就實現了內存的共享,因此當SPDK拿到qemu地址空間的指針時,經過計算偏移就能夠很方便的將該指針轉換到SPDK的地址空間。
由上述原理咱們能夠知道SPDK vhost經過共享大頁內存的方式使得IO請求能夠在二者之間快速傳遞這個過程當中不須要作內存拷貝,徹底是指針的傳遞,所以極大提高了IO路徑的性能。
咱們對比了原先使用的qemu雲盤驅動的延時和使用了SPDK vhost以後的延時,爲了單純對比虛擬化IO路徑的性能,咱們採用了收到IO後直接返回的方式:
1.單隊列(1 iodepth, 1 numjob)
qemu 網盤驅動延時:
SPDK vhost延時:
可見在單隊列狀況下延時降低的很是明顯,平均延時由原來的130us降低到了7.3us。
2.多隊列(128 iodepth,1 numjob)
qemu 網盤驅動延時:
SPDK vhost延時:
多隊列時IO延時通常會比單隊列更大些,可見在多隊列場景下平均延時也由3341us降低爲1090us,降低爲原來的三分之一。
三 SPDK熱升級
在咱們剛開始使用SPDK時,發現SPDK缺乏一重要功能——熱升級。咱們使用SPDK 並基於SPDK開發自定義的bdev設備確定會涉及到版本升級,而且也不能100%保證SPDK進程不會crash掉,所以一旦後端SPDK重啓或者crash,前端qemu裏IO就會卡住,即便SPDK重啓後也沒法恢復。
咱們仔細研究了SPDK的初始化過程發現,在SPDK vhost啓動初期,qemu會下發一些配置信息,而SPDK重啓後這些配置信息都丟失了,那麼這是否意味着只要SPDK重啓後從新下發這些配置信息就能使SPDK正常工做呢?咱們嘗試在qemu中添加了自動重連的機制,而且一旦自動重連完成,就會按照初始化的順序再次下發這些配置信息。開發完成後,初步測試發現確實可以自動恢復,但隨着更嚴格的壓測發現只有在SPDK正常退出時才能恢復,而SPDK crash退出後IO仍是會卡住沒法恢復。從現象上看應該是部分IO沒有被處理,因此qemu端虛機一直在等待這些IO返回致使的。
經過深刻研究virtio vring的機制咱們發如今SPDK正常退出時,會保證全部的IO都已經處理完成並返回了才退出,也就是所在的virtio vring中是乾淨的。而在乎外crash時是不能作這個保證的,意外crash時virtio vring中還有部分IO是沒有被處理的,因此在SPDK恢復後須要掃描virtio vring將未處理的請求下發下去。這個問題的複雜之處在於,virtio vring中的請求是按順序下發處理的,但實際完成的時候並非按照下發的順序的。
假設在virtio vring的available ring中有6個IO,索引號爲1,2,3,4,5,6,SPDK按順序的依次獲得這個幾個IO,並同時下發給設備處理,但實際可能請求1和4已經完成,並返回了成功了,以下圖所示,而2,3,5,6都尚未完成。這個時候若是crash,重啓後須要將2,3,5,6這個四個IO從新下發處理,而1和4是不能再次處理的,由於已經處理完成返回了,對應的內存也可能已經被釋放。也就是說咱們沒法經過簡單的掃描available ring來判斷哪些IO須要從新下發,咱們須要有一塊內存來記錄virtio vring中各個請求的狀態,當重啓後可以按照該內存中記錄的狀態來決定哪些IO是須要從新下發處理的,並且這塊內存不能因SPDK重啓而丟失,那麼顯然使用qemu進程的內存是最合適的。因此咱們在qemu中針對每一個virtio vring申請一塊共享內存,在初始化時發送給SPDK,SPDK在處理IO時會在該內存中記錄每一個virtio vring請求的狀態,並在乎外crash恢復後能利用該信息找出須要從新下發的請求。
四 SPDK在線遷移
SPDK vhost所提供的虛擬化IO路徑性能很是好,那麼咱們有沒有可能使用該IO路徑來代替原有的虛擬化IO路徑呢?咱們作了一些調研,SPDK在部分功能上並無現有的qemu IO路徑完善,其中尤其重要的是在線遷移功能,該功能的缺失是咱們使用SPDK vhost代替原有IO路徑的最大障礙。
SPDK在設計時更可能是爲網絡存儲準備的,因此支持設備狀態的遷移,但並不支持設備上數據的在線遷移。而qemu自己是支持在線遷移的,包括設備狀態和設備上的數據的在線遷移,但在使用vhost模式時是不支持在線遷移的。主要緣由是使用了vhost以後qemu只控制了設備的控制鏈路,而設備的數據鏈路已經託管給了後端的SPDK,也就是說qemu沒有設備的數據流IO路徑因此並不知道一個設備那些部分被寫入了。
在考察了現有的qemu在線遷移功能後,咱們覺着這個技術難點並非不能解決的,所以咱們決定在qemu裏開發一套針對vhost存儲設備的在線遷移功能。
塊設備的在線遷移的原理比較簡單,能夠分爲兩個步驟,第一個步驟將全盤數據從頭至尾拷貝到目標虛機,由於拷貝過程時間較長,確定會發生已經拷貝的數據又被再次寫入的狀況,這個步驟中那些再次被寫髒的數據塊會在bitmap中被置位,留給第二個步驟來處理,步驟二中經過bitmap來找到那些剩餘的髒數據塊,將這些髒數據塊發送到目標端,最後會block住全部的IO,而後將剩餘的一點髒數據塊同步到目標端遷移就完成了。
SPDK的在線遷移原理上於上面是相同的,複雜之處在於qemu沒有數據的流IO路徑,因此咱們在qemu中開發了一套驅動能夠用來實現遷移專用的數據流IO路徑,而且經過共享內存加進程間互斥的方式在qemu和SPDK之間建立了一塊bitmap用來保存塊設備的髒頁數量。考慮到SPDK是獨立的進程可能會出現意外crash的狀況,所以咱們給使用的pthread mutex加上了PTHREAD_MUTEX_ROBUST特性來防止意外crash後死鎖的狀況發生,總體架構以下圖所示:
五 SPDK IO uring體驗
IO uring是內核中比較新的技術,在上游內核5.1以上才合入,該技術主要是經過用戶態和內核態共享內存的方式來優化現有的aio系列系統調用,使得提交IO不須要每次都進行系統調用,這樣減小了系統調用的開銷,從而提供了更高的性能。
SPDK在最新發布的19.04版本已經包含了支持uring的bdev,但該功能只是添加了代碼,並無開放出來,固然咱們能夠經過修改SPDK代碼來體驗該功能。
首先新版本SPDK中只是包含了io uring的代碼甚至默認都沒有開放編譯,咱們須要作些修改:
1.安裝最新的liburing庫,同時修改spdk的config文件打開io uring的編譯;
2.參考其餘bdev的實現,添加針對io uring設備的rpc調用,使得咱們能夠像建立其餘bdev設備那樣建立出io uring的設備;
3.最新的liburing已經將io_uring_get_completion調用改爲了io_uring_peek_cqe,並須要配合io_uring_cqe_seen使用,因此咱們也要調整下SPDK中io uring的代碼實現,避免編譯時出現找不到io_uring_get_completion函數的錯誤:
4.使用修改open調用,使用O_SYNC模式打開文件,確保咱們在數據寫入返回時就落地了,而且比調用fdatasync效率更高,咱們對aio bdev也作了一樣的修改,同時添加讀寫模式:
通過上述修改spdk io uring設備就能夠成功建立出來了,咱們作下性能的對比:
使用aio bdev的時候:
使用io uring bdev的時候:
可見在最高性能和延時上 io uring都有不錯的優點,IOPS提高了約20%,延遲下降約10%。這個結果其實受到了底層硬件設備最大性能的限制,還未達到io uring的上限。
六 總結
SPDK技術的應用使得虛擬化IO路徑的性能提高再也不存在瓶頸,也促使UCloud高性能雲盤產品能夠更好的發揮出後端存儲的性能。固然一項技術的應用並無那麼順利,咱們在使用SPDK的過程當中也遇到了許多問題,除了上述分享的還有一些bug修復等咱們也都已經提交給了SPDK社區,SPDK做爲一個快速發展迭代的項目,每一個版本都會給咱們帶來驚喜,裏面也有不少有意思的功能等待咱們發掘並進一步運用到雲盤及其它產品性能的提高上。