【前言】
這是一次充滿曲折與反轉的問題分析,資料很少,程式碼很多,經驗很少,概念很多,當核心態,使用者態,DIF,LBA,大頁記憶體,SGL,RDMA,NVME和SSD一起迎面而來的時候,問題是單點的意外,還是群體的無奈?
為了加深記憶,也為了分享出來給人以啟示,特記錄這次問題分析過程。
【現象】
同事L在專案中需要使用NVMF寫盤,發現寫盤失敗,瘋狂列印錯誤碼:
圖片中雖然擷取的比較少,但實際是瘋狂的一直列印。
故障現象簡要描述一下就是:
通過NVMF寫盤失敗,瘋狂列印錯誤碼15;
作為對照,通過本地寫盤,一切正常。
注:這裡的盤,都是指SSD盤。目前實驗室使用的型號是公司V3版本(HWE3xxx)。
【分析】
在這裡把涉及到的一些基本縮略語都記錄一下:
習慣了縮略語作為名詞後,總是容易忽略其背後更多的含義,問題的分析,需要對這些有更深的理解,最初對這些理解不深,對資料處理流程不清晰,起步很艱難。
分析步驟(一)
在下發IO時,通過變換IO的大小,佇列深度,發現資料量較小時,則幾乎沒有問題,直接下發1M大小IO時,則必現。
因此,可以明顯的推測出IO的大小與問題的出現緊密相關。
直接執行業務來驗證問題,過於笨重了,而且非常麻煩,將問題直接簡化為,一個服務端和一個請求端,發現均能穩定復現,他們分別是:
1. 執行SPDK自帶的app,nvmf_tgt程式,這個就是NVMF的服務端了;
- 進入spdk目錄後,配置好2M大頁;
- 配置好nvmf.conf 配置檔案,假設檔案放在/opt/yy目錄下;配置檔案參考附錄;
- 執行./app/nvmf_tgt/nvmf_tgt -c /opt/yy/nvmf.conf;
2. 可以使用兩種模式的請求端,
- 一種是SPDK自帶的perf程式,路徑是./examples/nvme/perf/perf,會配置必要的引數; 注意:系統也自帶一個perf,不是系統自帶的那一個; Perf是一個測試工具,會隨機產生資料大量寫入,可以驗證問題修復性,但不利於問題最初的分析;
- 一種是自已改造nvme目錄下的helloworld程式(初始版本,由同事C提供,後來經過了一些改良,後續稱為DEMO程式); 程式碼見附錄;
因為都是執行在使用者態,所以開啟除錯還是很方便的。兩端同時開啟除錯模式,進行單步跟蹤,發現錯誤碼是在非同步模式下輪循得到,如圖
函式名稱已經告知,是處理完成的結果;
呼叫是來自於這裡,383行:
在303行下斷點,根據棧資訊(沒有有效資訊,略)看,錯誤碼可能來自於SPDK的某個非同步呼叫,也可能來自於裝置,查遍SPDK程式碼,發現根本沒有15這個錯誤碼的設定,基本推導為是由SSD返回的。
根據最初的資訊可知,IO的資料量大小會影響問題出現,IO資料量較小時不會出現,那麼分界點在哪裡呢?
採用二分法在DEMO程式上嘗試,發現LBA的個數為15時,是分界點。
那麼,怎麼用起來呢?
單步跟蹤,有一個引數進入視野,名稱空間(NVME的協議規範吧,一塊SSD下有一個控制,有若干個名稱空間)的sectors_per_max_io引數。
修改這個引數,可以控制最後寫盤時的大小,在DEMO程式上試驗,問題消失。
但是當IO大小與深度較大,要麼出現記憶體不足錯誤碼,要麼錯誤依然出現,另外多盤場景下非常容易再現。
給出有條件解決辦法1:
(1) 修改如上位置;
(2) 業務下發時要求對IO的大小和下發的盤數進行限定;
實際使用時,因為必需多盤,要改造成單盤,非常困難,不是理想的解決方案。
另外還發現不同版本的盤,最小適配值不一樣,最安全值是7,但是後來主要選取一塊15為安全線的盤來分析問題使用。
分析步驟(二)
為了快速解決問題,開始嘗試廣泛求助,這麼明顯的問題,別人有沒有遇到?
在遍訪hi3ms和搜遍google,以及請教相關可以找到的同事,嘿,還真沒有第二例!
而且更為奇怪的是,在Intel的基線報告中明明就有較大的IO資料量的NVMF測試,還有正常的結果。
怎麼在這裡就有問題呢?
不同點:
- Intel肯定使用Intel的盤;
- 這兒用的是公司的盤;
難道是因為這個?
硬體上,理論上沒有這麼大差異吧。
經過一番探索發現,當把硬碟格式化為不帶DIF時,NVMF也是正常的,如果格式化為帶DIF的,即512+8格式時,問題就會出現;
SO,Intel為啥沒有問題,基本已經確定,他們用的是不帶DIF格式,同時發現不帶DIF,時延會快一點點,這很好理解。
有一個疑惑,始終沒有答案,為什麼本地寫沒有出現,而NVMF寫會出現呢?
這是需要回答的最重要的問題。
作為基礎,需要先簡單瞭解一下NVME的寫盤。
這個過程是非同步的;
寫盤前,程式將資料按照佇列(比如SGL)準備好,然後通知SSD,程式就完事了;
然後是SSD會到機器中把資料取出寫入盤中,處理完成後,然後通知程式,程式檢查結果佇列。
可以看出,當前說的寫盤,主要是指將資料按照佇列準備好就完成了,後面一段是由SSD裝置來處理的。
有了這個基礎,可以較快理解本地寫盤了,呼叫SPDK API後,由SPDK準備佇列,然後提交,真正把資料存起來的事情是SSD裡控制器做的。。。
但是NVMF寫盤呢?畢竟中間有段網路,是怎麼處理的。。。
為了便於分析,所以選擇改造DEMO,主要是perf比較複雜,隨機的LBA和大資料量對分析有較大干擾。
在DEMO程式中,指定在0號LBA開始提交資料,而且每次提交17塊資料(總長度17*520=8840)。
那為啥資料塊指定17呢?
因為15及以下是不會出現問題的,根據前面的分析,這塊SSD的正常分界線是15,而16是2的4次方,在計算機中2的N次方過於特殊,因此選擇普通的17。
其次,保證其它地方完全一樣,僅在初始化時,形成兩種模式,一種是本地寫,一種是NVMF寫;
如圖,手動直接改變紅框裡的引數,由tr_rdma和tr_pcie,可以在兩種模式中切換;
這樣的目的是,可以形成完全的對比,對齊所有能對齊的條件,分析在NVMF的哪個環節出現問題。
在初步單步跟蹤了一下呼叫過程,可以梳理出本地寫與NVMF寫的基本處理流程:
本地寫:
- 在請求端,申請了一塊連續的記憶體1M大小,塊大小以4K大小對齊;
- 將其中的17個塊(也就是1M大小隻用了17*520位元組)通過呼叫SPDK的API進行寫盤;
- SPDK的API會呼叫以PCIE模式介面(系統初始化時,註冊的回撥函式,在初始化入口時,上面圖中紅框的引數決定了會走向PCIE對應介面);
- 準備資料佇列,提交SSD寫盤請求,返回;
- 輪循處理完成的介面,獲取到寫盤成功通知;
NVMF寫:
請求端側:
(1)在請求端,申請了一塊連續的記憶體1M大小,塊大小以4K大小對齊;
(2)將其中的17個塊(也就是1M大小隻用了17*520位元組)通過呼叫SPDK的API進行寫盤;
(3) SPDK的API會呼叫以RDMA模式介面(同上,初始化時,註冊了RDMA的回撥函式,上圖中紅框的引數決定了,這裡的呼叫走向RDMA對應介面);
(4)準備資料佇列,通過RDMA網路傳送到服務端,返回;
服務端側:
(5) 服務端的RDMA在輪循(poll)中收到資料到來的通知;
(6)組裝資料結構,便於內部API呼叫;
(7)資料一路呼叫bdev,spdk,nvme的api,地址被轉換為實體地址,最後呼叫pcie的資料介面提交;
(8)然後按規範按下提交門鈴,返回;
兩側非同步(提交請求後,只能非同步等待結果列印)列印結果:
(9)請求端輪循處理完成的介面,如果錯誤會出現列印;
通過debug可以看到錯誤碼是15
(10)服務端輪循處理完成的介面,如果錯誤,會出現列印:
反覆對本地和NVMF下發資料(上面0開始,17塊資料),逐個流程與引數對比(雙屏提供了較大的便利),確實發現不少異同點:
(1)本地寫的過程與NVMF寫的請求端過程,幾乎一樣,不同的是本地寫的資料提交是到SSD,NVMF請求端的寫呼叫RDMA的介面;
(2) NVMF服務端有很長的呼叫棧(有30層深),而本地寫根本不存在這個過程;
(3)NVMF服務端在經過系列呼叫後,最後走到了像本地寫盤一樣的函式呼叫,nvme_transport_qpair_submit_request;
似乎是個顯然的結論,NVME OVER RDMA實際是,資料經過了RDMA傳輸後,還是NVME OVER PCIE;
(4)本地寫時,只有1個SGL,這個SGL裡面只有1個SGE,NVMF的請求端在呼叫RDMA前,也是隻有1個SGL,這個SGL裡也只有1個SGE;
(5) NVMF服務端的在寫盤前,只有1個SGL,但是這個SGL裡有2個SGE;
整個過程,用圖來描述如下:
如圖:
這是一個重要的發現,基本可以解釋為什麼解決辦法1部分場合是有效的(15的安全線內資料大小小於8k,保證1個SGL裡只有1個SGE),但無法解釋有一些場合失敗。
捋一下,就清楚多了:
RDMA在NVMF的請求端拿到的資料是1個SGL內含1個SGE,經過RDMA後,從NVMF服務端拿到的資料是1個SGL內含2個SGE。
至此,似乎基本“鎖定”了肇事者了,就是RDMA了!
但是,在翻閱RDMA的資料,SSD的資料後,發現1個SGL裡,1個SGE,2個SGE根本是自由的,自由的。。。
雖然,RDMA在接收資料後,將1個SGE分成2個SGE,有引起問題的嫌疑,但是從資料介紹看,似乎不能直接構成問題。
為了驗證1個SGL裡多個SGE是不是問題,又開始改造DEMO了,構造了寫資料前,將資料分為多個SGE了,如圖:
先試了試NVMF,發現可以復現,和前面的NVMF沒有什麼兩樣,
接下來試了試本地,發現沒有問題,也就是說,疑問沒有消除。
分析步驟(三)
山重水複疑無路,只好推倒,從頭再來分析,一次偶然的NVMF下發中發現,2個SGE的地址中,第2個SGE的地址在前,第1個SGE的地址在後,然後密切關注,即便在DEMO程式中,這個地址的先後也有一定的隨機,多數時候是順序的,少數時候是顛倒的,但是無論怎樣,1個SGE與另1個SGE中是不連續,也就是SGE1與SGE2之間有空洞。
馬上構造相同的形態,
寫本地,發現重現了!
這是一個“重要發現”!本地也能重現!
幾乎可以順利成章的推論出,是否NVMF不是關鍵!那麼也就排除了RDMA的嫌疑了!
寫盤時,如果多個SGE的資料區完全連續,則沒有問題,如果多個SGE的資料區不連續,則會出現問題。
那麼,很容易推匯出問題所在點,當前用的這個SSD不支援不連續的SGE!難道是SSD?!
然後。。。(此處略去一段文字不表。。。)
。。。
。。。
是的,SSD沒有問題,有問題的是那個8192的長度,正確的應該是8320!
8320是什麼,8192是什麼?
8192是512 * 16;
8320是520 * 16;
看看,之前一直不理解那個刷屏的錯誤提示,什麼叫“DATA SGL LENGTH INVALID”,這個含糊不清的提示,也有很多可能,既可能是SGL裡的SGE個數不對,也可能是SGE裡的長度不對,還可能是裡面的長度欄位讀寫不對,還可能是暫存器出錯,還可能記憶體被踩。。。
但是,真相就是,SGE裡的資料長度沒有和BLOCK的基本大小520對齊!現在用的格式是帶DIF區的,512+8=520!
那個提示是告訴你,資料塊沒有對齊,SGE裡的長度無效!
當各個點針對性的改好了這個基本引數時,
DEMO的本地正常了,
DEMO的NVMF也正常了,
似乎真相大白了。。。
然而,還沒高興幾分鐘,使用perf下發1M的IO時,問題又復現了!
分析步驟(四)
細心的跟蹤後發現,雖然問題復現了,但是沒有以前刷屏那麼多了,而且通過單步發現,只要SGE資料的地址是以FF000結尾的,就會出現問題。
回溯這個地址,可以看到,來源於RDMA在收到資料後就出現了,偶爾會出現FF000結尾的,所以可以解釋錯誤刷屏沒有那麼密集了。
看起來,還是RDMA有問題啊~
繼續分析可以發現,這些地址,實際也不是RDMA臨時分配的,而是從緩衝佇列裡獲取的。
基本可以認為,緩衝佇列中有很多可供選擇,偶爾會拿到FF000結尾的這種來做緩衝,只要這種地址就會出現問題。
那麼,為什麼這種地址就會出現問題呢?
還記得前面有一個步驟嗎?設定2M大頁記憶體,SPDK是基於DPDK的,DPDK記憶體佇列是要求大頁記憶體的,最常用的是2M大頁。
這些緩衝就是從DPDK那些大頁裡獲取的,而FF000就是靠近2M邊界的,一般的緩衝使用也沒有啥問題,但是SSD不接受跨大頁的空間,因此在準備提交佇列時,如果遇到要跨大頁的,將這個SGE做切分,1分為2,以FF000結尾的地址上只能存4096位元組,因此一個SGE裡4096,餘下的放在下一個SGE裡,而4096又不是520的對齊倍數,所以出問題了。
針對性的解決辦法是,在獲取地址前,加一個判斷,如果是這種地址就跳過。
修改!
驗證!
屏住呼吸。。。
但是,再一次出乎意料,用perf在大IO下測試依然有問題!
不氣餒,再戰!
開啟日誌(因為是非同步,而且是大資料量測試,所以只好在關鍵地方增加日誌,記錄下這些地址分配細節,主要地點,一個是提交請求時,見上面的檔案和程式碼行,就不貼程式碼了,一個是入RDMA收到資料最開始拿到的地方,還有一個是完成時的結果),繼續分析。
一下就看到,還有一種地址分配異常,也會形成SGE中長度問題,如圖:
再一次在獲取地址的位置進行修改遮蔽之,將兩種要跳過的直接合一。
如圖(471~475,另外在nvmf_request_get_buffers函式中需要配置進行跳過處理):
修改!
驗證!
各用例測試通過!
問題消失!
提供第2個解決辦法,按如上程式碼,可以徹底解決問題。
雖然問題解決了,跳過一些特殊地址,有一些浪費,
但是總感覺這種改法太土了!可以消除問題,但是隱隱感覺不爽!
分析步驟(五)
有沒有其它方法?
帶著疑問繼續挖。
既然RDMA只是使用緩衝的佇列,那就有一個地方是分配這種緩衝佇列的,分配出來卻不用,明顯有點浪費,那至少可以做到,分配的時候就不要分配這種資料吧。
一路回溯,終於找到申請的地方,但是甚是複雜,容後慢慢消化吧。
發現有段文字描述很長,和地址的分配很相關,
帶著這些資訊再來單步檢視分配緩衝過程,大致推測修改過程中的一個引數,就可以影響到後面的處理流程了。
紅框1為程式碼預設引數,修改為紅框2的,紅框2兩個引數的含義為單生產者單消費者,DEMO程式中完全匹配這個模式。
修改!
驗證!
RDMA在獲取SGE地址時,是單向增長的。
問題消失!
一個引數消除掉問題,對比起來,舒適多了!
【小結】
(1)問題最後的解決辦法就是: NVMF的配置檔案中需要顯性設定IOUnitSize的大小,與所用的Block大小成整數倍對齊,當前使用520的Block,建議設定為8320;修改建立記憶體池引數;最後圖中的一個引數即可。
(2) 過程非常曲折,但是隻要不放棄,跟著程式碼,再翻閱資料,大膽假設,小心求證,不斷迭代,終能找到問題所在;如果對相關概念與處理過程熟悉,會大幅度節約時間;
(3)最後安利一下,VSC,配上Remote – SSH,可以直接在呈現Linux機器上的程式碼,進行視覺化除錯,在程式碼裡任意穿梭,哪裡疑惑點哪裡,對本次分析問題有極大的幫助;
附錄:
Nvmf的配置檔案如下
[Global]
[Nvmf]
[Transport]
Type RDMA
InCapsuleDataSize 16384
IOUnitSize 8192
[Nvme]
TransportID "trtype:PCIe traddr:0000:04:00.0" Nvme0
TransportID "trtype:PCIe traddr:0000:05:00.0" Nvme1
TransportID "trtype:PCIe traddr:0000:82:00.0" Nvme2
[Subsystem1]
NQN nqn.2020-05.io.spdk:cnode1
Listen RDMA 192.168.80.4:5678
SN SPDK001
MN SPDK_Controller1
AllowAnyHost Yes
Namespace Nvme0n1 1
[Subsystem2]
NQN nqn.2020-05.io.spdk:cnode2
Listen RDMA 192.168.80.4:5678
SN SPDK002
MN SPDK_Controller1
AllowAnyHost Yes
Namespace Nvme1n1 1
[Subsystem3]
NQN nqn.2020-05.io.spdk:cnode3
Listen RDMA 192.168.80.4:5678
SN SPDK003
MN SPDK_Controller1
AllowAnyHost Yes
Namespace Nvme2n1 1