1. 程式人生 > >淺談NVMe的多佇列技術和IO排程

淺談NVMe的多佇列技術和IO排程

crystalwit

NVMe標準中的諸多技術中,多佇列技術是其中一個重要的提高效能的方法。藉助於多佇列技術,NVMe實現了按照任務、排程優先順序和CPU的核來分配不同佇列,完成高效能的儲存功能。

每個佇列是一個先進先出的FIFO管道,用於連通主機端(Host)和裝置端(Device)。其中從主機端傳送到裝置端的命令管道稱之為傳送佇列;從裝置端傳送到主機端的命令完成管道稱之為完成佇列。對於一個IO請求,在主機端組裝完成後,通過傳送佇列發到裝置端,然後在裝置中進行處理並把相應的完成結果組裝成IO完成請求,最後通過完成佇列返還給主機端。

不管是傳送佇列還是完成佇列,都是一段記憶體,通常位於主機端的

DDR空間裡。這段記憶體劃分成若干等長的記憶體塊,每一塊用於儲存一個定常的訊息(NVMe的傳送訊息和完成訊息都是定常的)。在使用的時候,對於這個佇列,有一個頭指標和一個尾指標。當兩者相等時,佇列是空的。見下圖。

隨著新的訊息加入到佇列中來,尾指標不停向前移動。因為記憶體是定常的,因此指標一旦移動到隊記憶體的最後一個儲存空間,之後再移動的話需要環回到記憶體的起始位置。因此記憶體在使用上實際上當作一個環來迴圈使用。當尾指標的下一個指標就是頭指標的時候,這個佇列不能再接收新的訊息,即佇列已經滿了,如下圖所示。


隨著佇列的使用者不斷取出訊息並修改頭指標,佇列中的元素不斷釋放,一直到頭指標再次追上尾指標時,佇列完全變空。

那麼主機端將資料寫入佇列後,裝置端是怎麼知道該佇列所在的記憶體已經更新了呢?這就需要利用門鈴機制(Doorbell)。每個佇列都有一個門鈴指標。對於傳送佇列來說,這個指標表示的是傳送佇列的尾指標。主機端將資料寫入到傳送佇列後,更新對映到位於裝置暫存器空間中的門鈴的尾指標。實現在SoC控制器晶片上的尾指標一旦被更新,裝置就知道新資料到了。這裡並未涉及到主機端如何知道資料已經取走並且裝置已經更新了頭指標了。NVMe協議並沒有採用傳統的查詢暫存器的方式來讓主機獲得這個資訊,因為這樣勢必造成CPU與硬體暫存器的互動。對於x86來說,每一次與硬體的互動都會帶來效能的損失,因此降低硬體互動尤為重要。NVMe的方案是對於這個傳送訊息,在當它完成的時候會將完成的結果通過

DMA的方式寫入到記憶體中,主機根據每個IO請求及其完成請求中的CommandIdentifier (CID)欄位來匹配相應的傳送請求和完成請求。其中完成結果中攜帶有資訊表明最新的該請求所對應的傳送佇列的當前頭指標。

反過來,當裝置端完成一個NVMe請求時,也需要通過完成佇列來把完成的結果告知主機端,這是通過完成佇列來實現的。與傳送佇列不同,完成佇列是通過中斷機制(INTxMSIMSIx)告訴接收端(主機CPU)收到了新的完成訊息並安排後續處理。其中MSIMSIx因為可以關聯到具體的CPU核,所以通常建議使用MSIMSIx型別的中斷,並將中斷關聯到每一個完成佇列。這樣當中斷產生的時候,根據中斷向量可以確定是哪一個完成佇列有新的完成訊息。同樣的,為了確定完成佇列裡到底有多少是新的完成訊息,在每一個完成請求中,有一個標誌位PhaseTag,這個標誌位每次寫入的數值都會發生改變,並據此確定每一個完成請求是否是新的完成請求。通過這種機制,雖然主機端不能一下子確定到底有多少新的完成請求,但是可以逐漸的、一步步完成所有的完成請求,並將完成佇列用空。隨著主機逐漸從完成佇列裡取出完成訊息,主機會更新位於裝置上的完成佇列頭指標暫存器,告訴裝置完成佇列的實施狀況。

在最新的NVMe1.2A中,每一個NVMeController允許最多65535IO佇列和一個Admin佇列。Admin佇列在裝置初始化之後隨即建立,包括一個傳送佇列和一個完成佇列。其他的IO佇列則是由Admin佇列中傳送的控制命令來產生的。NVMe規定的IO佇列的關係比較靈活,既可以一個傳送佇列對應一個完成佇列,也可以幾個傳送佇列共同對應一個完成佇列。在主流的實現中,較多采用了一對一的方式。下圖列舉了兩種方式的示意:

 

 為了更好的說明這種設計如何被使用,我們拿NVMe Linux inbox driver作為一個例子來分析一下。這個驅動程式在核心版本3.3(RHEL7)中加入,最新的程式碼支援NVMe 1.0c。主要的程式碼位於/drivers/nvme/host/目錄下。

在pci.c中,nvme_setup_io_queues負責初始化佇列,它先查詢了到底有多少個CPU,然後再呼叫nvme_set_queue_count發命令給裝置,讓裝置按照CPU的個數來設定佇列的個數。

nvme_set_queue_count中,按照之前傳入的CPU數量來設定裝置的能力。其中NVME_FEAT_NUM_QUEUES對應於NVMe協議的Number of Queues (Feature Identifier 07h)。當然,根據裝置能力不同,如果不巧裝置剛好沒辦法支援這麼多佇列的話,驅動程式也會做一些取捨,選取裝置的能力和CPU數量中較小的值。

之所以驅動程式主動去申請佇列數量,而不是在NVMe Create Queue時逐個獲得佇列資源,主要是出於虛擬化的考慮(包括NVMf)。在虛擬化環境中,每個虛機所使用的佇列資源在整個裝置中是共享的,因此在Set Feature時設定有利於裝置優化資源使用。

隨後,在MQ初始化的地方,將MQ的數量設定為NVMe佇列的數量。並且把每一個IO queue關聯到一個NVMe佇列上下文。

藉助於Linux MQ,最終實現的效果是每一個CPU儘可能關聯到一個獨立的NVMe佇列,從而實現操作IO時CPU之間不需要相互通訊。因為每一個NVMe佇列對應的中斷也是獨立的,因此從傳送命令一直到接收請求的執行結果,全面實現了CPU之間的隔離,避免了鎖帶來的開銷。

NVMe對於使用不同的佇列還定義了排程策略,用於區分不同優先順序的任務。其中Admin佇列的優先順序是最高的,IO佇列可以定義絕對的優先順序或Weighted round robin(WRR)的排程策略。Intel最新發布的DC P3700 SSD包括了這個功能,先於主流的驅動程式支援WRR。


從生態系統的反饋來看,目前這個功能在使用上還有一定障礙。一方面從剛才的Linux源生驅動來看,MQ保證了每個CPU獨佔一個佇列。但是如果支援按照佇列的優先順序來進行排程的話,則是需要按照應用層面的任務來劃分佇列,這兩個需求是矛盾的;另一方面,無論是絕對的優先順序,還是WRR都不是符合使用者直覺特別是DBA使用習慣的方式。因此這一塊後續的發展還需要NVMe組織和生態圈的共同努力。

[1] NVM Express Revision 1.2a