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