Published at

Build một cái K-V Database đơn giản như thế nào? (P1 - Database file format)

Build một cái K-V Database đơn giản như thế nào? (P1 - Database file format)

Tìm hiểu cách Bolt DB tổ chức metadata, index và raw data trong file .db duy nhất thông qua hệ thống Page, Meta, Freelist, Branch/Leaf và cơ chế overflow.

Table of Contents

Ở phần này, ta chủ yếu quan tâm đến: cách mà metadata, index và raw data được lưu trữ trên disk.

Khi ta tạo một một instance Bolt mới thì nó sẽ tạo ra cho chúng ta một file instance_name.db . Đây là file sẽ chứa toàn bộ dữ liệu của DB, tức là nó chứa: metadata, index và dữ liệu thô. Khác với những DB khác, Bolt không chia ra thành nhiều file mà nó chỉ sử dụng 1 file duy nhất để quản lý instance, một file .db sẽ chứa toàn bộ các Page như hình minh hoạ ở dưới. Vì vậy Bolt cần có một cách tổ chức dữ liệu phù hợp và hiệu quả.

Database file structure with pages

Page

Gồm 2 thành phần cơ bản: header và body

Page structure showing header and body

Bolt sử dụng PAGE_SIZE của hệ thống để quyết định kích thước của 1 page, việc này nhằm tối ưu khả năng đọc ghi được cung cấp bởi file system. Phân tích header của một page, ta có các field như sau:

Page header structure with fields
FieldMô tả
idpage id
flagsĐịnh danh để phân biệt giữa các loại page
countGhi lại số lượng phần tử trong page
overflowKhi gặp dữ liệu lớn không thể vừa trong một page, nó sẽ tràn sang các page khác; overflow ghi lại tổng số lượng page tràn
ptrTrỏ tới địa chỉ bộ nhớ của dữ liệu page, field này chỉ tồn tại trong bộ nhớ

Xét từng field trong header, với flags, Bolt định nghĩa có 4 loại page trong hệ thống như sau:

const (
    branchPageFlag   = 0x01
    leafPageFlag     = 0x02
    metaPageFlag     = 0x04
    freelistPageFlag = 0x10
)

Ta sẽ xem xét qua từng loại page một

Meta Page

Meta page lưu trữ tất cả metadata của Bolt instance. Cho biết đây là file gì và cách diễn giải file này.

Cấu trúc body của một meta page được định nghĩa như sau:

Meta page structure
FieldMô tả
magicMột số ngẫu nhiên 32-bit có định được sử dụng để xác định rằng file này là file có số dữ liệu Bolt (khả năng một file khác có cùng dữ liệu ở đầu file aka magic value là cực kỳ thấp). Con số mà Bolt lựa chọn là 0xED0CDAED (3977042669).
versionCho biết phiên bản Bolt mà file này thuộc về, thuận tiện cho tương thích và di chuyển trong tương lai
page_sizePAGE_SIZE được để cập ở trên
flagsTrường dành riêng, không sử dụng
rootTất cả index và raw data trong instance Bolt được tổ chức thành cấu trúc cây; root là node gốc
freelistKhi Bolt xóa dữ liệu, có thể xuất hiện không gian thừa; thông tin về không gian này được ghi trong freelist để sử dụng sau
pgidId của page tiếp theo sẽ được cấp phát, có giá trị lớn hơn id của tất cả page đã được cấp phát
txidId của transaction tiếp theo sẽ được cấp phát. Transaction id tăng đơn điệu và có thể được hiểu là logical time của việc thực thi transaction
checksumĐược sử dụng để xác nhận tính toàn vẹn của dữ liệu meta page, đảm bảo rằng những gì được đọc là dữ liệu từ lần ghi cuối cùng

Bolt sử dụng magic, version và checksum để xác minh tính hợp lệ của file cơ sở dữ liệu

// validate checks the marker bytes and version of the meta page to ensure it matches this binary.
func (m *meta) validate() error {
    if m.magic != magic {
        return ErrInvalid
    } else if m.version != version {
        return ErrVersionMismatch
    } else if m.checksum != 0 && m.checksum != m.sum64() {
        return ErrChecksum
    }
    return nil
}

Freelist Page

Free list page dùng để quản lý tất cả các page trống do Bolt tạo ra. Được sắp xếp theo 1 list page id nhằm tối ưu khả năng cấp phát không gian cho dữ liệu mới. Ta sẽ tìm hiểu kĩ hơn ở phần Disk Space Manager.

Freelist page structure

Branch/Leaf Page

Bolt sử dung B+ tree để lưu trữ index và K-V data và coi các branch/leaf page là branch/leaf node luôn. Chúng có chung bố cục nhưng lại khác nhau về element header và data mà nó lưu trữ. Bố cục chung được mô tả như sau:

