作者: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,正在探索如何支持。
例如:
但是目前还没有哪种语言原生地对持久化内存进行支持。我们认为这是对推广pmem技术的一种障碍。这个提案就是要让Go成为第一个原生完全支持持久化内存的语言。
C库暴露了一个与现有编程模型明显不同(而且复杂)的编程模型。内存管理对于一个语言的外部库来说其实是很困难的。漏掉一个 "free "调用就会导致内存泄漏,而在持续化内存中,如果发生泄漏就是永久性的,不会在应用重新启动后消失。在Go这样有运行时的语言中,使本来只给垃圾收集管理的内存让外部库可见还是很困难的。为了能提供事务性的语义,需要对持久化内存的写操作进行定制和组织,这也需要对语言进行修改。经过我们的实践,对Go的编译器和运行时进行增量修改还是比较容易的。
我们目前的修改保留了Go 1.x未来兼容性的承诺。它做到了不会破坏不使用任何持久化内存功能的程序的兼容性。
说到这里,我们承认我们目前的设计还存在一些缺点。
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),展示了一个完整的代码示例,以及我们建议的替代方案。
我们的实现是基于Go 1.15版本的Go源代码的fork。我们的实现为Go增加了三个新的关键字:pnew、pmake和txn。pnew和pmake是持久化的内存分配API,而txn是用来标志持久化内存事务块。
func pnew(Type) *Type
就像new
一样,pnew
也会创建一个Type
参数的零值对象。并返回一个指向该对象的指针。
func pmake(t Type, size ...IntType) Type
pmake
API用于在持久化内存中创建slice。语义pmake
和Go中的make
完全一样。目前暂时不支持在 pmem 中创建 map 和 channel。
txn() {
// transaction data updates
}
我们对Go的代码修改可以分为两部分--运行时修改和编译器-SSA修改。
我们扩展了Go的运行时以支持持久化的内存分配。垃圾收集器现在可以在持久堆和易失堆中工作。mspan
数据基础架构有一个额外的数据成员 "memtype",用于区分持久化和易失性的span。我们还扩展了各种内存分配器在mcache、mcentral和mheap中的数据结构,将持久内存和易失性内存的元数据进行了区分。垃圾回收器现在就可以理解这些不同的span类型,并正确地根据memtype来进行不同的处理了。
持久化内存是以64MB的倍数来管理的。每个持久化内存领域在其头部分有一些元数据,这些元数据是为了方便在应用程序崩溃或重新启动时恢复堆。这里会存储两种类型的元数据:
我们在运行时包中添加了以下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)内的所有缓存,以确保任何更新到这个内存范围的数据都会被持久存储。
pnew
,pmake
,和txn
。txn()
块中,将这段Go代码作为事务性代码。txn
的新关键字。OpStore
/OpMove
/OpZero
)操作,并将这些操作的老数据存储在 撤销日志中。该操作将在进行实际的内存更新之前完成。我们开发了两个包,使go-pmem的编写持久化存储器的应用更容易。
它提供了一个简单的Init(fname string) bool
API,应用程序可以用它来实现初始化持久化内存。函数返回结果表示是不是第一次初始化,如果是则返回 true。如果不是的话,未完成的事务都会被 revert。
pmem包还提供了命名对象,这些名字可以和持久化内存中的对象关联起来。用户可以字符串名字来创建和获取这些对象。
事务包提供了撤消日志记录的实现,这些日志记录用于支持程序的崩溃后恢复,保证崩溃时的一致性。
下面是一个使用go-pmem编写的简单的链表应用程序。
// 一个简单的链接列表应用程序。在第一次调用时,它会创建一个
// 命名为 "dbRoot "的持久化内存指针,它持有指向第一个
// 也是链接列表中的最后一个元素。每次运行时,一个新的节点都会被添加
// 链接的列表和列表的所有内容都被打印出来。
package main
import (
"github.com/vmware/go-pmem-transaction/pmem"
"github.com/vmware/go-pmem-transaction/transaction"
)
const (
// Used to identify a successful initialization of the root object
magic = 0x1B2E8BFF7BFBD154
)
// Structure of each node in the linked list
type entry struct {
id int
next *entry
}
// The root object that stores pointers to the elements in the linked list
type root struct {
magic int
head *entry
tail *entry
}
// A function that populates the contents of the root object transactionally
func populateRoot(rptr *root) {
txn() {
rptr.magic = magic
rptr.head = nil
rptr.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 *root
if firstInit {
// Create a new named object called dbRoot and point it to rptr
rptr = (*root)(pmem.New("dbRoot", rptr))
populateRoot(rptr)
} else {
// Retrieve the named object dbRoot
rptr = (*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
}