boltdb 数据布局

194 阅读5分钟

page

type pgid uint64

type page struct {
	id       pgid
	flags    uint16
	count    uint16
	overflow uint32
}

page定义是一个或者连续多个page size数据页的头部数据\color{red}{头部数据}(在下面图均用page header代替),每个数据页分为page headerpage data两部分。下面两个图分别展示了一个数据页和多个数据页存储一个节点数据的场景,其中示例的数据页大小为4k,实际大小由用户自定义配置,或者操作系统决定。

截屏2023-01-03 下午12.16.06.png

截屏2023-01-03 下午12.17.02.png page中四个属性含义分别如下:

  • id为当前数据页的pgidboltdb把数据按照操作系统的page size大小分配,这样的话在读写磁盘的时候可以有较好的性能

    截屏2023-01-03 下午12.49.24.png

    在根据pgid读写数据的时候,可以使用pgid以及page size迅速找到对应数据的位置:

    1. 此操作在func( tx *Tx) write() error中,其中找到pageoffset的操作为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)
        }
      }
      
    2. 根据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代表元素数量,在如下三种类型的时候有数量的含义:

    1. branch类型的时候,表示非叶子节点元素数量。boltdb采用B+树存储数据\color{red}{boltdb 采用 B+ 树存储数据},非叶子节点只存储索引。
    2. leaf类型的时候,表示叶子节点中元素数量。
    3. 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三个属性含义分别如下:

  • poskey相对branchPageElement的偏移量
  • ksize对应的key的长度
  • pgid子节点对应的pgid

下图中展示了branch类型的数据在磁盘上的布局:

截屏2023-01-03 下午12.22.25.png

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
  • poskeyvalue的相对leafPageElement的偏移量。
  • ksize表示当前leafPageElementkey的大小。
  • vsize表示当前leafPageElementvalue的大小。

下图展示了leaf类型的在磁盘上的布局:

截屏2023-01-03 下午12.21.00.png

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中的各元素属性如下:

  1. magicversionchecksum

    magicversion都是代码中写死的数字。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
    }
    
  2. pageSize是创建db的时候,配置的pageSize,或者操作系统的pageSize

  3. flags暂未使用

  4. root表示boltdb的根节点

  5. freelist表示对应数据页的pgid

  6. pgid表示整个boltdb中已用pgid的水位线,所有数据都不应该保存在大于等于pgid对应的数据页。

  7. txid表示meta对应写事务的事务id

下图展示了meta类型数据在磁盘上的布局:

截屏2023-01-03 下午1.06.21.png

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布局如下:

    截屏2023-01-03 下午3.57.42.png

  • pgid数量大于等于64k的时候,由于超过了pagecount的最大值64k-1,所以pageHeader中的count是不够的。可以使用data部分第一个uint64来存储count。通过让page.count=64k-1,这样的话在读取的时候知道data部分第一个uint64存储的是pgid数组大小。这个时候freelist布局如下:

    截屏2023-01-03 下午3.57.15.png

bucket

bucketboltdb中组织数据的基本单位。meta中保存了boltdb根节点bucket,所有数据查找从此节点开始。

  1. 每个bucket是一个B+树
  2. 根节点是一个bucket
  3. bucket中可以包含bucket

下图中展示bucket在实际存储的时候一个布局。由于bucket中才可以存储数据,所以root bucket这个B+树中叶子节点的均为bucket类型。图中只选择展示了key=d这个bucket数据分布,其他bucket和这个类似。

截屏2023-01-05 下午2.37.51.png 上图中的bucket是一个单独的数据页保存的,实际上还有内联的场景,即bucket的数据和其他key以及value保存在一个叶子节点上。可以内联的bucket需要满足如下三个条件:

  1. bucket只包括一个叶子节点
  2. bucket中不包含其他bucket
  3. 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)
}