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

## 概要

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

https://github.com/etcd-io/bbolt


## 背景
- サーバに積むメモリを最小限に抑えるため、インメモリではなくでDisk based key/value storeを利用したい
- mmapを利用して自作しようと思ったが考慮することが多かったためあきらめた
- embedded key/value storeといえば、[RocksDB](https://rocksdb.org/)が思いつくが、Goでの組み込みが大変そうだった
- 「golang embeded key value store」で検索すると、多くの人がbboltを使っていたので利用することに決定
  - この辺とかhttps://www.reddit.com/r/golang/comments/rrccjq/recommendation_for_keyvalue_storage/?rdt=58913


## ユースケース
- ユーザー検索文字列をシノニム展開する
- 以下のようなシノニム辞書をbboltに保存している

```csv:synonym.csv
スタバ,スターバックス
Starbucks,スターバックス
マクド,マクドナルド
マック,マクドナルド
```

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


## コードサンプル


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

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

```shell:ディレクトリ構成
$ tree

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

4 directories, 6 files

```


```go: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")

}

```

```go: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
}

```

### 読み込み

```shell: ディレクトリ構成
$ 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

```

```go
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が発生するのでパフォーマンス測定は必要