最近工作中在研究Hyperledger Fabric区块链开源项目,其中区块链peer节点底层使用了leveldb作为State Database的存储介质,出于好奇,决定对一些常用的KV存储做一些研究。
这一次我主要对2种KV存储的源码做了分析,一个是BoltDB,这是LMDB的Go语言版本,另一个就是goleveldb。
在阅读BoltDB项目源码的过程中,我发现它将持久化文件以只读模式通过mmap映射到内存空间中,然后通过索引找到内存中key对应的value所指向的空间,然后将这段内存返回给用户。之前虽然也听说过内存映射文件技术,但一直没有实际使用过,因此这一次我决定对mmap做一些尝试,以下是这次尝试的过程。
文件写入
内存映射文件(Memory-mapped file)将一段虚拟内存逐字节对应于一个文件或类文件的资源,使得应用程序处理映射部分如同访问主存,用于增加I/O性能。Mmap函数存在于Go's syscall package中,它接收一个文件描述符,需要映射的大小(返回的切片容量)以及需要的内存保护和映射类型。
package main
import (
"fmt"
"os"
"syscall"
)
const maxMapSize = 0x8000000000
const maxMmapStep = 1 << 30 // 1GB
func main() {
file, err := os.OpenFile("my.db", os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
defer file.Close()
stat, err := os.Stat("my.db")
if err != nil {
panic(err)
}
size, err := mmapSize(int(stat.Size()))
if err != nil {
panic(err)
}
b, err := syscall.Mmap(int(file.Fd()), 0, size, syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil {
panic(err)
}
for index, bb := range []byte("Hello world") {
b[index] = bb
}
err = syscall.Munmap(b)
if err != nil {
panic(err)
}
}
func mmapSize(size int) (int, error) {
// Double the size from 32KB until 1GB.
for i := uint(15); i <= 30; i++ {
if size <= 1< maxMapSize {
return 0, fmt.Errorf("mmap too large")
}
// If larger than 1GB then grow by 1GB at a time.
sz := int64(size)
if remainder := sz % int64(maxMmapStep); remainder > 0 {
sz += int64(maxMmapStep) - remainder
}
// Ensure that the mmap size is a multiple of the page size.
// This should always be true since we're incrementing in MBs.
pageSize := int64(os.Getpagesize())
if (sz % pageSize) != 0 {
sz = ((sz / pageSize) + 1) * pageSize
}
// If we've exceeded the max size then only grow up to the max size.
if sz > maxMapSize {
sz = maxMapSize
}
return int(sz), nil
}
如果你直接运行这个程序,那么将会发生错误(内存地址会不一样)
unexpected fault address 0x13ac000
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x13ac000 pc=0x10c1375]
SIGBUS
信号意味着你在文件的地址以外写入内容,根据Linux man pages mmap(2)的描述:
SIGBUS Attempted access to a portion of the buffer that does not correspond to the file (for example, beyond the end of the file, including the case where another process has truncated the file).
在创建新文件时,它最初为空,即大小为0字节,您需要使用ftruncate
调整其大小,至少足以包含写入的地址(可能四舍五入到页面大小)。修改main
函数:
func main() {
file, err := os.OpenFile("my.db", os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
defer file.Close()
stat, err := os.Stat("my.db")
if err != nil {
panic(err)
}
size, err := mmapSize(int(stat.Size()))
if err != nil {
panic(err)
}
err = syscall.Ftruncate(int(file.Fd()), int64(size))
if err != nil {
panic(err)
}
b, err := syscall.Mmap(int(file.Fd()), 0, size, syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil {
panic(err)
}
for index, bb := range []byte("Hello world") {
b[index] = bb
}
err = syscall.Munmap(b)
if err != nil {
panic(err)
}
}
再次运行程序,可正常写入。
读取文件
读取文件的方式更加简单,直接以只读方式将文件映射到主存中即可:
func main() {
file, err := os.OpenFile("my.db", os.O_RDONLY, 0600)
if err != nil {
panic(err)
}
defer file.Close()
stat, err := os.Stat("my.db")
if err != nil {
panic(err)
}
b, err := syscall.Mmap(int(file.Fd()), 0, int(stat.Size()), syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil {
panic(err)
}
defer syscall.Munmap(b)
fmt.Println(string(b))
}
运行程序,即可打印出文件内容