本文來源:微信公眾號(hào):騰訊云數(shù)據(jù)庫(kù)
原文地址:https://mp.weixin.qq.com/s?__biz=Mzg4NjA4NTAzNQ==&mid=2247488667&idx=1&sn=c1d3f497b2f31699a41ae6b4e8080feb
作者 張鵬義,騰訊云數(shù)據(jù)庫(kù)高級(jí)工程師,曾參與華為Taurus分布式數(shù)據(jù)研發(fā)及騰訊CynosDB for PG研發(fā)工作,現(xiàn)從事騰訊云redis數(shù)據(jù)庫(kù)研發(fā)工作。
存儲(chǔ)體系結(jié)構(gòu)
回顧一下計(jì)算機(jī)的存儲(chǔ)體系結(jié)構(gòu)。正如下圖所示,計(jì)算機(jī)存儲(chǔ)設(shè)備根據(jù)訪問速度及容量等形成了一個(gè)金字塔形的層次結(jié)構(gòu)。在這個(gè)層次結(jié)構(gòu)中,從上到下,設(shè)備的訪問時(shí)延越來越大,容量也越來越大,而每字節(jié)的造價(jià)也越來越便宜。在這個(gè)層次結(jié)構(gòu)中,每一層都被看作是其下一層次的緩存。
同時(shí)在這個(gè)層次結(jié)構(gòu)中存在一個(gè)明顯的分界線,如圖中綠色的虛線。在分界線以上都是易失性的設(shè)備,掉電數(shù)據(jù)丟失,CPU通過load/store指令訪問存儲(chǔ)設(shè)備,即數(shù)據(jù)直接以cache line(64 byte)的粒度從寄存器中copy到存儲(chǔ)設(shè)備中,或者從存儲(chǔ)設(shè)備中copy到寄存器。而分界線以下都是可持久化的存儲(chǔ)設(shè)備,通過向I/O控制器發(fā)送命令,以block的粒度將數(shù)據(jù)換入換出到DRAM中,然后才能直接被CPU訪問。
隨著近互聯(lián)網(wǎng)的發(fā)展,數(shù)據(jù)密集型應(yīng)用已然成為主流,其主要特點(diǎn)表現(xiàn)為數(shù)據(jù)容量大,對(duì)數(shù)據(jù)分析的實(shí)時(shí)性及數(shù)據(jù)存儲(chǔ)的高效性提出了更高的要求,以spark、redis為代表的基于內(nèi)存的數(shù)據(jù)分析與存儲(chǔ)系統(tǒng)廣受歡迎。
但是DRAM的容量受限于晶體管-電容式(1T1C)單元的密度,以及難以擴(kuò)展的動(dòng)態(tài)刷新(refresh)操作,DRAM的容量沒辦法做得很大,服務(wù)器上主流的DRAM一般為16G或32G,容量再大功耗相應(yīng)也會(huì)增加。而且DRAM的價(jià)格比較高,數(shù)據(jù)不能持久化。
對(duì)于SSD等一些塊設(shè)備來說,確實(shí)能夠滿足大容量持久化的要求,但是訪問延遲卻是DRAM的1000多倍,同時(shí)塊設(shè)備存在隨機(jī)訪問能力比較差的問題。雖然近些年來出現(xiàn)了以rocksdb/leveldb為代表的LSM-Tree結(jié)構(gòu)的存儲(chǔ)系統(tǒng)從一定程度上解決了塊設(shè)備低效的寫問題,但同時(shí)又引入了一個(gè)比較嚴(yán)重的寫放大問題。
SSD與DRAM的訪問延遲差異導(dǎo)致在分界線處形成了一條巨大的鴻溝,持久化內(nèi)存的出現(xiàn)正好給填補(bǔ)上了。
持久化內(nèi)存在存儲(chǔ)體系層次結(jié)構(gòu)中的位置如下圖所示,它位于DRAM與SSD之間,同時(shí)注意到它是橫跨在分界線上的, 它模糊了傳統(tǒng)層次結(jié)構(gòu)中內(nèi)存與外存的邊界 ,它既具有DRAM一樣的數(shù)據(jù)訪問方式,也具有SSD一樣的持久化特性。所以可以把它當(dāng)成內(nèi)存或者持久化的存儲(chǔ)使用。
Intel optane DC persistent memory是Intel推出的基于3D Xpoint技術(shù)的持久化內(nèi)存產(chǎn)品,其代號(hào)為Apace Pass (AEP)。AEP的物理封裝為DIMM,直接插在內(nèi)存插槽上,單條AEP的容量目前已經(jīng)達(dá)到了512G,如上圖所示,如果一半的插槽插上AEP,那么單個(gè)CPU內(nèi)存容量很輕松的就可以擴(kuò)展到3TB以上。IMC(integrated memory controller)與AEP之間以cache line大小的粒度傳輸數(shù)據(jù),但是AEP的ECC(E rror correction code ) size為256字節(jié),即AEP內(nèi)部的controller與介質(zhì)之間是以256字節(jié)(4個(gè)cache line)為單位進(jìn)行讀寫。同時(shí)AEP的讀寫不對(duì)稱,讀性能明顯優(yōu)于寫性能,所以應(yīng)該避免頻率的寫入小對(duì)象。
AEP工作模式
AEP有兩種工作模式:Memory Mode和App Direct Mode,如果細(xì)分就還有Storage Over App Direct。
一、Memory Mode
根據(jù)AEP在存儲(chǔ)層次結(jié)構(gòu)中所處的位置,自然想到的是將DRAM作為AEP的緩存,AEP中的熱數(shù)據(jù)緩存在DRAM中,這也符合計(jì)算機(jī)存儲(chǔ)系統(tǒng)一慣的設(shè)計(jì)思想。在這種模式下AEP和DRAM共同組成了一塊對(duì)上層透明且容量更大的易失性內(nèi)存,這時(shí)系統(tǒng)的總?cè)萘康扔贏EP的容量,應(yīng)用無需做任何額外的修改即可使用。DRAM到AEP的緩存算法由IMC(集成在CPU里的memory controller)硬件管理。
當(dāng)前的緩存策略采用 direct-mapped-cache 算法實(shí)現(xiàn), 數(shù)據(jù)以cache line的粒度換入換出,但是這種算法會(huì)把AEP中多塊內(nèi)存映射到DRAM中的同一個(gè)位置,以致于在某些場(chǎng)景下出現(xiàn)沖突不命中的情況可能比較嚴(yán)重。所以這么一個(gè)通用的算法并不能很好地將熱數(shù)據(jù)從AEP中分離出來。
Memory Mode配置方法:
ipmctl create -goal memorymode=100 (ipmctl是Intel開發(fā)的一個(gè)管理持久化內(nèi)存的工具)
reboot后 free -g 就可以看到系統(tǒng)內(nèi)存容量等于AEP的總?cè)萘俊?/p>
二、AppDirect Mode
應(yīng)用可以不經(jīng)過DRAM而直接訪問AEP,用由應(yīng)用自己管理,這種使用方式叫做AppDirect Mode。那么在這種模式下應(yīng)用怎么訪問AEP?SNIA(Storage Networking Industry Association)制定了一套編程模型,如下圖所示,其中NVDIMM指非易失性內(nèi)存模塊,即AEP設(shè)備。
傳統(tǒng)的文件系統(tǒng)都有一層page cache, 訪問數(shù)據(jù)時(shí)先將數(shù)據(jù)copy到page cache中,然后再copy到應(yīng)用buffer 中。對(duì)于低延遲及可按字節(jié)尋址的AEP來說,沒這個(gè)必要。Pmem-Aware File System指的是支持直接訪問設(shè)備(DAX)的文件系統(tǒng),DAX特性從IO路徑上移除page cache, 同時(shí)允許mmap()直接建立到持久化內(nèi)存的映射關(guān)系。目前ext4和xfs都已支持DAX特性。
AppDirect Mode配置方法:
ipmctl create -goal PersistentMemoryType=appdirect
reboot (reboot后就可以在/dev下看到pmem0,pmem1設(shè)備)
ndctl create-namespace -m fsdax -r 0
mkfs -t ext4 /dev/pmem0
mount -o dax /dev/pmem0 /mnt/pmem0 (-o dax開啟DAX)
配置完成后就可以在/mnt/pmem0下創(chuàng)建、刪除、讀寫文件了,但是一般不會(huì)直接這么使用。通常在/mtn/pmem0/上通過mmap建立memory-map file, 一旦映射建立后,用戶虛擬地址空間通過MMU就直接映射到AEP的物理空間中,應(yīng)用無需陷入內(nèi)核態(tài)即可高效地以字節(jié)尋址的方式訪問AEP。再借助于libmemkind庫(kù),就可像使用DRAM一樣來使用AEP。Libmemkind將create/open file, mmap進(jìn)行了封閉,并提供類malloc/free的接口在AEP上分配內(nèi)存。所以應(yīng)用需要顯示通過類malloc/free接口決定哪些數(shù)據(jù)直接寫到AEP,對(duì)已經(jīng)代碼要做一定的修改。
要注意的是mmap必須是以shared的方式建立映射,如果是以private的方式映射,更新數(shù)據(jù)時(shí)并不會(huì)寫到介質(zhì)上,而是寫到進(jìn)程私有的空間中(page cache中),然而這里AppDirect模式下是沒有page cache的。這里引出了一個(gè)問題:如果進(jìn)程在AEP中分配了一塊內(nèi)存,然后fork一個(gè)子進(jìn)程,那么子進(jìn)程也是能夠看到父進(jìn)程對(duì)這塊內(nèi)存的更新,因?yàn)閮蓚€(gè)父子進(jìn)程的更新都會(huì)立即反映到同一塊物理內(nèi)存上。所以對(duì)于分配在AEP上的內(nèi)存就沒辦法利用fork的copy on write機(jī)制來獲取一致性的內(nèi)存狀態(tài)。(Redis正是利用fork的copy on write機(jī)制獲取一對(duì)致性內(nèi)存狀態(tài)做備份操作)。
AppDirect下即可以將AEP當(dāng)作易失性的內(nèi)存使用也可以當(dāng)作持久化的內(nèi)存使用。當(dāng)作易失性內(nèi)存使用時(shí),僅僅是我們不關(guān)注重啟后AEP上的數(shù)據(jù)內(nèi)容而已,并不是指掉電后AEP上的內(nèi)容真的丟失了。如果當(dāng)持久化的內(nèi)存使用,則應(yīng)用需要處理持久化及數(shù)據(jù)一致性等問題,下節(jié)詳細(xì)講。
三、Storage over App Direct
如上圖中的第三、四條路徑,分別對(duì)外提供block接口和標(biāo)準(zhǔn)的文件接口,應(yīng)用無需額外的適配工作,這和使用SSD并沒有什么區(qū)別,僅僅是延遲更小。但是這種使用方式軟件棧上的開銷所占的比例相當(dāng)大,并不能發(fā)揮出很好發(fā)揮出AEP的極致性能,基本不用考慮這種用法,除非是已有代碼確實(shí)不能改動(dòng)但又想獲得低延遲訪問的情況。
當(dāng)作持久化內(nèi)存使用時(shí)應(yīng)用需關(guān)注的問題
一、數(shù)據(jù)持久化
當(dāng)一條store指令完成后,數(shù)據(jù)并沒有立即寫到DIMM介質(zhì)上,很有可能還存在于cpu cache中或者M(jìn)emory Controller的Write Pending Queue中,掉電后還是會(huì)造成數(shù)據(jù)丟失,也就說這里是一個(gè)異步的持久化過程。所以應(yīng)用需要顯示flush cpu cache和WPQ后才能說明數(shù)據(jù)確實(shí)已經(jīng)持久化,當(dāng)然在Intel平臺(tái)上ADR模塊掉電后會(huì)觸發(fā)硬件中斷到Memory Controller并flush WPQ中的數(shù)據(jù)到DIMM介質(zhì)上。即Intel平臺(tái)上關(guān)注cpu cache就好。clflush、clflushopt、clwb指令都是用于flush cpu cache, 其中clflushopt、clwb是為了提升持久化內(nèi)存的flush效率新引入的,不一定所有平臺(tái)都支持。其中clflush是串行flush, 而clflushopt可以并行flush, clwb與clflush類似,只是并不一定會(huì)立即失效cache line,提升后續(xù)讀性能。
由于CPU亂序執(zhí)行和cpu cahe 并行flush問題可能會(huì)導(dǎo)致兩個(gè)數(shù)據(jù)對(duì)象實(shí)際在介質(zhì)上持久化的順序與應(yīng)用寫入的順序相反,如果說這兩個(gè)數(shù)據(jù)數(shù)據(jù)對(duì)象具有因果關(guān)系,那就出大問題了。
如下面這段代碼,如果list->length修改后的值所在的cache line先被flush到介質(zhì),而在list->tail->next修改后的值還未flush到介質(zhì)之前機(jī)器掉電,重啟后根據(jù)list->length再去遍歷list就可能會(huì)造成崩潰。
typedef struct List {
int length;
struct List* head;
struct List* tail;
};
typedef struct Node {
void * data;
struct Node* next;
}Node;
void appendList(List* list, Node* node){
list->tail->next = node;
list->tail = node;
list->length++;
}
正確的做法是在flush(&(list->length))之前加上fence指令確保持久化的先后順序。
void appendList(List* list, Node* node){
list->tail->next = node;
list->tail = node;
__mm_clwb(&(list->tail->next));
__mm_clwb(&(list->tail));
__mm_sfence(); //
list->length++;
__mm_clwb(&(list->length));
}
二、數(shù)據(jù)一致性
在X86平臺(tái)上,僅不大于8字節(jié)的對(duì)象能夠保證是原子寫,大于8字節(jié)的數(shù)據(jù)對(duì)象可能寫到一半出現(xiàn)機(jī)器掉電,重啟后的數(shù)據(jù)是否完整無法得知。所以應(yīng)用需要通過flag/redo log/undo log等方式判斷數(shù)據(jù)是否完整,以及不完整時(shí)該怎么去處理,當(dāng)然這勢(shì)必會(huì)引入比較重的開銷。
再比如將Redis的索引結(jié)構(gòu)(hash table)及數(shù)據(jù)都寫入到持久化內(nèi)存中,那么當(dāng)用戶寫入一條數(shù)據(jù)時(shí),內(nèi)部可能發(fā)生hash table擴(kuò)容,hash entry搬遷等多個(gè)動(dòng)作,要維護(hù)數(shù)據(jù)的一致性問題,那就也需要引入類似mini transaction的機(jī)制。所以把AEP當(dāng)作持久化內(nèi)存與易失性內(nèi)存來使用時(shí)性能肯定是一定的差異的。
一旦考慮把AEP當(dāng)作持久化的內(nèi)存來使用時(shí),所寫下的每一行代碼都考慮怎么處理數(shù)據(jù)一致性的問題,這并不是一件容易的事情。
為了簡(jiǎn)化持久化內(nèi)存在AppDirect下的使用,Intel開發(fā)了 PMDK (Persistent Memory Development Kit)。我們可以直接在PMDK的基礎(chǔ)上去開發(fā)自己的應(yīng)用,但是這些通用的庫(kù)也并不一定適合所有場(chǎng)景。
下面這幾個(gè)庫(kù)用的比較多一些:
1. libpmem: 用來將數(shù)據(jù)持久化到介質(zhì)上,以及提供一些優(yōu)化的內(nèi)存操作(memcpy, memset等)函數(shù)。
2. libpmemlog: 基于這個(gè)庫(kù)可以用來寫順序追加的log等。
3. ibpmemobj: 這個(gè)庫(kù)實(shí)現(xiàn)了一套事務(wù)機(jī)制,用來保證數(shù)據(jù)的一致性問題。
最后用一張圖總結(jié)一下AEP的使用方式及在數(shù)據(jù)領(lǐng)域潛在的價(jià)值。
本文來源:微信公眾號(hào):騰訊云數(shù)據(jù)庫(kù)
原文地址:https://mp.weixin.qq.com/s?__biz=Mzg4NjA4NTAzNQ==&mid=2247488667&idx=1&sn=c1d3f497b2f31699a41ae6b4e8080feb
作者 張鵬義,騰訊云數(shù)據(jù)庫(kù)高級(jí)工程師,曾參與華為Taurus分布式數(shù)據(jù)研發(fā)及騰訊CynosDB for PG研發(fā)工作,現(xiàn)從事騰訊云Redis數(shù)據(jù)庫(kù)研發(fā)工作。