分散式持久記憶體檔案系統Octopus(ATC-17 )分析(五)
清華課題 Octopus 原始碼分析(五)
前言
由於專案工作的需要,我們團隊閱讀了清華在檔案系統方面的一個比較新穎的工作:Octopus。Octopus是一個基於持久記憶體 NVM 和遠端直接記憶體訪問 RDMA 技術的分散式記憶體檔案系統。清華的陸游遊老師現已將程式碼開源,可 點選此處 閱讀。
這一工作中的是 ATC-17 (CCF A類),可 點選此處 閱讀論文。
我們團隊希望通過學習清華的這個優秀的同行工作,來進一步開展自己對於分散式持久記憶體檔案系統的研究。關於論文的分析,雖然有做PPT給同伴們介紹過,但具體的部落格分析可能會晚些才放上來。這一系列的內容主要是分析Octopus的原始碼設計(少許會結合論文內容來講,希望有興趣的同學可以自己先讀一讀),總結Octopus的框架、結構、設計元件及程式碼創新點等。
系列分析總共包括 個部分。第一部分是 論文摘要,相當於Octopus系統的一個簡介;第二部分是 設計框架,在這一部分我們會巨集觀地介紹Octopus的組成體系及各部分的功能及相互間的聯絡;第三部分是 程式碼分析,也是本部落格的重中之重。在這一部分我們首先介紹標頭檔案體系(在include資料夾中),瞭解Octopus的儲存結構,表結構,主要資料結構,記憶體池劃分等等。接下來我們介紹方法實現程式碼(在src資料夾中),我們會通過比對標頭檔案內的函式名稱來看每一個方法是採用何種方式實現,好處是什麼,取捨考慮是什麼。進一步地,我們會通過程式碼檔案間的依賴關係,函式依賴關係去深入探討Octopus的創新性、侷限性並留出進一步討論的空間。
論文摘要
(內容請見系列上一篇部落格)
設計框架
(內容請見系列上一篇部落格)
src目錄原始碼分析
fs 模組
(其他模組內容請見系列上一篇部落格)
TxManager.cpp
我需要強調的是,Octopus這部分的程式碼並沒有完全兌現其論文中所描述的技術。在這一部分中我們來看幾個主要函式的實現,它們分別是 FlushData
(從cache刷出資料到持久記憶體),TxLocalBegin
(本地事務開始),TxWriteData
(事務記錄),TxLocalCommit
(本地事務提交),TxDistributedBegin
(分散式事務開始),TxDistributedPrepare
TxDistributedCommit
(分散式事務提交)。
至於TxManager的建構函式以及getTxWriteDataAddress
,都是比較簡單直觀的,在此不再贅述。
首先我們來看這個關鍵的資料刷出函式:FlushData
,程式碼如下所示:
void TxManager::FlushData(uint64_t address, uint64_t size) {
uint32_t i;
size = size + ((unsigned long)(address) & (CACHELINE_SIZE - 1));
for (i = 0; i < size; i += CACHELINE_SIZE) {
_mm_clflush((void *)(address + i));
}
}
我們知道,由於CPU及cache對於程式資料執行的優化,會導致指令及資料輸出亂序(不嚴格按照程式執行順序)執行。因此,允許按序刷出資料的原語便提供給程式設計人員使用,以滿足特殊的資料順序要求。因特爾在最新的指令集中支援如下五種刷出原語:
命令 | 解釋 |
---|---|
CLFLUSH | 刷出單個cacheline,指令按序,無併發保障 |
CLFLUSHSHOPT(後接SFENCE) | 刷出單個cacheline,可不按序,支援併發 |
CLWB(後接SFENCE) | 與CLFLUSHSHOPT相似,區別是刷出單個cacheline時該資料仍可保留在cache中 |
NT STores(後接SFENCE) | 非即時刷出,“寫聚合”,旁路cache |
WBINVD | 僅核心態刷出,同時無效化所有該CPU上的cache line |
Octopus在該程式碼裡使用的是 clflush
機制(但是缺少mfence
的配合)。
注意 _mm_clflush(addr)
的巨集定義是:
#define _mm_clflush(addr)\
asm volatile("clflush %0" : "+m" (*(volatile char *)(addr)))
FlushData
到底做了一件什麼事呢?從程式碼中,我們瞭解到,它是從指定地址address開始,以CACHELINE_SIZE(64位元組)為單位,迴圈刷出指定大小size內的資料。我對此抱有的擔憂是,現代作業系統僅支援最多8位元組的原子寫,如果在64位元組資料刷出的過程中,系統或程式崩潰了,資料不一致該怎麼辦?顯然,TxManager 並沒有很好地考慮這一方面的問題。
接下來我們看 TxLocalBegin
的實現,程式碼如下所示:
uint64_t TxManager::TxLocalBegin() {
lock_guard<mutex> lck (LocalMutex);
LocalLogEntry *log = (LocalLogEntry *)LocalLogAddress;
log[LocalLogIndex].TxID = LocalLogIndex;
log[LocalLogIndex].begin = true;
FlushData((uint64_t)&log[LocalLogIndex], CACHELINE_SIZE);
LocalLogIndex += 1;
return (LocalLogIndex - 1);
}
一個事務是一次原子性的操作集合,這些操作要麼全都完成,要麼全都沒做(對軟體/使用者不可見)。事務的狀態包括 開始、中止 和 提交。
從程式碼裡可以看出,為了處理併發事務的操作,這裡用到了鎖機制。然而,每次事務的開始都需要對本地事務管理器加鎖,這會嚴重影響效能。可以優化的點是將鎖的粒度放得更細一些——比如訪問某塊資料時對該塊資料上鎖,其餘塊仍可繼續併發開始事務。當然這樣做的代價是鎖的空間開銷。
這一步將第 LocalLogIndex
項日誌的 begin
狀態標記為 true
,然後將整個日誌 CACHE_LINE
刷出到持久記憶體。
接著我們再看 TxWriteData
的實現,程式碼如下所示:
void TxManager::TxWriteData(uint64_t TxID, uint64_t buffer, uint64_t size) {
LocalLogEntry *log = (LocalLogEntry *)LocalLogAddress;
memcpy((void *)log[TxID].logData, (void *)buffer, size);
FlushData((uint64_t)log[TxID].logData, size);
}
memcpy
是將快取中的指定大小資料(待更新資料)拷貝到日誌結構中,然後將該日誌資料刷出到持久記憶體中。
而提交操作(程式碼如下所示)則進一步設定 commit
標籤為提交(完成=1)或者中止(=0),並刷出到持久記憶體中。
void TxManager::TxLocalCommit(uint64_t TxID, bool action) {
LocalLogEntry *log = (LocalLogEntry *)LocalLogAddress;
log[TxID].commit = action;
FlushData((uint64_t)&log[TxID].commit, CACHELINE_SIZE);
}
當事務完成以後,對於原始(檔案)資料的更新就可以真正反映在持久記憶體中了,關於預寫式日誌技術,如有疑問可以 點選這裡 檢視維基百科。
至於分散式的開始和提交,與本地開始、提交相似,我們此處略過不談,但 TxDistributedPrepare
值得看看,程式碼如下所示:
void TxManager::TxDistributedPrepare(uint64_t TxID, bool action) {
DistributedLogEntry *log = (DistributedLogEntry *)DistributedLogAddress;
log[TxID].prepare = action;
FlushData((uint64_t)&log[TxID].prepare, CACHELINE_SIZE);
}
在論文裡,作者說準備階段是要求所有的 Participant 將必要資料資訊交給 Coordinator 來統一處理,以計算資源為代價減少通訊資源開銷,從而得到一個優化的效能結果。
然而,此處程式碼顯得確實太過精簡了,只是做了一個 prepare
標籤的確認,而沒有給出具體的分散式日誌間的通訊技術。這是我們希望可以繼續攻堅的研究點——也是一個非常有趣的研究點。
除了兩階段提交協議,我們還能想出什麼協議來協調分散式資料一致性呢?怎麼能夠平衡好 compute, memory 和 communicate 三者間的 tradeoff 換取某一場景下的最優呢?
filesystem.cpp
(內容請見系列下一篇部落格)