Хабрахабр

Ящик для хранения данных в go-приложениях

image

Если совсем коротко: в остановленном состоянии БД данные лежат на диске, при запуске данные копируются в память. Небольшая заметка о встраиваемой key-value БД под названием Coffer, написанной на Golang. При записи изменяются данные памяти, а изменения записываются в журнал на диск. Чтение происходит из памяти. API позволяет создавать хидеры для записей БД и применять их в транзакциях, сохраняя при этом консистентность данных.
Но сначала небольшое лирическое вступление. Максимальный размер хранимых данных ограничен размером оперативной памяти. Посмотрев по сторонам и потыкавшись в разные пакеты, я как-то не нашёл того, что мне бы понравилось (субъективно), и просто применил решение с внешней реляционной БД. Давным давно, когда трава была зеленее, потребовалась мне встраивая key-value БД для go-приложения. Но как говорится, ложечка-то нашлась, а вот осадок остался. Отличное рабочее решение. И такие есть, достаточно поглядеть awesome-go. Прежде всего хотелось именно нативную, на Go написанную БД, прямо родную-родную. Это даже удивительно, если учесть, что редок на свете программист, который не писал в своей жизни БД, фреймворк или казуальную игру. Однако их там не миллион.

При этом все знают, или по крайней мере догадываются, что написание даже простой key-value БД кажется простым только на первый взгляд. Ну что-же, можно попробовать, и на коленке сваять свой велосипед, с блэкджеком и прочими плюшками. И ещё меня одолевало любопытство насчёт ACID и волновали транзакции. А на самом деле, всё гораздо веселее (и так и получилось). я тогда был занят в финтехе. Правда транзакции скорее в финансовом понимании, т.к.

Безопасность данных

Если в этот момент приложение от БД получило ok, значит данные этой операции не будут потеряны. Рассмотрим случай, когда во время работы приложения с активной записью накрылся медным тазом блок питания в компьютере и при этом диск не сломался. Ну и случай, когда приложение отправило запрос, но не получило ответ: эта операция скорей всего не выполнена, но есть маленький шанс, что операция попала в журнал, но ровно в момент отправки ответа произошло отключение энергии. Если приложение получило отрицательный ответ, то понятное дело, операция не выполнена.

Это интересный вопрос. Как при последнем кейсе узнать, что там было с последними операциями? Однако, если операции достаточно часты, боюсь, это не поможет. Косвенно вы можете об этом догадаться (сделать выводы), посмотрев значение интересующей записи после нового запуска приложения с БД. Думаю, в перспективе можно в API добавить возможность просматривать логи (естественно, логи в этом случае не должны удаляться). Можно посмотреть файл последнего лога (он будет с самым большим номером), но вручную это неудобно.

не хочется рисковать железом ради проверки БД. Признаюсь честно, сам я шнур из розетки не выдёргивал, т.к. Однако опыта практического использования БД нет, на проде она не работала, и риски есть. В тестах я просто порчу нормальные файлы логов, и в этом случае, всё происходит так, как я и предполагал. В общем, обычный disclaimer, гарантий нет. Впрочем, для пет-проектов думаю, БД можно юзать достаточно безбоязненно.

Прошу этот момент учитывать! БД на настоящий момент никак не защищается от использования в двух разных приложениях (или одинаковых, тут это не важно), сконфигурированных работать с одной и той же директорией. И ещё, поскольку БД встраиваемая, то передавая её в аргументах какой-нибудь ссылочный тип, точно не стоит его менять где-то в параллельной горутине.

Конфигурирование

Create() Возвращается ошибка (при ошибке дальнейшая работа с БД запрещена) и варнинг, о котором можно знать, но работе БД это не мешает. У базы довольно много параметров, которые можно сконфигурировать, однако практически все они имеют дефолтные значения, поэтому всё можно уместить в одну короткую строку cof, err, wrn := Db(dirPath).

Не буду загромождать текст громоздкими описаниями, при необходимости прошу смотреть их в ридми репозитория — github.com/claygod/coffer/blob/master/README_RU.md#config Обратите внимание на метод Handler, подключающий обработчик для транзакции, о нём я черкну пару строк пониже, здесь же я просто их перечислю:

  • Db(dirPath)
  • BatchSize(batchSize)
  • LimitRecordsPerLogfile(limitRecordsPerLogfile)
  • FollowPause(100*time.Second)
  • LogsByCheckpoint(1000)
  • AllowStartupErrLoadLogs(true)
  • MaxKeyLength(maxKeyLength)
  • MaxValueLength(maxValueLength)
  • MaxRecsPerOperation(1000000)
  • RemoveUnlessLogs(true)
  • LimitMemory(100 * 1000000)
  • LimitDisk(1000 * 1000000)
  • Handler(«handler1», &handler1)
  • Handler(«handler2», &handler2)
  • Handlers(map[string]*handler)
  • Create()

API

