Пример простейшей key-value СУБД на основе Badger
4 февраля 2019
Badger — это реализация LSM tree на языке Go. Не будет преувеличением сказать, что это как RocksDB, только написанный с нуля на другом языке программирования. Библиотека основана на WiscKey paper [PDF], обмазана кучей всевозможных тестов, неплохо показывает себя на бенчмарках, ну и в целом производит впечатление серьезного проекта. Мне захотелось познакомиться с библиотекой поближе. Поэтому я написал на ней простенькую key-value СУБД с REST-интерфейсом.
Вот что вышло в итоге:
import (
"encoding/json"
"flag"
"time"
"fmt"
"io/ioutil"
"net/http"
"runtime"
"strconv"
_ "net/http/pprof"
"github.com/dgraph-io/badger"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
var db *badger.DB
const InvalidVersion uint64 = 0
func openDatabase(path string) error {
opts := badger.DefaultOptions
opts.Dir = path
opts.ValueDir = path
var err error
db, err = badger.Open(opts)
return err
}
func badgerCleanupProc() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
again:
logrus.Infof("calling db.RunValueLogGC...")
err := db.RunValueLogGC(0.7)
if err == nil {
goto again
}
}
}
func reportError(w http.ResponseWriter, err error) {
type JsonError struct {
Error string `json:"error"`
}
jsonError := &JsonError{ Error: err.Error() }
w.WriteHeader(400)
encErr := json.NewEncoder(w).Encode(jsonError)
if encErr != nil {
logrus.Errorf("json.Encode failed: %v\n", encErr)
}
}
func keyAndVersion(r *http.Request) ([]byte, uint64, error) {
vars := mux.Vars(r)
key := []byte(vars["key"])
var err error
ver := InvalidVersion
query := r.URL.Query()
verStr := query.Get("ver")
if verStr != "" {
ver, err = strconv.ParseUint(verStr, 16, 64)
if err != nil {
return nil, InvalidVersion, err
}
}
return key, ver, nil
}
func GetHandler(w http.ResponseWriter, r *http.Request) {
key, ver, err := keyAndVersion(r)
if err != nil {
reportError(w, err)
return
}
logrus.Debugf("GetHandler: key = %s, ver = %x\n", key, ver)
var valCopy []byte
var verCopy uint64
err = db.View(func(txn *badger.Txn) error {
item, err := txn.Get(key)
if err != nil {
return err
}
verCopy = item.Version()
if ver != InvalidVersion && ver != verCopy {
return fmt.Errorf("version mismatch")
}
valCopy, err = item.ValueCopy(nil)
return err
})
if err != nil {
reportError(w, err)
return
}
h := w.Header()
h.Set("Content-Type", "application/octet-stream")
h.Set("X-Version", strconv.FormatUint(verCopy, 16))
w.WriteHeader(200)
_, err = w.Write(valCopy)
if err != nil {
logrus.Errorf("GetHandler, w.Write: %v\n", err)
}
}
func PutHandler(w http.ResponseWriter, r *http.Request) {
key, ver, err := keyAndVersion(r)
if err != nil {
reportError(w, err)
return
}
logrus.Debugf("PutHandler: key = %s, ver = %x\n", key, ver)
val, err := ioutil.ReadAll(r.Body)
if err != nil {
reportError(w, err)
return
}
err = db.Update(func(txn *badger.Txn) error {
if ver != InvalidVersion {
item, err := txn.Get(key)
if err != nil {
return err
}
if item.Version() != ver {
err = fmt.Errorf("version mismatch")
return err
}
}
return txn.Set(key, val)
})
if err != nil {
reportError(w, err)
return
}
h := w.Header()
h.Set("Content-Type", "application/octet-stream")
w.WriteHeader(200)
}
func DelHandler(w http.ResponseWriter, r *http.Request) {
key, ver, err := keyAndVersion(r)
if err != nil {
reportError(w, err)
return
}
logrus.Debugf("DelHandler: key = %s, ver = %x\n", key, ver)
err = db.Update(func(txn *badger.Txn) error {
if ver != InvalidVersion {
item, err := txn.Get(key)
if err != nil {
return err
}
if item.Version() != ver {
err = fmt.Errorf("version mismatch")
return err
}
}
return txn.Delete(key)
})
if err != nil {
reportError(w, err)
return
}
h := w.Header()
h.Set("Content-Type", "application/octet-stream")
w.WriteHeader(200)
}
var databasePath string
var listenAddr string
var verboseLogging bool
var profile bool
func main() {
flag.StringVar(&databasePath, "database", "./database",
"Path to the database")
flag.StringVar(&listenAddr, "listen", ":8080",
"Listen address")
flag.BoolVar(&verboseLogging, "verbose", true,
"Enable verbose logging")
flag.BoolVar(&profile, "profile", false,
"Enable blocks and mutexes profiling")
flag.Parse()
if profile {
runtime.SetBlockProfileRate(20)
runtime.SetMutexProfileFraction(20)
}
if verboseLogging {
logrus.SetLevel(logrus.DebugLevel)
}
err := openDatabase(databasePath)
if err != nil {
logrus.Fatalf("openDatabase: %v\n", err)
}
defer func() {
err := db.Close()
if err != nil {
logrus.Fatalf("db.Close(): %v\n", err)
}
}()
logrus.Infof("Starting badgerCleanupProc...")
go badgerCleanupProc()
logrus.Infof("Database ready! Starting HTTP server at %s...",
listenAddr)
r := mux.NewRouter().
PathPrefix("/api/v1").
Path("/{key}").
Subrouter()
r.Methods("GET").
HandlerFunc(GetHandler)
r.Methods("PUT").
HandlerFunc(PutHandler)
r.Methods("DELETE").
HandlerFunc(DelHandler)
http.Handle("/", r)
err = http.ListenAndServe(listenAddr, nil)
if err != nil {
logrus.Fatalf("http.ListenAndServe: %v\n", err)
}
logrus.Info("HTTP server terminated\n")
}
Подробно описывать словами код мне что-то не хочется. Во-первых, для меня он выглядит довольно понятным. Во-вторых, Badger имеет вполне адекватную документацию, и мое объяснение свелось бы к ее пересказу. Из возможных граблей стоит отметить разве что тот факт, что сборка мусора в Badger происходит явно, см функцию badgerCleanupProc
. Сделано так для того, чтобы GC можно было запускать по расписанию, например, во время минимума активности пользователей. Если забыть про сборку мусора, это приведет к бесконечному росту базы данных.
Демонстрация работы основных методов, а также compare and swap:
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Date: Sat, 12 Jan 2019 09:09:50 GMT
Content-Length: 0
$ curl -XGET localhost:8080/api/v1/answer -D -
HTTP/1.1 200 OK
Content-Type: application/octet-stream
X-Version: 12ba4eb
Date: Sat, 12 Jan 2019 09:09:58 GMT
Content-Length: 17
The answer is 43
$ curl -XPUT -d $'The answer is 42\n' \
localhost:8080/api/v1/answer?ver=123456 -D -
HTTP/1.1 400 Bad Request
Date: Sat, 12 Jan 2019 09:10:16 GMT
Content-Length: 29
Content-Type: text/plain; charset=utf-8
{"error":"version mismatch"}
$ curl -XPUT -d $'The answer is 42\n' \
localhost:8080/api/v1/answer?ver=12ba4eb -D -
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Date: Sat, 12 Jan 2019 09:10:24 GMT
Content-Length: 0
$ curl -XGET localhost:8080/api/v1/answer -D -
HTTP/1.1 200 OK
Content-Type: application/octet-stream
X-Version: 12ba4ec
Date: Sat, 12 Jan 2019 09:10:33 GMT
Content-Length: 17
The answer is 42
$ curl -XDELETE localhost:8080/api/v1/answer?ver=12ba4ec -D -
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Date: Sat, 12 Jan 2019 09:10:49 GMT
Content-Length: 0
$ curl -XGET localhost:8080/api/v1/answer -D -
HTTP/1.1 400 Bad Request
Date: Sat, 12 Jan 2019 09:10:53 GMT
Content-Length: 26
Content-Type: text/plain; charset=utf-8
{"error":"Key not found"}
Как видите, получилась хоть и простенькая, но вполне полноценная key-valye СУБД. Которая нормально выдержит несколько тысяч одновременных соединений, прошу заметить. Разработка в неспешном ритме заняла один вечер. От меня потребовалось написать 200 строк кода, и все просто заработало.
Это сильно отличается от моего опыта написания аналогичной СУБД на RocksDB и C++. Там мне пришлось с нуля написать простенький HTTP-сервер, потому что нормального не нашлось, прямо сильно думать о перемещении объектов и умных указателях, а прикрутить libevent так, чтобы код напоминал Futures в Scala, а не мешанину из колбеков, я и вовсе не осилил. В такие моменты лучше понимаешь, за что люди недолюбливают С++ и за что любят Go.
Само собой разумеется, данная заметка не претендует на то, чтобы быть полным описанием Badger. Больше информации вы найдете в документации библиотеки. Помимо прочего, из нее вы узнаете, что Badger также позволяет указывать время жизни значений (TTL), поддерживает range scans, в том числе по префиксам ключей, что обеспечиваемый им уровень изоляции является serializable snapshot isolation, и так далее. Кстати, многие альтернативные реализации LSM tree не могут объяснить, какой уровень изоляции они дают. Это является еще одной причиной, почему меня заинтересовал именно Badger.
Метки: Go.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.