[译]提案:在Go语言中增加对持久化内存的支持
作者:Jerrin Shaji George、Mohit Verma、Rajesh Venkatasubramanian、Pratap Subrahmanyam Jerrin Shaji George, Mohit Verma, Rajesh Venkatasubramanian, Pratap Subrahmanyam.
最后更新。2021年1月20日
討論地點:https://golang.org/issue/43810。
摘要
持久化存儲器是一種新的存儲器技術,其有接近DRAM的訪問速度,并提供類似磁盤的持久化。Linux和Windows服務器已經支持持久內存,服務器可用的商用硬件現在也已經推出了。關于這項技術的更多細節可以在pmem.io找到。
本文檔是為 Go 增加 pmem 支持的提案文檔,具體的詳細設計可以參考我們發表的2020年USENIX ATC論文[go-pmem](https://www.usenix.org/system/files/atc20-george.pdf)。基于Go 1.15版本的上述設計的實現,可以在以下網站找到此處。
背景
持久化存儲是一種新型的隨機存取存儲器,它提供了持久化的功能。并以類似DRAM的訪問速度實現尋址。操作系統提供了將該內存映射到應用程序的虛擬地址的能力。應用程序可以像使用內存一樣使用這個mmap區域。更新到持久化內存的數據,即使是崩潰/重啟后,這些數據依然能夠被正常使用。
使用持久化內存的應用程序在很多方面都有好處。由于數據更新到持久化內存是非易失性的,應用不再需要維護 DRAM 和存儲設備之間的數據關系,不需要在DRAM和存儲設備之間調配數據。相當一部分的應用程序代碼可以直接退役了。
另一個大的優勢是顯著減少了應用程序重新啟動時的啟動時間。這是因為應用程序不再需要把持久化的數據和內存中的數據進行轉換。商業應用SAP HANA給出的報告可以看到性能有 12 倍的提升:[12x improvement](https://cloud.google.com/blog/topics/partners/available-first-on-google-cloud-intel-optane-dc-persistent-memory)。
這個proposal是要為持久化內存提供原生支持,在Go語言中,我們的設計修改了Go 1.15,引入了一個垃圾收集的持久化的方法。我們還在 Go 編譯器中引入了新語義,以支持事務性更新到持久化內存數據結構。我們把我們修改后的Go套件稱為go-pmem。使用go-pmem開發的Redis數據庫與在NVMe SSD上運行的Redis相比,吞吐量提高了5倍。
提案
我們建議在Go中增加對持久化內存編程的本地支持。這需要在Go中提供以下功能。
支持持久化的內存分配
對持久化內存堆對象進行垃圾收集。
修改持久化內存數據結構需要保證“崩潰時的一致性”
使應用程序能夠在崩潰/重新啟動后恢復。
支持應用程序從持久化內存中恢復存儲的數據。
為了支持這些功能,我們擴展了Go運行時,并添加了一個新的SSA pass。我們的實現在后文中闡述。
理由
現在已經存在一些庫,如Intel PMDK,為C和C++開發人員提供了支持持久化內存編程的開發工具。其他編程語言,如Java和Python,正在探索如何支持。
例如:
Java - https://bugs.openjdk.java.net/browse/JDK-8207851
Python - https://pynvm.readthedocs.io/en/v0.3.1/
但是目前還沒有哪種語言原生地對持久化內存進行支持。我們認為這是對推廣pmem技術的一種障礙。這個提案就是要讓Go成為第一個原生完全支持持久化內存的語言。
為什么要改變語言?
C庫暴露了一個與現有編程模型明顯不同(而且復雜)的編程模型。內存管理對于一個語言的外部庫來說其實是很困難的。漏掉一個 "free "調用就會導致內存泄漏,而在持續化內存中,如果發生泄漏就是永久性的,不會在應用重新啟動后消失。在Go這樣有運行時的語言中,使本來只給垃圾收集管理的內存讓外部庫可見還是很困難的。為了能提供事務性的語義,需要對持久化內存的寫操作進行定制和組織,這也需要對語言進行修改。經過我們的實踐,對Go的編譯器和運行時進行增量修改還是比較容易的。
兼容性
我們目前的修改保留了Go 1.x未來兼容性的承諾。它做到了不會破壞不使用任何持久化內存功能的程序的兼容性。
說到這里,我們承認我們目前的設計還存在一些缺點。
我們將內存分配器元數據存儲在持久化內存中。當一個程序重新啟動,我們使用這些元數據來重新創建內存的程序狀態:分配器和垃圾收集器的相關狀態也包括在其中。與任何持久化數據一樣,我們需要維護這個元數據的數據布局。任何對Go內存分配器的數據結構修改都可能會破壞我們持久化的元數據。可以通過開發一個離線工具來解決這個問題。這樣我們可以將升級時的數據格式轉換功能嵌入到go-pmem中。
目前我們增加了三個新的Go關鍵字:pnew, pmake和txn。持久化內存分配API和txn用來劃分事務性的數據結構的更新。我們已經探討了一些方法來避免下文所述的語言變化。
a) pnew/pmake
在未來的Go版本中,對泛型的支持可以幫助我們避免引入這些內存分配函數。它們可以是普通的Go導出函數
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 "將是特殊的函數,可以取一個新的函數。類型作為參數。它們的行為與new()和make() 這兩個 API非常相似。不過它們是在持久化內存堆中分配對象的。
b) txn
一個替代的方案是定義一個新的Go規則,確定一個事務性的代碼塊。可以用如下語法:
//go:transactional {//?transactional?data?updates }還有一種方法可以是使用閉包,并借助一些運行時和編譯器的變化。例如。
runtime.Txn()?foo()這比較類似于Go編譯器在編譯期間存儲mrace/msan flag的做法。在這行代碼的情況下,foo會被事務性地執行。
playground代碼 [code](https://go2goplay.golang.org/p/WRUTZ9dr5W3),展示了一個完整的代碼示例,以及我們建議的替代方案。
Implementation
我們的實現是基于Go 1.15版本的Go源代碼的fork。我們的實現為Go增加了三個新的關鍵字:pnew、pmake和txn。pnew和pmake是持久化的內存分配API,而txn是用來標志持久化內存事務塊。
pnew -?func pnew(Type) *Type
就像new一樣,pnew也會創建一個Type參數的零值對象。并返回一個指向該對象的指針。
pmake -?func pmake(t Type, size ...IntType) Type
pmakeAPI用于在持久化內存中創建slice。語義pmake和Go中的make完全一樣。目前暫時不支持在 pmem 中創建 map 和 channel。
txn
我們對Go的代碼修改可以分為兩部分--運行時修改和編譯器-SSA修改。
runtime 的變化
我們擴展了Go的運行時以支持持久化的內存分配。垃圾收集器現在可以在持久堆和易失堆中工作。mspan?數據基礎架構有一個額外的數據成員 "memtype",用于區分持久化和易失性的span。我們還擴展了各種內存分配器在mcache、mcentral和mheap中的數據結構,將持久內存和易失性內存的元數據進行了區分。垃圾回收器現在就可以理解這些不同的span類型,并正確地根據memtype來進行不同的處理了。
持久化內存是以64MB的倍數來管理的。每個持久化內存領域在其頭部分有一些元數據,這些元數據是為了方便在應用程序崩潰或重新啟動時恢復堆。這里會存儲兩種類型的元數據:
GC堆類型位 - 每個對象的 GC 堆類型 bit 都會被拷貝到 metadata 段以在程序后續的執行中繼續進行使用
Span表 - 捕獲該arena上每個span的元數據,以使程序在下次執行時,可以根據這些元數據重建堆
我們在運行時包中添加了以下API來管理持久化內存。
func PmemInit(fname string) (unsafe.Pointer, error)。
用于初始化持久化內存。它采用持久化內存文件的路徑作為輸入,返回應用程序的根指針和一個錯誤值。
func SetRoot(addr unsafe.Pointer) (err Error)。
用于設置應用程序的根指針。所有應用程序的數據在持久化內存掛起這個根指針。
func GetRoot() (addr unsafe.Pointer)。
返回使用SetRoot()設置的根指針。
func InPmem(addr unsafe.Pointer) bool。
返回addr是否指向持久化內存中的數據。
func PersistRange(addr unsafe.Pointer, len uintptr)。
刷新地址范圍(addr,addr+len)內的所有緩存,以確保任何更新到這個內存范圍的數據都會被持久存儲。
編譯器-SSA變化
修改parser以識別三個新的token--pnew,pmake,和txn。
我們增加一個新的SSA pass,將所有的存儲操作都寫入到持久化內存。因為持久化內存中的數據可以在崩潰后存活,所以更新持久化內存中的數據必須是事務性的。
對Go AST和SSA進行了修改,現在用戶可以將通過將一個塊封裝在txn()塊中,將這段Go代碼作為事務性代碼。
為了做到這一點,我們在Go中添加了一個名為txn的新關鍵字。
然后,一個新的SSA pass將尋找 txn 塊中所有對持久化內存地址的store(OpStore/OpMove/OpZero)操作,并將這些操作的老數據存儲在?撤銷日志中。該操作將在進行實際的內存更新之前完成。
go-pmem packages
我們開發了兩個包,使go-pmem的編寫持久化存儲器的應用更容易。
pmem包
它提供了一個簡單的Init(fname string) bool?API,應用程序可以用它來實現初始化持久化內存。函數返回結果表示是不是第一次初始化,如果是則返回 true。如果不是的話,未完成的事務都會被 revert。
pmem包還提供了命名對象,這些名字可以和持久化內存中的對象關聯起來。用戶可以字符串名字來創建和獲取這些對象。
transaction包
事務包提供了撤消日志記錄的實現,這些日志記錄用于支持程序的崩潰后恢復,保證崩潰時的一致性。
Example Code
下面是一個使用go-pmem編寫的簡單的鏈表應用程序。
//?一個簡單的鏈接列表應用程序。在第一次調用時,它會創建一個 //?命名為?"dbRoot?"的持久化內存指針,它持有指向第一個 //?也是鏈接列表中的最后一個元素。每次運行時,一個新的節點都會被添加 //?鏈接的列表和列表的所有內容都被打印出來。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 }總結
以上是生活随笔為你收集整理的[译]提案:在Go语言中增加对持久化内存的支持的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 写一个 panic blame 机器人
- 下一篇: Go channel 的妙用