0
0

[bbolt]Golangでembedded key value storeを使う

Posted at

概要

bblotを使ってDisk based key/value storeを構築する

背景

  • サーバに積むメモリを最小限に抑えるため、インメモリではなくでDisk based key/value storeを利用したい
  • mmapを利用して自作しようと思ったが考慮することが多かったためあきらめた
  • embedded key/value storeといえば、RocksDBが思いつくが、Goでの組み込みが大変そうだった
  • 「golang embeded key value store」で検索すると、多くの人がbboltを使っていたので利用することに決定

ユースケース

  • ユーザー検索文字列をシノニム展開する
  • 以下のようなシノニム辞書をbboltに保存している
synonym.csv
スタバ,スターバックス
Starbucks,スターバックス
マクド,マクドナルド
マック,マクドナルド

例えば、ユーザー検索文字列が「スタバ」の場合、「スタバ、スターバックス」とシノニム展開する

コードサンプル

bboltへのデータ書き込みはデータベース作成時以外発生しないので、書き込みと読み込みのプロセスを分けている

書き込み

main.goを実行するとルートディレクトリにsynonym.dbファイルが作成される

ディレクトリ構成
$ tree

.
├── README.md
├── cmd
│   └── main.go
├── dict
│   └── synonym.csv
├── go.mod
├── go.sum
└── internal
    └── bboltRepository.go

4 directories, 6 files

main.go
package main

import (
	"fmt"
	repository "synonym-data-generator/internal"
)

func main() {

	filePath := "./dict/synonym.csv"
	bblot, err := repository.NewBboltRepositpry()
	if err != nil {
		fmt.Println(err)
		return
	}

	err = bblot.Write(filePath)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("create database done")

}

bboltRepository.go
package repository

import (
	"bufio"
	"go.etcd.io/bbolt"
	"os"
	"strings"
	"syscall"
	"time"
)

type BboltRepository struct {
	db         *bbolt.DB
	bucketName string
}

// NOTE: 所有者だけread write権限
const PERMISSION = 0600

func NewBboltRepositpry() (*BboltRepository, error) {
	// NOTE: 結局内部的にはmmapを使っている
	option := bbolt.Options{
		Timeout: 1 * time.Second,
		// NOTE: マップされたメモリの修正がプロセス固有であると設定
		MmapFlags: syscall.MAP_PRIVATE,
		// NOTE: mmpaの初期サイズ(bytes)
		// 100MBを設定
		InitialMmapSize: 100 * 1024 * 1024,
		// NOTE: mmapでメモリに展開した領域をロックする(ページアウトさせない)
		// ページフォルトが発生しないので高速化するが、ユースケースがわからない
		// そもそもメモリに乗り切らないデータを扱うためにmmapを利用すると思うが、なぜmlockするのか
		Mlock: false,
		// NOTE: mmapでの割り当てはページ単位であり、その割り当てでフリーになっているつまり、どのデータも割り当てられていない
		// ページをリストを管理する方法を設定
		// 二種類あり、arrayとhashMap
		//  hashMapの方が高速
		FreelistType: bbolt.FreelistMapType,
	}
	db, err := bbolt.Open("synonym.db", PERMISSION, &option)
	if err != nil {
		return nil, err
	}
	return &BboltRepository{
		db:         db,
		bucketName: "synonymBucket",
	}, err
}

func (r *BboltRepository) Write(filePath string) error {

	open, err := os.Open(filePath)
	defer open.Close()
	if err != nil {
		return err
	}

	err = r.db.Batch(func(tx *bbolt.Tx) error {
		bucket, err := tx.CreateBucket([]byte(r.bucketName))
		if err != nil {
			return err
		}
		scanner := bufio.NewScanner(open)
		for scanner.Scan() {
			raw := scanner.Text()
			raws := strings.Split(raw, ",")
			err = bucket.Put([]byte(raws[0]), []byte(raws[1]))
			if err != nil {
				return err
			}
			if err != nil {
				return err
			}
		}
		return nil
	})

	if err != nil {
		return err
	}

	return nil
}

読み込み

ディレクトリ構成
$ tree
.
├── Dockerfile
├── README.md
├── cmd
│   └── main.go
├── go.mod
├── go.sum
├── internal
│   ├── domain
│   │   └── SearchCondition.go
│   ├── presentation
│   │   ├── healthHandler.go
│   │   ├── model
│   │   │   ├── ErrorResponse.go
│   │   │   └── response.go
│   │   └── searchHandler.go
│   └── repository
│       └── bboltRepository.go
└── synonym.db

9 directories, 25 files

package repository

import (
	"fmt"
	"go.etcd.io/bbolt"
	"os"
	"syscall"
	"time"
)

type BboltRepository struct {
	db         *bbolt.DB
	bucketName string
}

// NOTE: 所有者だけread write権限
const PERMISSION = 0600
const DB_NAME = "synonym.db"
const BUCKET_NAME = "synonymBucket"

func NewBboltRepositpry() (*BboltRepository, error) {

	// まずDBの存在確認を行う
	_, err := os.Stat(DB_NAME)
	if err != nil {
		return nil, err
	}

	// NOTE: 結局内部的にはmmapを使っている
	option := bbolt.Options{
		Timeout: 1 * time.Second,
		// NOTE: マップされたメモリの修正がプロセス固有であると設定
		MmapFlags: syscall.MAP_PRIVATE,
		// NOTE: mmpaの初期サイズ(bytes)
		// 100MBを設定
		InitialMmapSize: 100 * 1024 * 1024,
		// NOTE: mmapでメモリに展開した領域をロックする(ページアウトさせない)
		// ページフォルトが発生しないので高速化するが、ユースケースがわからない
		// そもそもメモリに乗り切らないデータを扱うためにmmapを利用すると思うが、なぜmlockするのか
		Mlock: false,
		// NOTE: mmapでの割り当てはページ単位であり、その割り当てでフリーになっているつまり、どのデータも割り当てられていない
		// ページをリストを管理する方法を設定
		// 二種類あり、arrayとhashMap
		//  hashMapの方が高速
		FreelistType: bbolt.FreelistMapType,
	}
	db, err := bbolt.Open(DB_NAME, PERMISSION, &option)
	if err != nil {
		return nil, err
	}
	return &BboltRepository{
		db:         db,
		bucketName: BUCKET_NAME,
	}, err
}

func (r *BboltRepository) GetValue(key []byte) []byte {
	var value []byte
	r.db.View(func(tx *bbolt.Tx) error {
		bucket := tx.Bucket([]byte(r.bucketName))
		value = bucket.Get(key)
		return nil
	})

	fmt.Println(string(value))
	return value
}

その他機能

READMEでの開設が非常に充実しているのでそちらを参照
https://github.com/etcd-io/bbolt?tab=readme-ov-file#table-of-contents

まとめ

  • メモリを潤沢に積めるなら、アプリケーション起動時にhashMap構造でメモリに積んどいた方がパフォーマンスは良い
  • 内部的にはmmapを利用しておりページフォルトの場合に、Disk I/Oが発生するのでパフォーマンス測定は必要
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0