- PVSM.RU - https://www.pvsm.ru -
Небольшая заметка о встраиваемой key-value БД под названием Coffer
, написанной на Golang. Если совсем коротко: в остановленном состоянии БД данные лежат на диске, при запуске данные копируются в память. Чтение происходит из памяти. При записи изменяются данные памяти, а изменения записываются в журнал на диск. Максимальный размер хранимых данных ограничен размером оперативной памяти. API позволяет создавать хидеры для записей БД и применять их в транзакциях, сохраняя при этом консистентность данных.
Но сначала небольшое лирическое вступление. Давным давно, когда трава была зеленее, потребовалась мне встраивая key-value БД для go-приложения. Посмотрев по сторонам и потыкавшись в разные пакеты, я как-то не нашёл того, что мне бы понравилось (субъективно), и просто применил решение с внешней реляционной БД. Отличное рабочее решение. Но как говорится, ложечка-то нашлась, а вот осадок остался. Прежде всего хотелось именно нативную, на Go написанную БД, прямо родную-родную. И такие есть, достаточно поглядеть awesome-go. Однако их там не миллион. Это даже удивительно, если учесть, что редок на свете программист, который не писал в своей жизни БД, фреймворк или казуальную игру.
Ну что-же, можно попробовать, и на коленке сваять свой велосипед, с блэкджеком и прочими плюшками. При этом все знают, или по крайней мере догадываются, что написание даже простой key-value БД кажется простым только на первый взгляд. А на самом деле, всё гораздо веселее (и так и получилось). И ещё меня одолевало любопытство насчёт ACID и волновали транзакции. Правда транзакции скорее в финансовом понимании, т.к. я тогда был занят в финтехе.
Рассмотрим случай, когда во время работы приложения с активной записью накрылся медным тазом блок питания в компьютере и при этом диск не сломался. Если в этот момент приложение от БД получило ok
, значит данные этой операции не будут потеряны. Если приложение получило отрицательный ответ, то понятное дело, операция не выполнена. Ну и случай, когда приложение отправило запрос, но не получило ответ: эта операция скорей всего не выполнена, но есть маленький шанс, что операция попала в журнал, но ровно в момент отправки ответа произошло отключение энергии.
Как при последнем кейсе узнать, что там было с последними операциями? Это интересный вопрос. Косвенно вы можете об этом догадаться (сделать выводы), посмотрев значение интересующей записи после нового запуска приложения с БД. Однако, если операции достаточно часты, боюсь, это не поможет. Можно посмотреть файл последнего лога (он будет с самым большим номером), но вручную это неудобно. Думаю, в перспективе можно в API добавить возможность просматривать логи (естественно, логи в этом случае не должны удаляться).
Признаюсь честно, сам я шнур из розетки не выдёргивал, т.к. не хочется рисковать железом ради проверки БД. В тестах я просто порчу нормальные файлы логов, и в этом случае, всё происходит так, как я и предполагал. Однако опыта практического использования БД нет, на проде она не работала, и риски есть. Впрочем, для пет-проектов думаю, БД можно юзать достаточно безбоязненно. В общем, обычный disclaimer, гарантий нет.
БД на настоящий момент никак не защищается от использования в двух разных приложениях (или одинаковых, тут это не важно), сконфигурированных работать с одной и той же директорией. Прошу этот момент учитывать! И ещё, поскольку БД встраиваемая, то передавая её в аргументах какой-нибудь ссылочный тип, точно не стоит его менять где-то в параллельной горутине.
У базы довольно много параметров, которые можно сконфигурировать, однако практически все они имеют дефолтные значения, поэтому всё можно уместить в одну короткую строку cof, err, wrn := Db(dirPath).Create()
Возвращается ошибка (при ошибке дальнейшая работа с БД запрещена) и варнинг, о котором можно знать, но работе БД это не мешает.
Не буду загромождать текст громоздкими описаниями, при необходимости прошу смотреть их в ридми репозитория — github.com/claygod/coffer/blob/master/README_RU.md#config [1] Обратите внимание на метод Handler, подключающий обработчик для транзакции, о нём я черкну пару строк пониже, здесь же я просто их перечислю:
Насколько возможно, API я сделал простым, да и для key-value базы не стоит слишком мудрить:
Небольшие пояснения к API:
Простой пример использования:
package main
import (
"fmt"
"github.com/claygod/coffer"
)
const curDir = "./"
func main() {
// STEP init
db, err, wrn := coffer.Db(curDir).Create()
switch {
case err != nil:
fmt.Println("Error:", err)
return
case wrn != nil:
fmt.Println("Warning:", err)
return
}
if !db.Start() {
fmt.Println("Error: not start")
return
}
defer db.Stop()
// STEP write
if rep := db.Write("foo", []byte("bar")); rep.IsCodeError() {
fmt.Sprintf("Write error: code `%d` msg `%s`", rep.Code, rep.Error)
return
}
// STEP read
rep := db.Read("foo")
rep.IsCodeError()
if rep.IsCodeError() {
fmt.Sprintf("Read error: code `%v` msg `%v`", rep.Code, rep.Error)
return
}
fmt.Println(string(rep.Data))
}
Как выше уже сказано, моё определение транзакций может не совпадать с общепринятым в БД-строительстве, возможно, их объединяет только идея. В конкретной имплементации транзакция, это некий хидер, заданный на этапе конфигурирования БД (метод Handler
). Когда мы вызываем транзакцию с этим хидером, БД блокирует записи, с которыми будет работать хидер и передаёт их текущие значения на вход хидеру. Хидер манипулирует этими данными так, как ему надо, и возвращает новые значения БД, а та сохраняет их в сторадже. После этого записи разблокируются и становятся доступны для других операций.
В репо есть примеры, которые очень неплохо раскрывают суть использования транзакций. Из любопытства я сделал небольшой финансовый пример, в котором есть дебитная и кредитная операции, трансфер, купля-продажа. Написать этот пример было очень легко, и при этом эта наколеночная реализация вполне консистентна и годится для использования в разных финансовых решениях, или к примеру в логистике.
Важный момент: код хэндлеров не хранится в БД. У меня была идея хранить его в журнале, но это мне показалось слишком расточительным, поэтому я не стал усложнять, и соответственно ответственность за консистентность хэндлеров между разными запусками БД лежит на разработчике кода, использующего БД. Хэндлеры точно нельзя менять, если остановка приложения и БД была сопряжена с крашем. В этом случае надо сначала запустить БД и после этого штатно её остановить — будет создан новый снимок данных. Чтобы не запутаться советую в названии хэндлеров использовать номер версии.
БД возвращает репорты с указанием статуса ответа и с данными. Поскольку кодов много, и писать switch с обработкой каждой из них хлопотно, может возникнуть желание проверять на ок. Так делать не следует. Дело в том, что код может иметь статус Ok, Error, Panic. С Ок всё понятно, а что с остальными двумя? Если статус Error, конкретная операция выполнена, или выполнена не полностью. Эту ошибку нужно соответствующим образом обработать в приложении. Однако работать с БД дальше можно (и нужно). Другое дело Panic — работу с БД следует прекратить.
Проверка IsCodeError
упрощает работу со всеми ошибками, поэтому если вас не интересуют детали, работайте дальше.
Проверка IsCodePanic
охватывает все кейсы, при которых работу с БД необходимо прекратить.
В простом случае для обработки ответа достаточно тройного switch:
IsCodeOk
— продолжаем работу в штатном режимеIsCodeError
— логируем ошибку из репорта и работаем дальшеIsCodePanic
— логируем ошибку из репорта и прекращаем работу с БДДля названия выбран один из вариантов перевода слова ящик
на английский язык, предпочёл бы конечно box
, но это слишком популярное слово, надеюсь, coffer
тоже сойдёт.
Тема с ACID мне кажется достаточно холиварная, поэтому я бы сказал, что Coffer стремится к этому, но не факт, и я не утверждаю, что у него это получилось.
Я сразу писал БД с учётом параллелизма и конкуренции. Именно в таком режиме она проявляет свою эффективность (хотя это наверно слишком громко сказано). В лежащих ниже результатах бенчмарк демонстрирует пропускную способность в 200к rps. Это конечно искусственный бенч, и реальность будет совсем иной, т.к. многое зависит от размера записываемых данных, количества уже записанных данных, производительности железа и фазы луны. Но тенденция по крайней мере понятна. Если же БД использовать однопоточно, каждый запрос выполнять только после получения ответа на предыдущий, скорость будет медленной, и я бы посоветовал глядеть другие БД, но не Coffer.
Кстати, если кто-то потратит время и склонирует себе репозиторий с Coffer, по возмодности, запустите лежащий в нём бенч. Мне очень интересно, на каких машинах какую производительность покажет БД. Прежде всего, конечно всё зависит от диска. Это мне особенно стало понятно, после того как я не так давно купил себе новый Samsung EVO. Но не беспокойтесь, это не на замену убитому диску. Старичок Toshiba продолжает исправно служить и хранит сейчас в себе мой видеоархив.
Встроенный инмемори стораж пока представлянт собой простой мэп, даже не поделенный на секции. Его конечно можно здорово усовершенствовать, например, чтобы сделать быстрыми выборки ключей по префиксам и суффиксам. Пока я этим не занимался, т.к. основной функционал, так сказать фишку БД я вижу в транзакциях, и узким местом в производительности для транзакций будет работа с диском, и уже потом, работа с памятью.
Сейчас лицензия позволяет хранить в базе до десяти миллионов записей, мне показалось, что это достаточная цифра. Дальнейшие планы по развитию БД в стадии формирования.
В общем случаем мне интересно, чтобы БД использовали как пакет, и ориентировались прежде всего на его API.
В последнее время часто встречаюсь с задачей написания сервисов с характеристикой высокой доступности. К сожалению, из-за того, что это почти всегда подразумевает наличие нескольких инстансов, использовать при таком кейсе встраиваемую БД не стоит. Остаётся вариант обычного приложения или сервиса, существующего в одном экземпляре. Это мне кажется более редким кейсом, но тем не менее он есть, и на такой случай неплохо иметь БД, старающуюся по возможности, сберечь хранящиеся в неё данные. Созданный мной Coffer пытается решить такую задачу. Посмотрим, насколько у него это получается.
Репозиторий БД: github.com/claygod/coffer [2]
Описание на русском языке: github.com/claygod/coffer/blob/master/README_RU.md [3]
Автор: Эдуард
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/bazy-danny-h/337665
Ссылки в тексте:
[1] github.com/claygod/coffer/blob/master/README_RU.md#config: https://github.com/claygod/coffer/blob/master/README_RU.md#config
[2] github.com/claygod/coffer: https://github.com/claygod/coffer
[3] github.com/claygod/coffer/blob/master/README_RU.md: https://github.com/claygod/coffer/blob/master/README_RU.md
[4] Источник: https://habr.com/ru/post/477154/?utm_source=habrahabr&utm_medium=rss&utm_campaign=477154
Нажмите здесь для печати.