page
type pgid uint64
type page struct {
id pgid
flags uint16
count uint16
overflow uint32
}
page
定义是一个或者连续多个page size
数据页的(在下面图均用page header
代替),每个数据页分为page header
和page data
两部分。下面两个图分别展示了一个数据页和多个数据页存储一个节点数据的场景,其中示例的数据页大小为4k
,实际大小由用户自定义配置,或者操作系统决定。
page
中四个属性含义分别如下:
-
id
为当前数据页的pgid
。boltdb
把数据按照操作系统的page size
大小分配,这样的话在读写磁盘的时候可以有较好的性能。在根据
pgid
读写数据的时候,可以使用pgid
以及page size
迅速找到对应数据的位置:-
写
此操作在
func( tx *Tx) write() error
中,其中找到page
的offset
的操作为offset := int64(p.id) * int64(tx.db.pageSize)
// Write pages to disk in order. for _, p := range pages { rem := (uint64(p.overflow) + 1) * uint64(tx.db.pageSize) offset := int64(p.id) * int64(tx.db.pageSize) var written uintptr // Write out page in "max allocation" sized chunks. for { sz := rem if sz > maxAllocSize-1 { sz = maxAllocSize - 1 } buf := unsafeByteSlice(unsafe.Pointer(p), written, 0, int(sz)) if _, err := tx.db.ops.writeAt(buf, offset); err != nil { return err } // Update statistics. tx.stats.Write++ // Exit inner for loop if we've written all the chunks. rem -= sz if rem == 0 { break } // Otherwise move offset forward and move pointer to next chunk. offset += int64(sz) written += uintptr(sz) } }
-
读
根据
pgid
找到page
在文件中的offset
来读取page
// page retrieves a page reference from the mmap based on the current page size. func (db *DB) page(id pgid) *page { pos := id * pgid(db.pageSize) return (*page)(unsafe.Pointer(&db.data[pos])) }
-
-
flags
表示当前数据页的四种类型,在后面会分别做解释。func (p *page) typ() string { if (p.flags & branchPageFlag) != 0 { return "branch" } else if (p.flags & leafPageFlag) != 0 { return "leaf" } else if (p.flags & metaPageFlag) != 0 { return "meta" } else if (p.flags & freelistPageFlag) != 0 { return "freelist" } return fmt.Sprintf("unknown<%02x>", p.flags) }
-
count
代表元素数量,在如下三种类型的时候有数量的含义:branch
类型的时候,表示非叶子节点元素数量。,非叶子节点只存储索引。leaf
类型的时候,表示叶子节点中元素数量。freelist
类型的时候,表示可用pgid
的数量。
-
overflow
,当节点数据超过page size
的时候,数据需要由多个数据页才能够保存。在overflow
大于0的时候,表示后续还有连续overflow
个数据页共属于当前数据页。下面代码为给一个tx
分配count
个数据页的代码。// allocate returns a contiguous block of memory starting at a given page. func (db *DB) allocate(txid txid, count int) (*page, error) { // Allocate a temporary buffer for the page. var buf []byte if count == 1 { buf = db.pagePool.Get().([]byte) } else { buf = make([]byte, count*db.pageSize) } p := (*page)(unsafe.Pointer(&buf[0])) // Note: 注意此处 p.overflow = uint32(count - 1) // Use pages from the freelist if they are available. if p.id = db.freelist.allocate(txid, count); p.id != 0 { return p, nil } // Resize mmap() if we're at the end. p.id = db.rwtx.meta.pgid var minsz = int((p.id+pgid(count))+1) * db.pageSize if minsz >= db.datasz { if err := db.mmap(minsz); err != nil { return nil, fmt.Errorf("mmap allocate error: %s", err) } } // Move the page id high water mark. db.rwtx.meta.pgid += pgid(count) return p, nil }
page 类型
branch 非叶子节点
type branchPageElement struct {
pos uint32
ksize uint32
pgid pgid
}
// key returns a byte slice of the node key.
func (n *branchPageElement) key() []byte {
return unsafeByteSlice(unsafe.Pointer(n), 0, int(n.pos), int(n.pos)+int(n.ksize))
}
branchPageElement
三个属性含义分别如下:
pos
为key
相对branchPageElement
的偏移量ksize
对应的key
的长度pgid
子节点对应的pgid
下图中展示了branch
类型的数据在磁盘上的布局:
leaf 叶子节点
type leafPageElement struct {
flags uint32
pos uint32
ksize uint32
vsize uint32
}
// key returns a byte slice of the node key.
func (n *leafPageElement) key() []byte {
i := int(n.pos)
j := i + int(n.ksize)
return unsafeByteSlice(unsafe.Pointer(n), 0, i, j)
}
// value returns a byte slice of the node value.
func (n *leafPageElement) value() []byte {
i := int(n.pos) + int(n.ksize)
j := i + int(n.vsize)
return unsafeByteSlice(unsafe.Pointer(n), 0, i, j)
}
leafPageElement
的四个属性含义分别如下:
flags
表示当前value
是正常的value
,还是一个bucket
。pos
为key
和value
的相对leafPageElement
的偏移量。ksize
表示当前leafPageElement
的key
的大小。vsize
表示当前leafPageElement
的value
的大小。
下图展示了leaf
类型的在磁盘上的布局:
meta 元数据
type bucket struct {
root pgid // page id of the bucket's root-level page
sequence uint64 // monotonically incrementing, used by NextSequence()
}
type meta struct {
magic uint32
version uint32
pageSize uint32
flags uint32
root bucket
freelist pgid
pgid pgid
txid txid
checksum uint64
}
meta
中的各元素属性如下:
-
magic
、version
和checksum
magic
和version
都是代码中写死的数字。checksum
是对meta
中除了checksum
计算的一个checksum
,用来校验meta
的完整性。// The data file format version. const version = 2 // Represents a marker value to indicate that a file is a Bolt DB. const magic uint32 = 0xED0CDAED func (m *meta) validate() error { if m.magic != magic { return ErrInvalid } else if m.version != version { return ErrVersionMismatch } else if m.checksum != m.sum64() { return ErrChecksum } return nil }
-
pageSize
是创建db的时候,配置的pageSize
,或者操作系统的pageSize
。 -
flags
暂未使用 -
root
表示boltdb
的根节点 -
freelist
表示对应数据页的pgid
-
pgid
表示整个boltdb
中已用pgid
的水位线,所有数据都不应该保存在大于等于pgid
对应的数据页。 -
txid
表示meta
对应写事务的事务id
下图展示了meta
类型数据在磁盘上的布局:
freelist 空闲pgid
空闲的pgid
可以写入内容。下面是freelist
存储到物理到page
的部分代码。
// write writes the page ids onto a freelist page. All free and pending ids are
// saved to disk since in the event of a program crash, all pending ids will
// become free.
func (f *freelist) write(p *page) error {
// Combine the old free pgids and pgids waiting on an open transaction.
// Update the header flag.
p.flags |= freelistPageFlag
// The page.count can only hold up to 64k elements so if we overflow that
// number then we handle it by putting the size in the first element.
l := f.count()
if l == 0 {
p.count = uint16(l)
} else if l < 0xFFFF {
p.count = uint16(l)
var ids []pgid
data := unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))
unsafeSlice(unsafe.Pointer(&ids), data, l)
f.copyall(ids)
} else {
p.count = 0xFFFF
var ids []pgid
data := unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))
unsafeSlice(unsafe.Pointer(&ids), data, l+1)
ids[0] = pgid(l)
f.copyall(ids[1:])
}
return nil
}
根据pgid
的数量决定其存储位置:
-
pgid
数量少于64k
的时候,由于page
中属性count
最大值是64k-1
,所以pageHeader
中的count
是足够的。这个时候freelist
布局如下: -
pgid
数量大于等于64k
的时候,由于超过了page
中count
的最大值64k-1
,所以pageHeader
中的count
是不够的。可以使用data
部分第一个uint64
来存储count
。通过让page.count=64k-1
,这样的话在读取的时候知道data
部分第一个uint64
存储的是pgid
数组大小。这个时候freelist
布局如下:
bucket
bucket
是boltdb
中组织数据的基本单位。meta
中保存了boltdb
根节点bucket
,所有数据查找从此节点开始。
- 每个
bucket
是一个B+树 - 根节点是一个
bucket
bucket
中可以包含bucket
下图中展示bucket
在实际存储的时候一个布局。由于bucket
中才可以存储数据,所以root bucket
这个B+
树中叶子节点的均为bucket
类型。图中只选择展示了key=d
这个bucket
数据分布,其他bucket
和这个类似。
上图中的
bucket
是一个单独的数据页保存的,实际上还有内联的场景,即bucket
的数据和其他key
以及value
保存在一个叶子节点上。可以内联的bucket
需要满足如下三个条件:
bucket
只包括一个叶子节点bucket
中不包含其他bucket
bucket
中所有数据占用空间小于等于page_size/4
// inlineable returns true if a bucket is small enough to be written inline
// and if it contains no subbuckets. Otherwise returns false.
func (b *Bucket) inlineable() bool {
var n = b.rootNode
// Bucket must only contain a single leaf node.
if n == nil || !n.isLeaf {
return false
}
// Bucket is not inlineable if it contains subbuckets or if it goes beyond
// our threshold for inline bucket size.
var size = pageHeaderSize
for _, inode := range n.inodes {
size += leafPageElementSize + uintptr(len(inode.key)) + uintptr(len(inode.value))
if inode.flags&bucketLeafFlag != 0 {
return false
} else if size > b.maxInlineBucketSize() {
return false
}
}
return true
}
// Returns the maximum total size of a bucket to make it a candidate for inlining.
func (b *Bucket) maxInlineBucketSize() uintptr {
return uintptr(b.tx.db.pageSize / 4)
}