Дело было вечером, делать было нечего, и я пошел гулять по интернетам в поисках существующих встраиваемых (embedded) баз данных для Erlang. Вот, скажем, есть у нас приложенька, которая работает с небольшими (десяток-другой гигабайт) объемами не саммых ценных в системе данных. Ну не поднимать же Riak кластер только ради этой приложеньки и не таскать же за ней повсюду PostgreSQL ? В общем, посмотрел я на существующие решения (DETS, Mnesia, Bitcask, LevelDB, SQLite, Innostore, HanoiDB) и пошел классно писать очередной ненужный KV-велосипед.

SimplicityDB — это очень простая фигня, даже не база данных на самом деле. Вы пишите в конфиге что-то вроде:

[
{simplicitydb, [
{dir, «/var/tmp/simplicitydb»},
{pool_size, 32},
{sync, true}
]}
].

Здесь dir задает путь к каталогу, куда будут писаться все данные, pool_size задает количество воркеров, которые будут обслуживать базу, а sync говорит этим воркерам делать fsync после каждой записи данных.

Затем запускаете приложеньку:

$ erl -pa ./apps/simplicitydb/ebin -config test.config
1> application:ensure_all_started(simplicitydb).

Запуск может растянутся секунд на пять или больше, если у вас не SSD. При запуске в dir будет создано 256 каталогов, каждый из которых внутри также имеет 256 пока что пустых каталогов. Такая инициализация сделана чтобы воркерам во время работы приходилось делать меньше проверок и созданий каталогов. Воркеров, соответственно, может быть не более 256. Каждый из них работает в своем подмножестве каталогов и не мешается другим.

Теперь вы что-то пишите в базу:

2> simplicitydb:write([some,key], [ {k1, v1}, {k2, v2} ]).

При этом происходит следующее. Ключ сериализуется с помощью term_to_binary, от результата считается crc32, на основе контрольной суммы выбирается воркер и ему делается gen_server:call. Также, чтобы воркер выполнял меньше работы в handle_call, на стороне клиента определяется полное имя файла, в который нужно записать значение. В нашем случае имя файла будет таким:

3> T = term_to_binary([some,key]).
<<131,108,0,0,0,2,100,0,4,115,111,109,101,100,0,3,107,101,121,106>>
4> simplicitydb_utils:hash_to_filepath(erlang:crc32(T)).
«/217/20/458/»
5> simplicitydb_utils:key_to_filename(T).
«idgmaaaaaaacgeaaaehdgpgngfgeaaadglgfhjgk»

Здесь имя файла — это base16 от сериализованного ключа. Благодаря такой схеме базулька может работать на ФС с case insensitive именами файлов. Кроме того, поскольку ключ однозначно отображается в имя файла, не нужно разрешать никаких конфликтов, как пришлось бы, если бы использовались какие-то хэши. С другой стороны, в теории могут возникнуть сложности при использовании слишком длинных ключей.

Получив запрос на чтение, воркер (simplicitydb_storage_srv) проверяет, есть ли уже файл с таким именем и если есть, переименовывает его в старое_имя.bak . Мы не можем просто перезаписывать файлы, ибо если приложение упадет в процессе записи, данные будут потеряны. Затем воркер пишет данные в файл, делает file:datasync/1 , если в конфиге прописано sync = true , закрывает файл, удаляет .bak.

Теперь данные можно считать из базы по заданному ключу:

6> simplicitydb:read([ some, key ]).
{ok,[{k1,v1},{k2,v2}]}

Если данных нет, возвращается undefined. Во время чтения воркер проверяет, есть ли файл, соответствующий ключу, с именем чтото.bak . Если есть, значит во время последней записи что-то пошло не так, файл восстанавливается, затем из него производится чтение. На самом деле, при записи такая проверка тоже происходит, в итоге код получается забавным:

file : rename ( Backup , Filename ) ,
file : rename ( Filename , Backup ) ,

Помимо самих данных каждый файл содержит crc32 хранимых данных. Если при чтении контрольная сумма не сходится, значит данные каким-то образом оказались повреждены и simplicitydb:read/1 возвращает undefined, как будто данных в базе нет. Понятное дело, выполняются все вычисления контрольных сумм на клиенте, чтобы разгрузить gen_server .

Также данные можно удалить:

7> simplicitydb:delete([ some, key ]).
ok

Еще база умеет замораживаться ( freeze/0 ) и размораживатся ( unfreeze/0 ). Когда база заморожена ( is_frozen() =:= true ), писать в нее нельзя, write/2 вернет frozen. Эту возможность можно использовать при тестировании или в скриптах для создания бэкапов.

В общем-то, это все.

SimplicityDB хороша тем, что это реально очень тупая база данных на файликах. Поэтому в ней можно эффективно хранить всякие там mp3, аттачи к электронной почте и так далее. При этом размер базы ограничен только свободным местом на диске. В теории, можно записать очень-очень много маленьких значений, что заставит базу сильно тупить, но по моим расчетам для этого придется занять около 1 Тб места на диске (256 каталогов * 256 каталогов * 1024 каталога * 4096 файлов, по одному байту данных в каждом + 4 байта на crc32 = 1280 Гб), не считая накладных расходов на каталоги и файлы.

При использовании SimplicityDB вы вряд ли столкнетесь с проблемами, что какие-то индексы перестали помещаться в память (как в случае с Bitcask), что чтение данных иногда занимает 5 мс, а иногда — 5 секунд, что кончился счетчик первичных ключей, или что база выделяет место на диске, но не освобождает его. Кроме того, база позволяет использовать в качестве ключей и значений обычные эрланговые термы, что есть удобно. Наконец, структура базы так проста, что в нее в случае необходимости не страшно вонзиться руками. Реализация всего этого добра занимает 150 не пустых строк кода на Erlang без каких-либо зависимостей.

Плоха SimplicityDB тем, что она нефига не проверена в бою. Мне кажется, что я все предусмотрел, но кто знает. В общем, бэкапы, бэкапы. Еще SimplicityDB медленная. У меня на SSD, в несколько потоков и при хорошей погоде с sync = true получалось 230 записей и 3700 чтений в секунду. Если поставить sync = false , скорость записи сильно возрастает (я видел 1700 записей в секунду), но в этом случае вы пишите в кэш ФС, а не на диск, со всеми вытекающими. В общем, для работы с временными рядами базулька не годится. Но если у вас небольшие нагрузки, много данных и меняются они раз в пол года, я бы присмотрелся.

Интересно в базульке то, что поверх нее можно наделать разных прикольных динамических оперденей. Можно прикрутить кэшик и увеличить скорость, можно поднять менеджер локов и получить некое подобие транзакций. Можно без труда реализовать подписку на обновление БД, что, например, не совсем понятно, как сделать в случае с Riak’ом. Можно даже попытаться изобразить подобие индексов на каких-нибудь dict или ETS.

Исходники вы найдете в этом репозитории . Шлите багрепорты и пуллреквесты, не стесняйтесь. Буду рад, если кому-нибудь это пригодится на практике. Если нет — все равно буду рад, потому что писать SimplicityDB было прикольным code kata.

Как обычно, буду счастлив ознакомиться с любыми вашими мыслями по поводу написанного.

admin

Share
Published by
admin

Recent Posts

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

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

2 месяца ago

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

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

2 месяца ago

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

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

2 месяца ago

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

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

2 месяца ago

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

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

2 месяца ago

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

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

2 месяца ago