从零实现一个数据库(DataBase) Go语言实现版 6.持久化到磁盘

英文源地址

持久化至磁盘

前一章中的b树数据结构可以很容易地转存到磁盘上.让我们在它之上建立一个简单地kv存储. 由于我们的b树实现是不可变的, 我们将以仅追加的方式分配磁盘空间, 重用磁盘空间将推迟到下一章.

持久化数据的方式

正如前面章节所提到的, 将数据持久化到磁盘不仅仅是将数据转存到文件中. 这里有几个要考虑的因素:

  1. 崩溃恢复: 这包括数据库进程崩溃, 操作系统崩溃和电源故障.重启后数据库必须处于可用状态.
  2. 持久性: 在数据库成功响应后, 所涉及的数据可以保证持久化, 即使在崩溃之后也是如此. 换句话说, 持久化在响应客户端之前发生.

有许多描述数据库的专业术语ACID(原子性, 一致性, 隔离性, 持久性), 但这些概念不是正交的, 很难解释, 所以让我们转而关注我们的实际示例.

  1. b树的不可变方面: 更新b树不会触及b树的前一个版本, 这使得崩溃恢复变得容易–如果更新出错, 我们可以简单地恢复到前一个版本.
  2. 持久性是通过fsync linux系统调用实现的.通过write或mmap的普通文件IO首先进入页缓存(page cache), 系统稍后必须将页缓存(page cache)刷新(flush)到磁盘上.fsync系统调用阻塞, 知道所有脏页都被刷新.

如果更新出错, 我们将如何恢复到以前的版本? 我们可以将更新分为两个阶段:

  1. 更新操作创建新的节点, 将它们写入磁盘.
  2. 每次更新都会创建一个新的根节点, 我们需要将指向根节点的指针存储在某个地方.

第一个阶段可能涉及将多个页面写入磁盘, 这通常不是原子操作.但是第二阶段只涉及单个指针, 并且可以在原子的单页写入操作中完成. 这使得整个操作原子化–如果数据库崩溃, 更新将不会发生.
第一个阶段必须在第二个阶段前持久化, 否则, 根节点指针可能会在崩溃后指向一个损坏的(部份持久化的)树版本.这两个阶段之间应该有一个fsync(作为一个屏障)
第二阶段也应该在响应客户端之前进行fsync.

基于mmap的IO

可以使用mmap系统调用从虚拟地址映射磁盘文件的内容. 从这个地址读取将启动透明磁盘IO, 这与通过read系统调用读取文件相同, 但不需要用户空间缓冲区(buffer)和系统调用的开销. 映射地址是页缓存(page cache)的代理, 通过它修改数据与write系统调用相同.
mmap很方便, 我们将在KV存储中使用它. 然而, 使用mmap并不是必需的.

