- PVSM.RU - https://www.pvsm.ru -
В интернетах довольно много информации и споров по поводу выбора sql/nosql подхода, а также плюсах и минусах того или иного KV-хранилища. То, что вы сейчас читаете, не является пособием по rocksdb или агитацией за использование именного этого хранилища и моего драйвера к нему. Я хотел бы поделиться промежуточным результатом работы по оптимизации процесса разработки NIF для Erlang. В данной статье представлен работоспособный драйвер для rocksdb, разработанный за пару вечеров.
Итак, в одном из проектов возникла задача надежной обработки большого объема событий. Каждое событие занимает от 50 до 350 байт, в день на один узел генерируется более 80 млн событий. Сразу хочется отметить, что вопросы отказоустойчивости доставки сообщений на узлы не рассматриваются. Также одним из ограничений обработки является атомарное и консистентное изменение группы событий.
Таким образом, основными требованиями к драйверу являются:
После беглого анализа текущей ситуации с драйверами erlang для rocksdb стало понятно, что ни один из них не соответствует требованиям проекта полностью. Хотя можно было бы и использовать erlang-rocksdb, но появилась пара свободных вечеров, а после успешной разработки и внедрения фильтра Блума на Rust и любопытство: возможно ли реализовать все требования текущего проекта и воплотить большинство функций в NIF за короткий промежуток времени?
Rocker – это NIF для Erlang, использующий Rust обертку для rocksdb. Ключевыми особенностями являются безопасность, производительность и минимальная кодовая база. Ключи и данные хранятся в бинарном виде, что не накладывает никаких ограничений на формат хранения. На данный момент проект пригоден для использования в сторонних решениях.
Исходный код находится в репозитории проекта [4].
Работа с базой возможна в двух режимах:
Общее пространство ключей. В данном режиме все ваши ключи будут помещены в одно множество. Rocksdb позволяет гибко настраивать опции хранилища под текущие задачи. В зависимости от них базу можно открыть двумя способами:
используя стандартный набор опций
rocker:open_default(<<"/project/priv/db_default_path">>) -> {ok, Db}.
Результатом этой операции станет указатель для работы с базой, а база будет заблокирована для любых других попыток открытия. База будет автоматически разблокирована сразу после очистки данного указателя.
{ok, Db} = rocker:open(<<"/project/priv/db_path">>, #{
create_if_missing => true,
set_max_open_files => 1000,
set_use_fsync => false,
set_bytes_per_sync => 8388608,
optimize_for_point_lookup => 1024,
set_table_cache_num_shard_bits => 6,
set_max_write_buffer_number => 32,
set_write_buffer_size => 536870912,
set_target_file_size_base => 1073741824,
set_min_write_buffer_number_to_merge => 4,
set_level_zero_stop_writes_trigger => 2000,
set_level_zero_slowdown_writes_trigger => 0,
set_max_background_compactions => 4,
set_max_background_flushes => 4,
set_disable_auto_compactions => true,
set_compaction_style => universal
}).
{ok, Db} = case rocker:list_cf(BookDbPath) of
{ok, CfList} -> rocker:open_cf_default(BookDbPath, CfList);
_Else -> CfList = [], rocker:open_default(BookDbPath)
end.
Для корректного удаления базы данных необходимо вызвать rocker:destroy(Path).
При этом база не должна использоваться.
В случае системного сбоя базу можно восстановить с помощью метода rocker:repair(Path)
, Данный процесс состоит из 4 шагов:
Cf = <<"testcf1">>,
rocker:create_cf_default(Db, Cf) -> ok.
Cf = <<"testcf1">>,
rocker:drop_cf(Db, Cf) -> ok.
rocker:put(Db, <<"key">>, <<"value">>) -> ok.
rocker:get(Db, <<"key">>) -> {ok, <<"value">>} | notfound
rocker:delete(Db, <<"key">>) -> ok.
rocker:put_cf(Db, <<"testcf">>, <<"key">>, <<"value">>) -> ok.
rocker:get_cf(Db, <<"testcf">>, <<"key">>) -> {ok, <<"value">>} | notfound
rocker:delete_cf(Db, <<"testcf">>, <<"key">>) -> ok
Как известно, одним из основных принципов работы rocksdb является упорядоченное хранение ключей. Данная особенность очень полезна в реальных задачах. Чтобы ей воспользоваться нам необходимы итераторы данных. В rocksdb есть несколько режимов прохода по данным (подробные примеры кода можно найти в тестах [5]):
{'start'}
{'end'}
{'from', Key, forward}
{'from', Key, reverse}
Стоит отметить, что эти режимы также работают и для прохода по данным, хранящимся в column families.
rocker:iterator(Db, {'start'}) -> {ok, Iter}.
rocker:iterator_valid(Iter) -> {ok, true} | {ok, false}.
rocker:iterator_cf(Db, Cf, {'start'}) -> {ok, Iter}.
Префиксный итератор требует явного указания длины префикса при создании базы данных.
{ok, Db} = rocker:open(Path, #{
prefix_length => 3
}).
Пример создания итератора по префиксу “aaa”:
{ok, Iter} = rocker:prefix_iterator(Db, <<"aaa">>).
Аналогично предыдущему префиксному итератору, требует явного задания prefix_length
для column family
{ok, Iter} = rocker:prefix_iterator_cf(Db, Cf, <<"aaa">>).
Метод возвращает следующее ключ/значение, либо ok, если итератор завершился.
rocker:next(Iter) -> {ok, <<"key">>, <<"value">>} | ok
Довольно частое явление – требование одновременной записи изменений группы ключей. Rocker позволяет объединять CRUD операции как в рамках общего множества, так и в рамках CF.
Данный пример иллюстрирует работу с транзакциями:
{ok, 6} = rocker:tx(Db, [
{put, <<"k1">>, <<"v1">>},
{put, <<"k2">>, <<"v2">>},
{delete, <<"k0">>, <<"v0">>},
{put_cf, Cf, <<"k1">>, <<"v1">>},
{put_cf, Cf, <<"k2">>, <<"v2">>},
{delete_cf, Cf, <<"k0">>, <<"v0">>}
]).
В наборе тестов можно найти тест производительности. Он показывает около 30к RPS на запись и 200к RPS на чтение на моей машине. В реальных условиях можно ожидать 15-20к RPS на запись и около 120к RPS на чтение при среднем размере данных около 1 Кб на ключ и общего количества ключей больше 1 млрд.
Разработка и применение Rocker в нашем проекте позволила уменьшить время отклика системы, повысить надежность, уменьшить время перезапуска. Данные плюсы удалось получить с минимальными затратами на разработку и внедрение.
Лично для себя я сделал вывод, что для Erlang проектов, требующих оптимизации, применение Rust оптимально. На Erlang удается быстро и эффективно реализовать 95% кода, а на Rust переписать/дописать тормозящие 5% без снижения общей надежности системы.
P.S. Есть позитивный опыт разработки NIF для Arbitrary-precision arithmetic в Erlang, который можно оформить в виде отдельной статьи. Хотелось бы уточнить, интересна ли тема NIF на Rust сообществу?
Автор: mr_elzor
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/282253
Ссылки в тексте:
[1] erocksdb: https://github.com/leo-project/erocksdb
[2] rockse: https://github.com/ttyerlsol/rockse
[3] erlang-rocksdb: https://gitlab.com/barrel-db/erlang-rocksdb
[4] репозитории проекта: https://github.com/Vonmo/rocker
[5] тестах: https://github.com/Vonmo/rocker/blob/master/test/rocker_SUITE.erl
[6] Источник: https://habr.com/post/413353/?utm_campaign=413353
Нажмите здесь для печати.