Branch and leaf page layout comparison

Sau Page Header là các Element Header, các element này lưu trữ thông tin về data như vị trí và kích thước. Ta có thể thấy, kích thước của các element header là không đổi, vì vậy chỉ cần sắp xếp theo thứ tự. Tuy nhiên với data thì lại khác, vì mỗi data lưu một dữ liệu khác nhau nên không thể có kích thước như nhau được. Việc tổ chức cố định 1 data block sẽ gây ra hiện tượng thiếu/thừa vùng nhớ, vì vậy Bolt sử dụng kĩ thuật Slotted Pages để sắp xếp các data block, dùng element header là chỉ dụng để định danh vị trí của data.

Ta sẽ xem xét chi tiết Branch Page:

  • Lưu trữ dữ liệu của branch node trong B+ tree. Mỗi branch node cần lưu trữ một số key để biểu thị giới hạn trên và dưới của các khoá có trong child node:
Branch page detailed structure
  • Mở rộng element header, ta có bố cục như sau:
Branch page element header structure
FieldMô tả
posVị trí của khóa aka offset
ksizeĐộ dài của key
pgidPage id nơi child node được đặt

Tiếp theo là Leaf Page:

  • Lưu trữ dữ liệu leaf node của B+ tree. Tức là dữ liệu key-value. Bố cục như sau:
Leaf page detailed structure
  • Ta thấy cũng tương tự như branch page, tuy nhiên ở phần data thì thay vì chỉ là value, lúc này ta có thêm key nữa. Lúc này header sẽ có chút thay đổi như sau:
Leaf page element header structure
FieldMô tả
flagsĐây là trường dành riêng, cũng như để căn chỉnh
posVị trí của khóa aka offset
ksizeĐộ dài của key
vsizeĐộ dài của value

Có một điểm nhận ra khác nhau giữa branch page element header và leaf page element header là: Branch page element header có pgid dùng để chỉ định đến page tiếp theo cần visit (hữu ích cho việc navigation). Còn Leaf page element header thì lại không có pgid vì nó đã là endpoint, không có child node để chỉ định đến. Tuy nhiên lại có flags để phân biệt page hiện tại có phải là sub-bucket hay không (cái này ta sẽ tìm hiểu ở phần Bucket).

Tràn dữ liệu (Data overflow)

Vấn đề: Nếu data cần lưu có kích thước to hơn cả một page size thì sao?

Bolt giải quyết vấn đề này bằng cách sử dụng overflow page. Khi cần lưu trữ dữ liệu vượt quá kích thước của một page, N page liên tiếp đáp ứng yêu cầu kích thước được yêu cầu, trong đó page thứ 2 đến thứ N là overflow của page thứ 1. Chúng chia sẻ page header với page thứ 1:

Data overflow across multiple pages

Những page liên tiếp này được Bolt xem như một page logic khổng lồ, tạo thuận lợi cho logic đọc-ghi thống nhất. Khi ghi overflow page, Bolt sẽ tính toán trước kích thước của toàn bộ page dựa trên page header và element header. Nếu kích thước của nó vượt quá một page, nhiều page liên tiếp được yêu cầu và số lượng overflow page được ghi lại (field này nằm ở page header). Khi đọc, nó cũng được coi như một page logic để đọc vào. Vì các page liên tiếp, dữ liệu liên quan có thể được đọc trực tiếp dựa trên số lượng overflow page được ghi trong page header. Tuy nhiên, một trường hợp đặc biệt cần xem xét cụ thể, đó là Freelist page siêu lớn:

Các phần tử trong freelist page đều là pgid, không có element header và không có thông tin offset dữ liệu. Kích thước của toàn bộ page logic chỉ có thể được xác định thông qua count trong page header. Tuy nhiên, có thể có trường hợp độ dài thực tế của freelist vượt quá giới hạn trên của count(uint16) là 65535. Bolt xử lý vấn đề này thông qua các giá trị countđặc biệt:

// If the page.count is at the max uint16 value (64k) then it's considered
// an overflow and the size of the freelist is stored as the first element.
idx, count := 0, int(p.count)
if count == 0xFFFF {
    idx = 1
    count = int(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[0])
}

Khi count có giá trị chính xác là giá trị giới hạn trên, được coi là 8 byte (uint64) sau page header ghi lại độ dài thực tế của freelist, lúc này page body sẽ bắt đầu sau count chứ không sau page header nữa:

Freelist count overflow handling

Kết

File format chỉ là nửa câu chuyện. Dữ liệu được tổ chức đẹp đẽ trên disk cũng vô nghĩa nếu việc đọc/ghi chậm như rùa bò. Vấn đề thực sự là: làm sao để bridge gap giữa slow disk và fast memory? Đây chính là challenge mà phần Storage and Cache Management tiếp theo sẽ giải quyết.