golang-badger/

Badger — это реализация LSM tree на языке Go. Не будет преувеличением сказать, что это как RocksDB , только написанный с нуля на другом языке программирования. Библиотека основана на WiscKey paper [PDF] , обмазана кучей всевозможных тестов , неплохо показывает себя на бенчмарках , ну и в целом производит впечатление серьезного проекта. Мне захотелось познакомиться с библиотекой поближе. Поэтому я написал на ней простенькую key-value СУБД с REST-интерфейсом .

Вот что вышло в итоге:

package main

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:

$ curl -XPUT -d $’The answer is 43n’ localhost:8080/api/v1/answer -D —

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 42n’
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 42n’
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.

EnglishRussianUkrainian