それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

Disk Manager の実装について

この記事は「けんつの1人 DBMS アドベントカレンダー Advent Calendar 2019 - Adventar」17 日目の記事です。

はじめに

どうも、最近そろそろ DBMS 作るのでなく MySQL とかの勉強をしたいなと思い始めたけんつです。
今日はディスク周りの話です。
といってもすることがあまりないのでめちゃくちゃ短くなりそうです。

データの永続化

ディレクトリ構成

.toybox
├── hello
│   └── page_0
├── sys
│   └── hello.json
└── world
    └── page_0

ディスク周りの永続化は↑のようなディレクトリ構成のもと実行される。
カタログと言われるテーブル情報を保持するデータは sys ディレクトリ以下にテーブルごとの json ファイルとしてまとめて保存されている。

テーブル毎の Page データは page_[pageid] といったファイルにバイナリを書き込んでいる。

何故こうなったか

Page

これはとくにない。テーブル毎にディレクトリを作って、PageId 0 から一意に id を割り振りたかった。
バイナリにシリアライズしたのは、今後いろんな対応をする上で楽になると思ったから。

System Page

Buffer Pool を作っていて、新しく Page を追加する際に PageId を割り振る必要があるのだがテーブル毎に
メモリ、ディスクの両方で(この場合は主にディスクを考慮している)ページをどれだけ保持しているかを確認したかったから。
そのためにディスクも含めて一番大きい pageId がいくつなのかを知る必要があった。

実装

今回は Direct I/O を使っていない
やっていることも簡単でファイルの RW だけ
一応 Page, SysPage 毎に分けているけどこれを書いていて、もう少し抽象化した方がいいなと思った

package storage

import (
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"strconv"
)

type DiskManager struct {
}

const (
	RootDir             = ".toybox"
	PagePrefix          = "page_"
	SystemPageDirectory = "sys"
	SystemPageFileExt   = ".json"
)

func NewDisk() *DiskManager {
	return &DiskManager{}
}

func (d *DiskManager) SavePage(tableName string, pageId uint64, page *Page) error {
	buf, err := SerializePage(page)
	if err != nil {
		return err
	}

	tablePath := filepath.Join(RootDir, tableName)
	_, err = os.Stat(tablePath)
	if os.IsNotExist(err) {
		err := os.MkdirAll(tablePath, 0777)
		if err != nil {
			panic(err)
		}
	}

	pid := strconv.FormatUint(pageId, 10)
	pagePath := path.Join(RootDir, tableName, PagePrefix+pid)
	return ioutil.WriteFile(pagePath, buf[:], 0777)
}

func (d *DiskManager) SaveSystemPage(tableName string, sysPage *SystemPage) error {
	json := SerializeSystemPage(sysPage)

	tablePath := filepath.Join(RootDir, SystemPageDirectory)
	_, err := os.Stat(tablePath)
	if os.IsNotExist(err) {
		err := os.MkdirAll(tablePath, 0777)
		if err != nil {
			panic(err)
		}
	}

	sysPagePath := path.Join(RootDir, SystemPageDirectory, tableName+SystemPageFileExt)
	return ioutil.WriteFile(sysPagePath, json, 0777)
}

func (d *DiskManager) LoadPage(tableName string, pageId uint64) (*Page, error) {

	pid := strconv.FormatUint(pageId, 10)
	pagePath := path.Join(RootDir, tableName, PagePrefix+pid)

	_, err := os.Stat(pagePath)
	if os.IsNotExist(err) {
		return nil, err
	}

	bytes, err := ioutil.ReadFile(pagePath)
	if err != nil {
		return nil, err
	}

	var buf [PageByteSize]byte
	copy(buf[:], bytes)
	return DeserializePage(buf)
}

func (d *DiskManager) LoadSystemPage(tableName string) (*SystemPage, error) {

	pagePath := path.Join(RootDir, SystemPageDirectory, tableName+SystemPageFileExt)
	_, err := os.Stat(pagePath)
	if err != nil {
		return nil, err
	}

	bytes, err := ioutil.ReadFile(pagePath)
	if err != nil {
		return nil, err
	}
	return DeserializeSystemPage(bytes)
}

func (d *DiskManager) LoadAllSystemPage() (map[string]*SystemPage, error) {

	path := path.Join(RootDir, SystemPageDirectory)
	files, err := ioutil.ReadDir(path)

	if err != nil {
		return nil, err
	}

	systems := make(map[string]*SystemPage)
	for _, file := range files {
		p := filepath.Join(path, file.Name())
		bytes, err := ioutil.ReadFile(p)
		if err != nil {
			return nil, err
		}

		sysPage, err := DeserializeSystemPage(bytes)
		if err != nil {
			return nil, err
		}
		systems[file.Name()[:len(file.Name())-5]] = sysPage
	}

	return systems, nil
}

おわりに

次は、SystemPage (Catalog) の話なのでまた短い。