Categories: Go

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.

admin

Share
Published by
admin
Tags: Go

Recent Posts

Консоль удаленного рабочего стола(rdp console)

Клиент удаленного рабочего стола (rdp) предоставляет нам возможность войти на сервер терминалов через консоль. Что…

1 месяц ago

Настройка сети в VMware Workstation

В VMware Workstation есть несколько способов настройки сети гостевой машины: 1) Bridged networking 2) Network…

1 месяц ago

Логи брандмауэра Windows

Встроенный брандмауэр Windows может не только остановить нежелательный трафик на вашем пороге, но и может…

1 месяц ago

Правильный способ отключения IPv6

Вопреки распространенному мнению, отключить IPv6 в Windows Vista и Server 2008 это не просто снять…

1 месяц ago

Ключи реестра Windows, отвечающие за параметры экранной заставки

Параметры экранной заставки для текущего пользователя можно править из системного реестра, для чего: Запустите редактор…

1 месяц ago

Как управлять журналами событий из командной строки

В этой статье расскажу про возможность просмотра журналов событий из командной строки. Эти возможности можно…

1 месяц ago