func mmapInit(fp *os.File) (int, []byte, error) {
	fi, err := fp.Stat()
	if err != nil {
		return 0, nil, fmt.Errorf("stat: %w", err)
	}
	if fi.Size()%BTREE_PAGE_SIZE == 0 {
		return 0, nil, errors.New("File size is not a multiple of page size.")
	}
	mmapSize := 64 << 20
	if mmapSize%BTREE_PAGE_SIZE != 0 {
		panic("wrong mmapsize")
	}
	for mmapSize < int(fi.Size()) {
		mmapSize *= 2
	}
	chunk, err := syscall.Mmap(int(fp.Fd()), 0, mmapSize, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
	if err != nil {
		return 0, nil, fmt.Errorf("mmap: %w", err)
	}
	return int(fi.Size()), chunk, nil
}

上面的函数创建了至少与文件大小相等的初始映射. 映射的大小可以大于文件大小, 并且超过文件末尾的范围是不可访问的(SIGBUS), 但是可以稍后拓展文件.
随着文件的增长, 我们可能需要扩展映射的范围. 扩展mmap范围的系统调用是mremap.不幸的是, 当通过重新映射扩展范围时. 我们可能无法保留起始地址. 我们扩展映射的方法是使用多个映射–为溢出文件范围创建一个新的映射.

type KV struct {
	Path string
	fp *os.File
	tree BTree
	mmap struct{
		file int
		total int
		chunks [][]byte
	}
	page struct{
		flushed uint64
		temp [][]byte
	}
}

func extendMmap(db *KV, npages int) error {
	if db.mmap.total >= npages*BTREE_PAGE_SIZE {
		return nil
	}
	chunk, err := syscall.Mmap(
		int(db.fp.Fd()), int64(db.mmap.total), db.mmap.total,
		syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
	if err != nil {
		return fmt.Errorf("mmap: %w", err)
	}
	db.mmap.total += db.mmap.total
	db.mmap.chunks = append(db.mmap.chunks, chunk)
	return nil
}

新映射的大小呈指数增长, 因此我们不必频繁调用mmap.
下面是我们如何从映射地址访问一个页.

func (db *KV) pageGet(ptr uint64) BNode {
	start := uint64(0)
	for _, chunk := range db.mmap.chunks {
		end := start + uint64(len(chunk))/BTREE_PAGE_SIZE
		if ptr < end {
			offset := BTREE_PAGE_SIZE * (ptr - start)
			return BNode{chunk[offset: offset+BTREE_PAGE_SIZE]}
		}
		start = end
	}
	panic("bad ptr")
}

主页 MasterPage

文件的第一页用于存储指向根节点的指针, 我们称之为’master page’.分配新节点所需的总页数, 因此页存储在那里.在这里插入图片描述
下面的函数在初始化数据库时读取master page.

const DB_SIG = "BuildYourOwnDB05"

func masterLoad(db *KV) error {
	if db.mmap.file == 0 {
		db.page.flushed = 1
		return nil
	}
	data := db.mmap.chunks[0]
	root := binary.LittleEndian.Uint64(data[16:])
	used := binary.LittleEndian.Uint64(data[24:])

	if !bytes.Equal([]byte(DB_SIG), data[:16]) {
		return errors.New("Bad signature")
	}
	bad := !(1 <= used && used <= uint64(db.mmap.file/BTREE_PAGE_SIZE))
	bad = bad || !(0 <= root && root < used)
	if bad {
		return errors.New("Bad master page.")
	}
	db.tree.root = root
	db.page.flushed = used

	return nil
}

下面是更新master page的功能. 与用于读取的代码不同, 它不使用映射地址进行写入.这是因为通过mmap修改页不是原子性的. 内核可能会在中途刷新页并损坏磁盘文件, 而不跨越页边界的小的write操作则保证是原子性的.

分配自盘页

我们将简单地将新的页添加到数据库的末尾, 直到在下一章中添加空闲列表.
新的页暂时保存在内存中, 直到稍后(在可能拓展文件之后)复制到文件中.

func (db *KV) pageNew(node BNode) uint64 {
	if len(node.data) > BTREE_PAGE_SIZE {
		panic("error size")
	}
	ptr := db.page.flushed + uint64(len(db.page.temp))
	db.page.temp = append(db.page.temp, node.data)
	return ptr
}

func (db *KV) pageDel(uint64)  {
	
}

在写入挂起的页面之前, 我们可能首先扩展文件. 对应的系统调用是fallocate

func extendFile(db *KV, npages int) error {
	filePages := db.mmap.file / BTREE_PAGE_SIZE
	if filePages >= npages {
		return nil
	}

	for filePages < npages {
		inc := filePages / 8
		if inc < 1 {
			inc = 1
		}
		filePages += inc
	}
	
	fileSize := filePages * BTREE_PAGE_SIZE
	err := syscall.Fallocate(int(db.fp.Fd()), 0, 0, int64(fileSize))
	if err != nil {
		return fmt.Errorf("fallocate: %w", err)
	}
	
	db.mmap.file = fileSize
	return nil
}

初始化数据库

将我们已经完成的放在一起

func (db *KV) Open() error {
	fp, err := os.OpenFile(db.Path, os.O_RDWR|os.O_CREATE, 0664)
	if err != nil {
		return fmt.Errorf("OpenFile: %w", err)
	}
	db.fp = fp
	
	sz, chunk, err := mmapInit(db.fp)
	if err != nil {
		goto fail
	}
	db.mmap.file = sz
	db.mmap.total = len(chunk)
	db.mmap.chunks = [][]byte{chunk}
	
	db.tree.get = db.pageGet
	db.tree.new = db.pageNew
	db.tree.del = db.pageDel
	
	err = masterLoad(db)
	if err != nil {
		goto fail
	}
	
	return nil
	
	fail:
		db.Close()
		return fmt.Errorf("KV.Open: %w", err)
}

func (db *KV) Close()  {
	for _, chunk := range db.mmap.chunks {
		err := syscall.Munmap(chunk)
		if err != nil {
			panic("error")
		}
	}
	_ = db.fp.Close()
}

更新操作

与查询不同, 更新操作在返回之前必须持久化数据

func (db *KV) Get(key []byte) ([]byte, bool)  {
	return db.tree.Get(key)
}

func (db *KV) Set(key []byte, val []byte) error {
	db.tree.Insert(key, val)
	return flushPages(db)
}

func (db *KV) Del(key []byte) (bool, error) {
	deleted := db.tree.Delete(key)
	return deleted, flushPages(db)
}

flushPages是持久化新页的函数

func flushPages(db *KV) error {
	if err := writePages(db); err != nil {
		return err
	}

	return syncPages(db)
}

如前所述, 它分为两个阶段

func writePages(db *KV) error {
	npages := int(db.page.flushed) + len(db.page.temp)
	if err := extendFile(db, npages); err!= nil {
		return err
	}
	if err := extendMmap(db, npages); err != nil {
		return err
	}

	for i, page := range db.page.temp {
		ptr := db.page.flushed + uint64(i)
		copy(db.pageGet(ptr).data, page)
	}
	
	return nil
}

fsync在它们之间和之后.

func syncPages(db *KV) error {
	if err := db.fp.Sync(); err != nil {
		return fmt.Errorf("fsync: %w", err)
	}
	db.page.flushed += uint64(len(db.page.temp))
	db.page.temp = db.page.temp[:0]

	if err := masterLoad(db); err != nil {
		return err
	}
	if err := db.fp.Sync(); err != nil {
		return fmt.Errorf("fsync: %w", err)
	}
	return nil
}

我们的KV存储是功能性的, 但是当我们更新数据库时, 文件不可能永远增长, 我们将在下一章通过重用磁盘页来完成kv存储.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值