SimplicityDB, предельно простая встраиваемая KV-базулька, написанная на чистом Erlang
17 марта 2014
Дело было вечером, делать было нечего, и я пошел гулять по интернетам в поисках существующих встраиваемых (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 после каждой записи данных.
Затем запускаете приложеньку:
1> application:ensure_all_started(simplicitydb).
Запуск может растянутся секунд на пять или больше, если у вас не SSD. При запуске в dir будет создано 256 каталогов, каждый из которых внутри также имеет 256 пока что пустых каталогов. Такая инициализация сделана чтобы воркерам во время работы приходилось делать меньше проверок и созданий каталогов. Воркеров, соответственно, может быть не более 256. Каждый из них работает в своем подмножестве каталогов и не мешается другим.
Теперь вы что-то пишите в базу:
При этом происходит следующее. Ключ сериализуется с помощью term_to_binary, от результата считается crc32, на основе контрольной суммы выбирается воркер и ему делается gen_server:call. Также, чтобы воркер выполнял меньше работы в handle_call, на стороне клиента определяется полное имя файла, в который нужно записать значение. В нашем случае имя файла будет таким:
<<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.
Теперь данные можно считать из базы по заданному ключу:
{ok,[{k1,v1},{k2,v2}]}
Если данных нет, возвращается undefined. Во время чтения воркер проверяет, есть ли файл, соответствующий ключу, с именем чтото.bak
. Если есть, значит во время последней записи что-то пошло не так, файл восстанавливается, затем из него производится чтение. На самом деле, при записи такая проверка тоже происходит, в итоге код получается забавным:
file:rename(Filename, Backup),
Помимо самих данных каждый файл содержит crc32 хранимых данных. Если при чтении контрольная сумма не сходится, значит данные каким-то образом оказались повреждены и simplicitydb:read/1
возвращает undefined, как будто данных в базе нет. Понятное дело, выполняются все вычисления контрольных сумм на клиенте, чтобы разгрузить gen_server.
Также данные можно удалить:
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.
Как обычно, буду счастлив ознакомиться с любыми вашими мыслями по поводу написанного.
Метки: Erlang, СУБД, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.