Насколько возможно, API я сделал простым, да и для key-value базы не стоит слишком мудрить:

  • Start — запуск БД
  • Stop — остановка БД
  • StopHard — остановка невзирая на прямо сейчас исполняемые операции (возможно уберу)
  • Save — сохранить снимок текущего состояния БД
  • Write — добавить одну запись в БД
  • WriteList — добавить несколько записей в БД (режимы strict и optional)
  • WriteListUnsafe — добавить несколько записей в БД без оглядки на безопасность данных
  • Read — получить одну запись по ключу
  • ReadList — получить список записей
  • ReadListUnsafe — получить список записей без оглядки на безопасность данных
  • Delete — удалить одну запись
  • DeleteList — удалить несколько записей в strict/optional режиме
  • Transaction — выполнить транзакцию
  • Count — сколько записей в БД
  • CountUnsafe — сколько записей в БД (чуть быстрей, но unsafe)
  • RecordsList — список всех ключей БД
  • RecordsListUnsafe — список всех ключей БД (чуть быстрей, но unsafe)
  • RecordsListWithPrefix — список ключей с указанным префиксом
  • RecordsListWithSuffix — список ключей с указанным окончанием

Небольшие пояснения к API:

  • Strict режим — сделай всё или ничего.
  • Optional режим — сделай всё, что получится.
  • StopHard — возможно, это метод стоит убрать из API, пока не определился.
  • Все RecordsList методы не быстрые, т.к. индексов в сторадже сейчас нет, пока это фуллскан.
  • Все Unsafe методы более быстрые, но при их использовании консистентность не подразумевается. Их логично использовать на остановленной БД для быстрого её наполнения или ещё чего-то в таком же духе.
  • За регулярным обновлением снимка БД следит фолловер, поэтому метод Save тут скорей всего для каких-то особых случаев, когда вы точно хотите создать новый снимок (пока мне на ум такой кейс не приходит, но возможно он есть).

Простой пример использования:

package main import ( "fmt" "github.com/claygod/coffer"
) const curDir = "./" func main() 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 — логируем ошибку из репорта и прекращаем работу с БД

Offtop

Для названия выбран один из вариантов перевода слова ящик на английский язык, предпочёл бы конечно box, но это слишком популярное слово, надеюсь, coffer тоже сойдёт.
Тема с ACID мне кажется достаточно холиварная, поэтому я бы сказал, что Coffer стремится к этому, но не факт, и я не утверждаю, что у него это получилось.

Производительность

Именно в таком режиме она проявляет свою эффективность (хотя это наверно слишком громко сказано). Я сразу писал БД с учётом параллелизма и конкуренции. Это конечно искусственный бенч, и реальность будет совсем иной, т.к. В лежащих ниже результатах бенчмарк демонстрирует пропускную способность в 200к rps. Но тенденция по крайней мере понятна. многое зависит от размера записываемых данных, количества уже записанных данных, производительности железа и фазы луны. Если же БД использовать однопоточно, каждый запрос выполнять только после получения ответа на предыдущий, скорость будет медленной, и я бы посоветовал глядеть другие БД, но не Coffer.

  • BenchmarkCofferTransactionSequence-4 2000 227928 ns/op
  • BenchmarkCofferTransactionPar32HalfConcurent-4 100000 4199 ns/op

Мне очень интересно, на каких машинах какую производительность покажет БД. Кстати, если кто-то потратит время и склонирует себе репозиторий с Coffer, по возмодности, запустите лежащий в нём бенч. Это мне особенно стало понятно, после того как я не так давно купил себе новый Samsung EVO. Прежде всего, конечно всё зависит от диска. Старичок Toshiba продолжает исправно служить и хранит сейчас в себе мой видеоархив. Но не беспокойтесь, это не на замену убитому диску.

Его конечно можно здорово усовершенствовать, например, чтобы сделать быстрыми выборки ключей по префиксам и суффиксам. Встроенный инмемори стораж пока представлянт собой простой мэп, даже не поделенный на секции. основной функционал, так сказать фишку БД я вижу в транзакциях, и узким местом в производительности для транзакций будет работа с диском, и уже потом, работа с памятью. Пока я этим не занимался, т.к.

Лицензия

Дальнейшие планы по развитию БД в стадии формирования.
В общем случаем мне интересно, чтобы БД использовали как пакет, и ориентировались прежде всего на его API. Сейчас лицензия позволяет хранить в базе до десяти миллионов записей, мне показалось, что это достаточная цифра.

Выводы

К сожалению, из-за того, что это почти всегда подразумевает наличие нескольких инстансов, использовать при таком кейсе встраиваемую БД не стоит. В последнее время часто встречаюсь с задачей написания сервисов с характеристикой высокой доступности. Это мне кажется более редким кейсом, но тем не менее он есть, и на такой случай неплохо иметь БД, старающуюся по возможности, сберечь хранящиеся в неё данные. Остаётся вариант обычного приложения или сервиса, существующего в одном экземпляре. Посмотрим, насколько у него это получается. Созданный мной Coffer пытается решить такую задачу.

Благодарности

  • Всем, кто дочитал статью до самого конца
  • Комментаторам, пожелавшим поделиться своим мнением
  • Приславшим в личку инфу по опечаткам и ошибкам в тексте
  • Соседу, включающему музыку по ночам

Ссылки

Репозиторий БД
Описание на русском языке

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Проверьте также

Закрыть
Кнопка «Наверх»
Закрыть