顛覆三觀,內(nèi)存真能當(dāng)SSD用了!??!
大家好,我是小風(fēng)哥。
在之前的文章《阿里終面:為什么不能把SSD當(dāng)內(nèi)存用?》中討論了為什么SSD不能當(dāng)內(nèi)存用這一話題,然后就有很多同學(xué)跑過來咨詢文章中提到的新硬件。
在這里說下,大家有什么問題可以直接添加我的個(gè)人微信:coder_saver,備注”讀者”即可,feel free to contact me。
在這篇文章中博主提到了Intel有一款新設(shè)備,即能當(dāng)內(nèi)存用也能像磁盤一樣持久化內(nèi)存數(shù)據(jù),很多同學(xué)咨詢這種新硬件的編程問題,那么在這篇文章中小風(fēng)哥就給大家聊聊這話題。
持久內(nèi)存
在這里簡單說下,這種新硬件就是Intel推出的傲騰持久內(nèi)存,Intel Optane Persistent Memory ,這種硬件可以當(dāng)做內(nèi)存來用,和普通內(nèi)存一樣,但同時(shí)也具有非易失特性,像磁盤或者SSD一樣斷電后內(nèi)存中的數(shù)據(jù)不會(huì)丟失,就是這么的神奇。
對(duì)于這類持久化內(nèi)存來講就真的沒有重啟一說了,因?yàn)閿?shù)據(jù)會(huì)一直保存在內(nèi)存里,加電后直接用即可,你的程序就再也沒有啟動(dòng)或者初始化一說了,這是一種全新的設(shè)備,對(duì)程序員來說有一定的挑戰(zhàn)。
那么針對(duì)這類硬件該如何編程呢?
面向存儲(chǔ)編程
實(shí)際上數(shù)據(jù)結(jié)構(gòu)或者說數(shù)據(jù)存放在兩個(gè)地方:內(nèi)存以及存儲(chǔ)設(shè)備,這里的存儲(chǔ)設(shè)備就是程序員熟悉的磁盤或者SSD。
對(duì)于需要將數(shù)據(jù)存放在存儲(chǔ)設(shè)備的程序員來說,通常必須小心的維護(hù)數(shù)據(jù)的一致性,為什么呢?
對(duì)于高可靠程序來說你必須能隨時(shí)應(yīng)對(duì)斷電或者程序崩潰,如果數(shù)據(jù)沒有及時(shí)的從內(nèi)存刷入磁盤,那么此時(shí)你的數(shù)據(jù)將會(huì)丟失;而如果在寫入磁盤的過程中發(fā)生了斷電或者程序崩潰crash,那么此時(shí)寫入磁盤就是不完整的數(shù)據(jù)。
對(duì)于面向存儲(chǔ)設(shè)備編程的程序員來說解決上述問題有一個(gè)常用的方法,那就是write-ahead logging。
你不是會(huì)隨時(shí)斷電或者隨時(shí)程序崩潰嗎,我在真正寫入磁盤之前先寫一段log,這段log的內(nèi)容可能是這樣的:“我要往磁盤中寫入一句話,這句話是“小風(fēng)哥太帥了!””。
那么假設(shè)在將真正的數(shù)據(jù)“小風(fēng)哥太水了!”這個(gè)字符串寫入磁盤的過程中機(jī)房斷電或者程序崩潰,放心,在這種情況下是丟失不了數(shù)據(jù)的,此后程序在重啟時(shí)通過再次讀取該log:“我要往磁盤中寫入一句話“小風(fēng)哥太帥了!”,該程序就能獲得足夠的信息來再次往磁盤中寫數(shù)據(jù),這就是write-ahead logging的妙用。
到這里我先應(yīng)該能大體明白這類程序員所面臨的挑戰(zhàn)了。
面向內(nèi)存編程
而對(duì)于面向內(nèi)存編程,也就是通常不需要關(guān)心數(shù)據(jù)持久存儲(chǔ)問題的程序員來說也沒那么容易,雖然你不需要關(guān)心數(shù)據(jù)持久存儲(chǔ)所面臨的一致性問題,但你需要在程序運(yùn)行過程中解決多線程訪問的一致性問題。
當(dāng)多個(gè)線程訪問同一段內(nèi)存時(shí),程序員通常需要加鎖,這樣當(dāng)一個(gè)程序修改這段內(nèi)存時(shí)可以確保其它線程不會(huì)看到中間狀態(tài)——也就是修改到一半時(shí)的內(nèi)存數(shù)據(jù)。
當(dāng)斷電或者程序崩潰后內(nèi)存中的內(nèi)容就消失了,因此你不需要去關(guān)心持久存儲(chǔ)所面臨的一致性問題。
內(nèi)存數(shù)據(jù)斷電后消失、程序崩潰后內(nèi)存內(nèi)容消失以及磁盤數(shù)據(jù)可以持久存儲(chǔ)這些特性對(duì)于當(dāng)前的程序員來說已經(jīng)像空氣一樣習(xí)以為常了。
面向持久內(nèi)存編程
現(xiàn)在告訴你,有一種新硬件,這種硬件能讓你直接當(dāng)內(nèi)存來用,也就是可以直接字節(jié)尋址但與此同時(shí)斷電后內(nèi)容又不消失,你覺得會(huì)怎樣?
我想會(huì)有很多同學(xué)大呼神奇,該技術(shù)可以讓你獲得大量廉價(jià)內(nèi)存,同時(shí)內(nèi)存中的數(shù)據(jù)在斷電以及程序崩潰時(shí)內(nèi)容不丟失。
但神奇的不止是這種硬件,針對(duì)該硬件進(jìn)行編程同樣需要編程思維上的轉(zhuǎn)變。
從特性上看,該硬件即是內(nèi)存又是磁盤,因此上面關(guān)于持久數(shù)據(jù)一致性以及多線程一致性的考慮都適用于該硬件,也就是說針對(duì)該硬件進(jìn)行編程時(shí)你即需要考慮多線程訪問一致性,也需要考慮持久數(shù)據(jù)一致性。
于此同時(shí),最讓C/C++的程序員頭疼的問題之一,即內(nèi)存泄漏在持久內(nèi)存的場景下就更有挑戰(zhàn)了,在普通內(nèi)存下內(nèi)存泄漏后大不了重啟,而在持久內(nèi)存場景下,如果出現(xiàn)了內(nèi)存泄漏,那就是持久的內(nèi)存泄漏,重啟不再起作用。
這些程序員來說一個(gè)極大的挑戰(zhàn)。
For example
我們來看一個(gè)簡單的示例:
假設(shè)這段代碼出自銀行的賬戶系統(tǒng),定義了一個(gè)簡單的結(jié)構(gòu)體:結(jié)構(gòu)體包含兩項(xiàng):用戶姓名和賬戶余額:
- struct account {
- string name;
- int money;
- };
當(dāng)有新用戶存錢時(shí),那么需要?jiǎng)?chuàng)建一個(gè)實(shí)例然后更新姓名和賬戶余額:
- struct account *xfg = new account();
- xfg->name = "xiaofengge";
- xfg->money = 100000000; // 單位人民幣
是的,你沒有看錯(cuò),小風(fēng)哥在這段代碼里已經(jīng)財(cái)務(wù)自由了圖片。
這段代碼在程序員看來平淡無奇如同白開水一般。
第一行代碼從堆上分配一段內(nèi)存用來構(gòu)建data對(duì)象,后兩行用來初始化各個(gè)字段,簡單吧。
假設(shè)此時(shí)程序在執(zhí)行到第3行時(shí)機(jī)器斷電,或者系統(tǒng)崩潰,那么此時(shí)內(nèi)存會(huì)一掃而空,不會(huì)再有mydata的數(shù)據(jù)存在,小風(fēng)哥我在這家銀行不會(huì)有任何信息存在,當(dāng)然還包括我的1億巨款。
但假設(shè)該程序不是運(yùn)行在普通內(nèi)存而是持久內(nèi)存當(dāng)中會(huì)怎么樣呢?
讓我們?cè)賮砜匆幌逻@段代碼:
- struct data *xfg = new data();
- xfg->name = "xiaofengge";
- xfg->money = 100000000;
假設(shè)在執(zhí)行到第三行時(shí)機(jī)房斷電了,注意,此時(shí)程序的數(shù)據(jù)都保存在持久內(nèi)存中,那么此時(shí)斷電小風(fēng)哥的賬戶名稱已經(jīng)保存下來了,還不錯(cuò),但最重要的1億元卻沒有保存下來,那么當(dāng)程序再次啟動(dòng)時(shí)小風(fēng)哥就只能看到一個(gè)空的賬戶了。
現(xiàn)在你應(yīng)該意識(shí)到基于持久內(nèi)存進(jìn)行編程的難點(diǎn)了吧。
基于持久化內(nèi)存編程的復(fù)雜性
程序員在基于持久內(nèi)存進(jìn)行編程時(shí)需要時(shí)刻意識(shí)到這是一塊內(nèi)存,因此需要維護(hù)多線程訪問的一致性,但與此同時(shí)這又是一塊存儲(chǔ)設(shè)備,需要維護(hù)在運(yùn)行時(shí)以及持久化的數(shù)據(jù)一致性。
基于此現(xiàn)狀,當(dāng)前的支持持久內(nèi)存的庫都支持這樣一種特性,也即原子特性,atomic。
原子在不用的應(yīng)用場景下有不同的語義,在多線程編程場景下,原子也即意味著除了當(dāng)前線程之外沒有任何一個(gè)線程更看到數(shù)據(jù)的中間狀態(tài),換句話說就是不會(huì)有多個(gè)線程同時(shí)去修改一塊內(nèi)存。
但原子在持久內(nèi)存下的語言就不太一樣了,原子在這種場景下的語義是說不管在任何時(shí)刻斷電也好、程序崩潰也好,當(dāng)程序重啟后不會(huì)看到數(shù)據(jù)的中間狀態(tài),該數(shù)據(jù)要么已經(jīng)正確的持久化要么還沒有開始持久化。
這里的數(shù)據(jù)和上面一樣,小到一個(gè)字節(jié),大到一個(gè)非常復(fù)雜的結(jié)構(gòu)體。
多線程中的鎖只能保證內(nèi)存更新的原子性,但不能保證數(shù)據(jù)持久化的原子性。
解決方案
為實(shí)現(xiàn)數(shù)據(jù)持久化原子性,持久內(nèi)存編程SDK通常從數(shù)據(jù)中借鑒一個(gè)叫做事務(wù)的概念,transaction。
事務(wù)的意思是這樣的:假設(shè)某個(gè)數(shù)據(jù)可能需要經(jīng)過A、B、C、D幾個(gè)步驟才能修改完畢,我們把這四個(gè)步驟打包放到事務(wù)中,那么事務(wù)就可以確保這四個(gè)步驟要么全部執(zhí)行完畢,要么全部都不去執(zhí)行。這樣即使在任意一個(gè)步驟斷電或者程序崩潰都不會(huì)影響到數(shù)據(jù)的一致性問題。
如果你對(duì)持久化內(nèi)存編程非常感興趣,關(guān)注公眾號(hào)碼農(nóng)的荒島求生并回復(fù)pmem即可下載詳細(xì)編程資料。
值得注意的是,程序員常用的磁盤flush操作只是確保當(dāng)該函數(shù)執(zhí)行完成后數(shù)據(jù)已經(jīng)被寫到了磁盤,但flush并不等同于事務(wù),因?yàn)槿绻趂lush過程中如果斷電或者系統(tǒng)崩潰那么數(shù)據(jù)就處于薛定諤狀態(tài),可能數(shù)據(jù)已經(jīng)被完全寫到磁盤了,也可能只寫了一部分,當(dāng)然,也可能什么都沒寫。
有的同學(xué)可能不理解為什么讀寫磁盤時(shí)要flush,原因在于操作系統(tǒng)會(huì)把內(nèi)存當(dāng)做磁盤的緩存用,出于性能的考慮你寫到磁盤中的數(shù)據(jù)并不會(huì)立即刷入磁盤,而是會(huì)有一個(gè)異步任務(wù)來完成寫磁盤操作,這就是Linux下的page cache機(jī)制,關(guān)于這類機(jī)制的實(shí)現(xiàn)原理請(qǐng)參見博主的深入理解操作系統(tǒng),關(guān)注公眾號(hào)碼農(nóng)的荒島求生并回復(fù)操作系統(tǒng)即可。
總結(jié)
本文介紹一種全新的內(nèi)存設(shè)備,這類設(shè)備可以被操作系統(tǒng)識(shí)別為內(nèi)存,但又像磁盤一樣斷電后內(nèi)容不丟失,這類設(shè)備尤其適用于對(duì)內(nèi)存容量要求高以及程序啟動(dòng)時(shí)間長的場景。
但,這類設(shè)備在編程上對(duì)程序員來說是一大挑戰(zhàn),這種持久內(nèi)存在未來是否會(huì)成為主流也尚待觀察。
我是小風(fēng)哥,希望這篇文章能給大家一些啟發(fā)。
本文轉(zhuǎn)載自微信公眾號(hào)「碼農(nóng)的荒島求生」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼農(nóng)的荒島求生公眾號(hào)。