[译]提案:在Go语言中增加对持久化内存的支持
作者:Jerrin Shaji George、Mohit Verma、Rajesh Venkatasubramanian、Pratap Subrahmanyam Jerrin Shaji George, Mohit Verma, Rajesh Venkatasubramanian, Pratap Subrahmanyam.
最后更新。2021年1月20日
討論地點(diǎn):https://golang.org/issue/43810。
摘要
持久化存儲(chǔ)器是一種新的存儲(chǔ)器技術(shù),其有接近DRAM的訪問速度,并提供類似磁盤的持久化。Linux和Windows服務(wù)器已經(jīng)支持持久內(nèi)存,服務(wù)器可用的商用硬件現(xiàn)在也已經(jīng)推出了。關(guān)于這項(xiàng)技術(shù)的更多細(xì)節(jié)可以在pmem.io找到。
本文檔是為 Go 增加 pmem 支持的提案文檔,具體的詳細(xì)設(shè)計(jì)可以參考我們發(fā)表的2020年USENIX ATC論文[go-pmem](https://www.usenix.org/system/files/atc20-george.pdf)?;贕o 1.15版本的上述設(shè)計(jì)的實(shí)現(xiàn),可以在以下網(wǎng)站找到此處。
背景
持久化存儲(chǔ)是一種新型的隨機(jī)存取存儲(chǔ)器,它提供了持久化的功能。并以類似DRAM的訪問速度實(shí)現(xiàn)尋址。操作系統(tǒng)提供了將該內(nèi)存映射到應(yīng)用程序的虛擬地址的能力。應(yīng)用程序可以像使用內(nèi)存一樣使用這個(gè)mmap區(qū)域。更新到持久化內(nèi)存的數(shù)據(jù),即使是崩潰/重啟后,這些數(shù)據(jù)依然能夠被正常使用。
使用持久化內(nèi)存的應(yīng)用程序在很多方面都有好處。由于數(shù)據(jù)更新到持久化內(nèi)存是非易失性的,應(yīng)用不再需要維護(hù) DRAM 和存儲(chǔ)設(shè)備之間的數(shù)據(jù)關(guān)系,不需要在DRAM和存儲(chǔ)設(shè)備之間調(diào)配數(shù)據(jù)。相當(dāng)一部分的應(yīng)用程序代碼可以直接退役了。
另一個(gè)大的優(yōu)勢(shì)是顯著減少了應(yīng)用程序重新啟動(dòng)時(shí)的啟動(dòng)時(shí)間。這是因?yàn)閼?yīng)用程序不再需要把持久化的數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)進(jìn)行轉(zhuǎn)換。商業(yè)應(yīng)用SAP HANA給出的報(bào)告可以看到性能有 12 倍的提升:[12x improvement](https://cloud.google.com/blog/topics/partners/available-first-on-google-cloud-intel-optane-dc-persistent-memory)。
這個(gè)proposal是要為持久化內(nèi)存提供原生支持,在Go語言中,我們的設(shè)計(jì)修改了Go 1.15,引入了一個(gè)垃圾收集的持久化的方法。我們還在 Go 編譯器中引入了新語義,以支持事務(wù)性更新到持久化內(nèi)存數(shù)據(jù)結(jié)構(gòu)。我們把我們修改后的Go套件稱為go-pmem。使用go-pmem開發(fā)的Redis數(shù)據(jù)庫(kù)與在NVMe SSD上運(yùn)行的Redis相比,吞吐量提高了5倍。
提案
我們建議在Go中增加對(duì)持久化內(nèi)存編程的本地支持。這需要在Go中提供以下功能。
支持持久化的內(nèi)存分配
對(duì)持久化內(nèi)存堆對(duì)象進(jìn)行垃圾收集。
修改持久化內(nèi)存數(shù)據(jù)結(jié)構(gòu)需要保證“崩潰時(shí)的一致性”
使應(yīng)用程序能夠在崩潰/重新啟動(dòng)后恢復(fù)。
支持應(yīng)用程序從持久化內(nèi)存中恢復(fù)存儲(chǔ)的數(shù)據(jù)。
為了支持這些功能,我們擴(kuò)展了Go運(yùn)行時(shí),并添加了一個(gè)新的SSA pass。我們的實(shí)現(xiàn)在后文中闡述。
理由
現(xiàn)在已經(jīng)存在一些庫(kù),如Intel PMDK,為C和C++開發(fā)人員提供了支持持久化內(nèi)存編程的開發(fā)工具。其他編程語言,如Java和Python,正在探索如何支持。
例如:
Java - https://bugs.openjdk.java.net/browse/JDK-8207851
Python - https://pynvm.readthedocs.io/en/v0.3.1/
但是目前還沒有哪種語言原生地對(duì)持久化內(nèi)存進(jìn)行支持。我們認(rèn)為這是對(duì)推廣pmem技術(shù)的一種障礙。這個(gè)提案就是要讓Go成為第一個(gè)原生完全支持持久化內(nèi)存的語言。
為什么要改變語言?
C庫(kù)暴露了一個(gè)與現(xiàn)有編程模型明顯不同(而且復(fù)雜)的編程模型。內(nèi)存管理對(duì)于一個(gè)語言的外部庫(kù)來說其實(shí)是很困難的。漏掉一個(gè) "free "調(diào)用就會(huì)導(dǎo)致內(nèi)存泄漏,而在持續(xù)化內(nèi)存中,如果發(fā)生泄漏就是永久性的,不會(huì)在應(yīng)用重新啟動(dòng)后消失。在Go這樣有運(yùn)行時(shí)的語言中,使本來只給垃圾收集管理的內(nèi)存讓外部庫(kù)可見還是很困難的。為了能提供事務(wù)性的語義,需要對(duì)持久化內(nèi)存的寫操作進(jìn)行定制和組織,這也需要對(duì)語言進(jìn)行修改。經(jīng)過我們的實(shí)踐,對(duì)Go的編譯器和運(yùn)行時(shí)進(jìn)行增量修改還是比較容易的。
兼容性
我們目前的修改保留了Go 1.x未來兼容性的承諾。它做到了不會(huì)破壞不使用任何持久化內(nèi)存功能的程序的兼容性。
說到這里,我們承認(rèn)我們目前的設(shè)計(jì)還存在一些缺點(diǎn)。
我們將內(nèi)存分配器元數(shù)據(jù)存儲(chǔ)在持久化內(nèi)存中。當(dāng)一個(gè)程序重新啟動(dòng),我們使用這些元數(shù)據(jù)來重新創(chuàng)建內(nèi)存的程序狀態(tài):分配器和垃圾收集器的相關(guān)狀態(tài)也包括在其中。與任何持久化數(shù)據(jù)一樣,我們需要維護(hù)這個(gè)元數(shù)據(jù)的數(shù)據(jù)布局。任何對(duì)Go內(nèi)存分配器的數(shù)據(jù)結(jié)構(gòu)修改都可能會(huì)破壞我們持久化的元數(shù)據(jù)。可以通過開發(fā)一個(gè)離線工具來解決這個(gè)問題。這樣我們可以將升級(jí)時(shí)的數(shù)據(jù)格式轉(zhuǎn)換功能嵌入到go-pmem中。
目前我們?cè)黾恿巳齻€(gè)新的Go關(guān)鍵字:pnew, pmake和txn。持久化內(nèi)存分配API和txn用來劃分事務(wù)性的數(shù)據(jù)結(jié)構(gòu)的更新。我們已經(jīng)探討了一些方法來避免下文所述的語言變化。
a) pnew/pmake
在未來的Go版本中,對(duì)泛型的支持可以幫助我們避免引入這些內(nèi)存分配函數(shù)。它們可以是普通的Go導(dǎo)出函數(shù)
func?Pnew[T?any](_?T)?*T?{ptr?:=?runtime.pnew(T)return?ptr }func?Pmake[T?any](_?T,?len,?cap?int)?[]T?{slc?:=?runtime.pmake([]T,?len,?cap)return?slc }"runtime.pnew "和 "runtime.pmake "將是特殊的函數(shù),可以取一個(gè)新的函數(shù)。類型作為參數(shù)。它們的行為與new()和make() 這兩個(gè) API非常相似。不過它們是在持久化內(nèi)存堆中分配對(duì)象的。
b) txn
一個(gè)替代的方案是定義一個(gè)新的Go規(guī)則,確定一個(gè)事務(wù)性的代碼塊??梢杂萌缦抡Z法:
//go:transactional {//?transactional?data?updates }還有一種方法可以是使用閉包,并借助一些運(yùn)行時(shí)和編譯器的變化。例如。
runtime.Txn()?foo()這比較類似于Go編譯器在編譯期間存儲(chǔ)mrace/msan flag的做法。在這行代碼的情況下,foo會(huì)被事務(wù)性地執(zhí)行。
playground代碼 [code](https://go2goplay.golang.org/p/WRUTZ9dr5W3),展示了一個(gè)完整的代碼示例,以及我們建議的替代方案。
Implementation
我們的實(shí)現(xiàn)是基于Go 1.15版本的Go源代碼的fork。我們的實(shí)現(xiàn)為Go增加了三個(gè)新的關(guān)鍵字:pnew、pmake和txn。pnew和pmake是持久化的內(nèi)存分配API,而txn是用來標(biāo)志持久化內(nèi)存事務(wù)塊。
pnew -?func pnew(Type) *Type
就像new一樣,pnew也會(huì)創(chuàng)建一個(gè)Type參數(shù)的零值對(duì)象。并返回一個(gè)指向該對(duì)象的指針。
pmake -?func pmake(t Type, size ...IntType) Type
pmakeAPI用于在持久化內(nèi)存中創(chuàng)建slice。語義pmake和Go中的make完全一樣。目前暫時(shí)不支持在 pmem 中創(chuàng)建 map 和 channel。
txn
我們對(duì)Go的代碼修改可以分為兩部分--運(yùn)行時(shí)修改和編譯器-SSA修改。
runtime 的變化
我們擴(kuò)展了Go的運(yùn)行時(shí)以支持持久化的內(nèi)存分配。垃圾收集器現(xiàn)在可以在持久堆和易失堆中工作。mspan?數(shù)據(jù)基礎(chǔ)架構(gòu)有一個(gè)額外的數(shù)據(jù)成員 "memtype",用于區(qū)分持久化和易失性的span。我們還擴(kuò)展了各種內(nèi)存分配器在mcache、mcentral和mheap中的數(shù)據(jù)結(jié)構(gòu),將持久內(nèi)存和易失性內(nèi)存的元數(shù)據(jù)進(jìn)行了區(qū)分。垃圾回收器現(xiàn)在就可以理解這些不同的span類型,并正確地根據(jù)memtype來進(jìn)行不同的處理了。
持久化內(nèi)存是以64MB的倍數(shù)來管理的。每個(gè)持久化內(nèi)存領(lǐng)域在其頭部分有一些元數(shù)據(jù),這些元數(shù)據(jù)是為了方便在應(yīng)用程序崩潰或重新啟動(dòng)時(shí)恢復(fù)堆。這里會(huì)存儲(chǔ)兩種類型的元數(shù)據(jù):
GC堆類型位 - 每個(gè)對(duì)象的 GC 堆類型 bit 都會(huì)被拷貝到 metadata 段以在程序后續(xù)的執(zhí)行中繼續(xù)進(jìn)行使用
Span表 - 捕獲該arena上每個(gè)span的元數(shù)據(jù),以使程序在下次執(zhí)行時(shí),可以根據(jù)這些元數(shù)據(jù)重建堆
我們?cè)谶\(yùn)行時(shí)包中添加了以下API來管理持久化內(nèi)存。
func PmemInit(fname string) (unsafe.Pointer, error)。
用于初始化持久化內(nèi)存。它采用持久化內(nèi)存文件的路徑作為輸入,返回應(yīng)用程序的根指針和一個(gè)錯(cuò)誤值。
func SetRoot(addr unsafe.Pointer) (err Error)。
用于設(shè)置應(yīng)用程序的根指針。所有應(yīng)用程序的數(shù)據(jù)在持久化內(nèi)存掛起這個(gè)根指針。
func GetRoot() (addr unsafe.Pointer)。
返回使用SetRoot()設(shè)置的根指針。
func InPmem(addr unsafe.Pointer) bool。
返回addr是否指向持久化內(nèi)存中的數(shù)據(jù)。
func PersistRange(addr unsafe.Pointer, len uintptr)。
刷新地址范圍(addr,addr+len)內(nèi)的所有緩存,以確保任何更新到這個(gè)內(nèi)存范圍的數(shù)據(jù)都會(huì)被持久存儲(chǔ)。
編譯器-SSA變化
修改parser以識(shí)別三個(gè)新的token--pnew,pmake,和txn。
我們?cè)黾右粋€(gè)新的SSA pass,將所有的存儲(chǔ)操作都寫入到持久化內(nèi)存。因?yàn)槌志没瘍?nèi)存中的數(shù)據(jù)可以在崩潰后存活,所以更新持久化內(nèi)存中的數(shù)據(jù)必須是事務(wù)性的。
對(duì)Go AST和SSA進(jìn)行了修改,現(xiàn)在用戶可以將通過將一個(gè)塊封裝在txn()塊中,將這段Go代碼作為事務(wù)性代碼。
為了做到這一點(diǎn),我們?cè)贕o中添加了一個(gè)名為txn的新關(guān)鍵字。
然后,一個(gè)新的SSA pass將尋找 txn 塊中所有對(duì)持久化內(nèi)存地址的store(OpStore/OpMove/OpZero)操作,并將這些操作的老數(shù)據(jù)存儲(chǔ)在?撤銷日志中。該操作將在進(jìn)行實(shí)際的內(nèi)存更新之前完成。
go-pmem packages
我們開發(fā)了兩個(gè)包,使go-pmem的編寫持久化存儲(chǔ)器的應(yīng)用更容易。
pmem包
它提供了一個(gè)簡(jiǎn)單的Init(fname string) bool?API,應(yīng)用程序可以用它來實(shí)現(xiàn)初始化持久化內(nèi)存。函數(shù)返回結(jié)果表示是不是第一次初始化,如果是則返回 true。如果不是的話,未完成的事務(wù)都會(huì)被 revert。
pmem包還提供了命名對(duì)象,這些名字可以和持久化內(nèi)存中的對(duì)象關(guān)聯(lián)起來。用戶可以字符串名字來創(chuàng)建和獲取這些對(duì)象。
transaction包
事務(wù)包提供了撤消日志記錄的實(shí)現(xiàn),這些日志記錄用于支持程序的崩潰后恢復(fù),保證崩潰時(shí)的一致性。
Example Code
下面是一個(gè)使用go-pmem編寫的簡(jiǎn)單的鏈表應(yīng)用程序。
//?一個(gè)簡(jiǎn)單的鏈接列表應(yīng)用程序。在第一次調(diào)用時(shí),它會(huì)創(chuàng)建一個(gè) //?命名為?"dbRoot?"的持久化內(nèi)存指針,它持有指向第一個(gè) //?也是鏈接列表中的最后一個(gè)元素。每次運(yùn)行時(shí),一個(gè)新的節(jié)點(diǎn)都會(huì)被添加 //?鏈接的列表和列表的所有內(nèi)容都被打印出來。package?mainimport?("github.com/vmware/go-pmem-transaction/pmem""github.com/vmware/go-pmem-transaction/transaction" )const?(//?Used?to?identify?a?successful?initialization?of?the?root?objectmagic?=?0x1B2E8BFF7BFBD154 )//?Structure?of?each?node?in?the?linked?list type?entry?struct?{id???intnext?*entry }//?The?root?object?that?stores?pointers?to?the?elements?in?the?linked?list type?root?struct?{magic?inthead??*entrytail??*entry }//?A?function?that?populates?the?contents?of?the?root?object?transactionally func?populateRoot(rptr?*root)?{txn()?{rptr.magic?=?magicrptr.head?=?nilrptr.tail?=?nil} }//?Adds?a?node?to?the?linked?list?and?updates?the?tail?(and?head?if?empty) func?addNode(rptr?*root)?{entry?:=?pnew(entry)txn()?{entry.id?=?rand.Intn(100)if?rptr.head?==?nil?{rptr.head?=?entry}?else?{rptr.tail.next?=?entry}rptr.tail?=?entry} }func?main()?{firstInit?:=?pmem.Init("database")var?rptr?*rootif?firstInit?{//?Create?a?new?named?object?called?dbRoot?and?point?it?to?rptrrptr?=?(*root)(pmem.New("dbRoot",?rptr))populateRoot(rptr)}?else?{//?Retrieve?the?named?object?dbRootrptr?=?(*root)(pmem.Get("dbRoot",?rptr))if?rptr.magic?!=?magic?{//?An?object?named?dbRoot?exists,?but?its?initialization?did?not//?complete?previously.populateRoot(rptr)}}addNode(rptr)????//?Add?a?new?node?in?the?linked?list }總結(jié)
以上是生活随笔為你收集整理的[译]提案:在Go语言中增加对持久化内存的支持的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 写一个 panic blame 机器人
- 下一篇: Go channel 的妙用