Erlang Mnesia доступ к базе данных - PullRequest
2 голосов
/ 01 декабря 2011

Я разработал базу данных Mnesia с 5 различными таблицами.Идея состоит в том, чтобы смоделировать запросы от множества узлов (компьютеров), а не только от одного, в данный момент с терминала я могу выполнить запрос, но мне просто нужна помощь, как я могу сделать так, чтобы я запрашивал информацию со многих компьютеров.Я тестирую масштабируемость и хочу исследовать производительность mnesia по сравнению с другими базами данных.Любая идея будет высоко оценена.

Ответы [ 2 ]

8 голосов
/ 02 декабря 2011

Лучший способ протестировать mnesia - использовать интенсивную многопоточную работу как на локальном узле Erlang, где работает mnesia, так и на удаленных узлах. Обычно вы хотите иметь удаленные узлы, использующие <b>RPC calls</b>, в которых операции чтения и записи выполняются в таблицах mnesia. Конечно, с высоким параллелизмом приходит компромисс; скорость транзакций уменьшится, многие могут быть повторены, поскольку блокировок может быть много в данный момент времени; Но mnesia гарантирует, что все процессы получат {atomic,ok} за каждый сделанный ими транзакционный вызов.

Концепция
Я предлагаю, чтобы у нас была неблокирующая перегрузка и с записью, и с чтением, направленным в каждую таблицу мнезийных операций как можно большим количеством процессов. Мы измеряем разницу во времени между вызовом функции write и временем, которое требуется нашему массовому абоненту mnesia для получения события Write. Эти события отправляются mnesia каждый раз после успешной транзакции, поэтому нам не нужно прерывать рабочие процессы / процессы перегрузки, а позволить «сильному» подписчику mnesia ожидать асинхронных событий, сообщающих об успешном удалении и записи, как только они происходят.
Метод здесь заключается в том, что мы берем отметку времени в точке непосредственно перед вызовом функции записи, а затем записываем record key, write <b>CALL</b> timestamp. Тогда наш абонент mnesia запишет record key, write/read <b>EVENT</b> timestamp. Тогда разница во времени между этими двумя временными метками (давайте назовем это: CALL-to-EVENT Time) дала бы нам приблизительное представление о том, насколько загружены или насколько эффективны наши действия. Поскольку блокировки увеличиваются с параллелизмом, мы должны регистрировать увеличивающийся параметр CALL-to-EVENT Time. Процессы, выполняющие запись (без ограничений), будут делать это одновременно, в то время как те, кто выполняет чтение, также будут продолжать делать это без прерываний. Мы выберем количество процессов для каждой операции, но давайте сначала заложим основу для всего теста.
Все вышеприведенное понятие предназначено для локальных операций (процессы, запущенные на том же узле, что и Mnesia)

-> Имитация множества узлов
Ну, я лично не имитировал узлы в Erlang, я всегда работал с настоящими узлами Erlang на одной и той же коробке или на нескольких разных машинах в сетевом окружении. Тем не менее, я советую вам внимательно посмотреть на этот модуль: http://www.erlang.org/doc/man/slave.html, сконцентрируйтесь больше на этом здесь: http://www.erlang.org/doc/man/ct_slave.html, и посмотрите на следующие ссылки, когда они говорят о создании, моделировании и управление многими узлами под другим родительским узлом (http://www.erlang.org/doc/man/pool.html, Erlang: начальный подчиненный узел , https://support.process -one.net / doc / display / ERL / Starting + a + set + of + Erlang + кластер + узлы , http://www.berabera.info/oldblog/lenglet/howtos/erlangkerberosremctl/index.html). Я не буду погружаться в джунгли узлов Erlang здесь, потому что это также другой сложная тема, но я сконцентрируюсь на тестах на том же узле, где работает mnesia. Я придумал вышеупомянутую концепцию тестирования mnesia и здесь, давайте приступим к ее реализации.

Теперь, во-первых, вам нужно составить план тестирования для каждой таблицы (отдельно). Это должно включать как записи, так и чтения. Затем вам нужно решить, хотите ли вы выполнять грязные операции или транзакционные операции с таблицами. Вам нужно проверить скорость обхода таблицы мнезий по отношению к ее размеру. Давайте возьмем пример простой таблицы мнезий

-record(key_value,{key,value,instanceId,pid}).

Мы бы хотели иметь общую функцию для записи в нашу таблицу, здесь ниже:

write(Record)->
    %% Use mnesia:activity/4 to test several activity
    %% contexts (and if your table is fragmented)
    %% like the commented code below
    %%
    %%  mnesia:activity(
    %%      transaction, %% sync_transaction | async_dirty | ets | sync_dirty
    %%      fun(Y) -> mnesia:write(Y) end,
    %%      [Record],
    %%      mnesia_frag
    %%  )
    mnesia:transaction(fun() -> ok = mnesia:write(Record) end).

И для наших чтений у нас будет:

read(Key)->
    %% Use mnesia:activity/4 to test several activity
    %% contexts (and if your table is fragmented)
    %% like the commented code below
    %%
    %%  mnesia:activity(
    %%      transaction, %% sync_transaction | async_dirty| ets | sync_dirty
    %%      fun(Y) -> mnesia:read({key_value,Y}) end,
    %%      [Key],
    %%      mnesia_frag
    %%  )
    mnesia:transaction(fun() -> mnesia:read({key_value,Key}) end).
Теперь мы хотим записать очень много записей в нашу маленькую таблицу. Нам нужен генератор ключей. Этот генератор ключей будет нашим собственным генератором псевдослучайных строк. Однако нам нужен наш генератор, чтобы сообщить нам, как только он сгенерирует ключ, чтобы мы его записали. Мы хотим посмотреть, сколько времени потребуется, чтобы написать сгенерированный ключ. Давайте отложим это так:
timestamp()-> erlang:now().<br>
str(XX)-> integer_to_list(XX).<br>
generate_instance_id()->
    random:seed(now()),
    guid() ++ str(crypto:rand_uniform(1, 65536 * 65536)) ++ str(erlang:phash2({self(),make_ref(),time()})).<br> 
guid()->
    random:seed(now()),
    MD5 = erlang:md5(term_to_binary({self(),time(),node(), now(), make_ref()})),
    MD5List = binary_to_list(MD5),
    F = fun(N) -> f("~2.16.0B", [N]) end,
    L = lists:flatten([F(N) || N <- MD5List]),
  %% tell our massive mnesia subscriber about this generation
    InstanceId = generate_instance_id(),
    <b>mnesia_subscriber ! {self(),{key,write,L,timestamp(),InstanceId}},</b>
    {L,InstanceId}.
Чтобы сделать очень много одновременных записей, нам нужна функция, которая будет выполняться многими процессами, которые мы создадим.В этой функции желательно НЕ помещать какие-либо блокирующие функции, такие как sleep/1, обычно реализуемые как sleep(T)-> receive after T -> true end..Такая функция заставляет выполнение процессов зависать в течение указанных миллисекунд.mnesia_tm выполняет управление блокировками, повторные попытки, блокировку и т. Д. От имени процессов, чтобы избежать «мертвых» блокировок.Допустим, мы хотим, чтобы каждый процесс записал unlimited amount of records.Вот наша функция:
-define(NO_OF_PROCESSES,20).

start_write_jobs()->
    [spawn(?MODULE,generate_and_write,[]) || _ <- lists:seq(1,?NO_OF_PROCESSES)],
    ok.

generate_and_write()-> 
    %% remember that in the function ?MODULE:guid/0,
    %% we inform our mnesia_subscriber about our generated key
    %% together with the timestamp of the generation just before 
    %% a write is made.
    %% The subscriber will note this down in an ETS Table and then
    %% wait for mnesia Event about the write operation. Then it will
    %% take the event time stamp and calculate the time difference
    %% From there we can make judgement on performance. 
    %% In this case, we make the processes make unlimited writes 
    %% into our mnesia tables. Our subscriber will trap the events as soon as
    %% a successful write is made in mnesia
    %% For all keys we just write a Zero as its value<br>
    {Key,Instance} = guid(),
    write(#key_value{key = Key,value = 0,instanceId = Instance,pid = self()}),
    generate_and_write().

Аналогично, давайте посмотрим, как будут выполняться задания на чтение.У нас будет поставщик ключей, этот поставщик ключей будет вращаться вокруг стола mnesia, выбирая только ключи, вверх и вниз по столу, который он будет продолжать вращать.Вот его код:

first()-> mnesia:dirty_first(key_value).

next(FromKey)-> mnesia:dirty_next(key_value,FromKey).

start_key_picker()-> register(key_picker,spawn(fun() -> key_picker() end)).

key_picker()->
    try ?MODULE:first() of      
        '$end_of_table' -> 
            io:format("\n\tTable is empty, my dear !~n",[]),
            %% lets throw something there to start with
            ?MODULE:write(#key_value{key = guid(),value = 0}),
            key_picker();
        Key -> wait_key_reqs(Key)
    catch
        EXIT:REASON -> 
            error_logger:error_info(["Key Picker dies",{EXIT,REASON}]),
            exit({EXIT,REASON})
    end.

wait_key_reqs('$end_of_table')->
receive
    {From,<<"get_key">>} -> 
        Key = ?MODULE:first(),
        From ! {self(),Key},
        wait_key_reqs(?MODULE:next(Key));
    {_,<<"stop">>} -> exit(normal)
end;
wait_key_reqs(Key)->
receive
    {From,<<"get_key">>} -> 
        From ! {self(),Key},
        NextKey = ?MODULE:next(Key),
        wait_key_reqs(NextKey);
    {_,<<"stop">>} -> exit(normal)
end.

key_picker_rpc(Command)->
    try erlang:send(key_picker,{self(),Command}) of
        _ -> 
            receive
                {_,Reply} -> Reply
            after timer:seconds(60) -> 
                %% key_picker hang, or too busy
                erlang:throw({key_picker,hanged})
            end
    catch
        _:_ -> 
            %% key_picker dead
            start_key_picker(),
            sleep(timer:seconds(5)),
            key_picker_rpc(Command)
    end.

%% Now, this is where the reader processes will be
%% accessing keys. It will appear to them as though
%% its random, because its one process doing the 
%% traversal. It will all be a game of chance
%% depending on the scheduler's choice
%% he who will have the next read chance, will
%% win ! okay, lets get going below :)

get_key()-> 
    Key = key_picker_rpc(<<"get_key">>),

    %% lets report to our "massive" mnesia subscriber
    %% about a read which is about to happen
    %% together with a time stamp.
    Instance = generate_instance_id(),
    mnesia_subscriber ! {self(),{key,read,Key,timestamp(),Instance}},
    {Key,Instance}. 

Ух ты !!!Теперь нам нужно создать функцию, с которой мы запустим всех читателей.

-define(NO_OF_READERS,10).

start_read_jobs()->
    [spawn(?MODULE,constant_reader,[]) || _ <- lists:seq(1,?NO_OF_READERS)],
    ok.

constant_reader()->
    {Key,InstanceId} = ?MODULE:get_key(),
    Record = ?MODULE:read(Key),
    %% Tell mnesia_subscriber that a read has been done so it creates timestamp
    mnesia:report_event({read_success,Record,self(),InstanceId}),   
    constant_reader().

Теперь самая большая часть; mnesia_subscriber !!!Это простой процесс, который подпишется на простые события.Получить документацию о событиях mnesia из руководства пользователя mnesia.Вот подписчик Mnesia

-record(read_instance,{
        instance_id,
        before_read_time,
        after_read_time,
        read_time       %% after_read_time - before_read_time

    }).

-record(write_instance,{
        instance_id,
        before_write_time,
        after_write_time,
        write_time          %% after_write_time - before_write_time
    }).

-record(benchmark,{
        id,         %% {pid(),Key}
        read_instances = [],
        write_instances = []
    }).

subscriber()->
    mnesia:subscribe({table,key_value, simple}),

    %% lets also subscribe for system
    %% events because events passing through
    %% mnesia:event/1 will go via
    %% system events. 

    mnesia:subscribe(system),
    wait_events().

-include_lib("stdlib/include/qlc.hrl").

wait_events()->
receive
    {From,{key,write,Key,TimeStamp,InstanceId}} -> 
        %% A process is just about to call
        %% mnesia:write/1 and so we note this down
        Fun = fun() -> 
                case qlc:e(qlc:q([X || X <- mnesia:table(benchmark),X#benchmark.id == {From,Key}])) of
                    [] -> 
                        ok = mnesia:write(#benchmark{
                                id = {From,Key},
                                write_instances = [
                                        #write_instance{
                                            instance_id = InstanceId,
                                            before_write_time = TimeStamp                                               
                                        }]
                                }),
                                ok;
                    [Here] -> 
                        WIs = Here#benchmark.write_instances,
                        NewInstance = #write_instance{
                                        instance_id = InstanceId,
                                        before_write_time = TimeStamp                                               
                                    },
                        ok = mnesia:write(Here#benchmark{write_instances = [NewInstance|WIs]}),
                        ok                          
                end
            end,
        mnesia:transaction(Fun),
        wait_events();      
    {mnesia_table_event,{write,#key_value{key = Key,instanceId = I,pid = From},_ActivityId}} ->
        %% A process has successfully made a write. So we look it up and 
        %% get timeStamp difference, and finish bench marking that write
        WriteTimeStamp = timestamp(),
        F = fun()->
                [Here] = mnesia:read({benchmark,{From,Key}}),
                WIs = Here#benchmark.write_instances,
                {_,WriteInstance} = lists:keysearch(I,2,WIs),
                BeforeTmStmp = WriteInstance#write_instance.before_write_time,
                NewWI = WriteInstance#write_instance{
                            after_write_time = WriteTimeStamp,
                            write_time = time_diff(WriteTimeStamp,BeforeTmStmp)
                        },
                ok = mnesia:write(Here#benchmark{write_instances = [NewWI|lists:keydelete(I,2,WIs)]}),
                ok
            end,
        mnesia:transaction(F),
        wait_events();      
    {From,{key,read,Key,TimeStamp,InstanceId}} ->
        %% A process is just about to do a read
        %% using mnesia:read/1 and so we note this down
        Fun = fun()-> 
                case qlc:e(qlc:q([X || X <- mnesia:table(benchmark),X#benchmark.id == {From,Key}])) of
                    [] -> 
                        ok = mnesia:write(#benchmark{
                                id = {From,Key},
                                read_instances = [
                                        #read_instance{
                                            instance_id = InstanceId,
                                            before_read_time = TimeStamp                                                
                                        }]
                                }),
                                ok;
                    [Here] -> 
                        RIs = Here#benchmark.read_instances,
                        NewInstance = #read_instance{
                                        instance_id = InstanceId,
                                        before_read_time = TimeStamp                                            
                                    },
                        ok = mnesia:write(Here#benchmark{read_instances = [NewInstance|RIs]}),
                        ok
                end
            end,
        mnesia:transaction(Fun),
        wait_events();
    {mnesia_system_event,{mnesia_user,{read_success,#key_value{key = Key},From,I}}} ->
        %% A process has successfully made a read. So we look it up and 
        %% get timeStamp difference, and finish bench marking that read
        ReadTimeStamp = timestamp(),
        F = fun()->
                [Here] = mnesia:read({benchmark,{From,Key}}),
                RIs = Here#benchmark.read_instances,
                {_,ReadInstance} = lists:keysearch(I,2,RIs),
                BeforeTmStmp = ReadInstance#read_instance.before_read_time,
                NewRI = ReadInstance#read_instance{
                            after_read_time = ReadTimeStamp,
                            read_time = time_diff(ReadTimeStamp,BeforeTmStmp)
                        },
                ok = mnesia:write(Here#benchmark{read_instances = [NewRI|lists:keydelete(I,2,RIs)]}),
                ok
            end,
        mnesia:transaction(F),
        wait_events();  
    _ -> wait_events();
end.

time_diff({A2,B2,C2} = _After,{A1,B1,C1} = _Before)->        
    {A2 - A1,B2 - B1,C2 - C1}.


Хорошо!Это было огромно :) Итак, мы закончили с подписчиком.Нам нужно собрать код, который соберет все вместе и запустить необходимые тесты.

install()->
    mnesia:stop().
    mnesia:delete_schema([node()]),
    mnesia:create_schema([node()]),
    mnesia:start(),
    {atomic,ok} = mnesia:create_table(key_value,[
        {attributes,record_info(fields,key_value)},
        {disc_copies,[node()]}<br>
        ]),
    {atomic,ok} = mnesia:create_table(benchmark,[
            {attributes,record_info(fields,benchmark)},
            {disc_copies,[node()]}
        ]),
    mnesia:stop(),
    ok.<br> 
start()->
    mnesia:start(),
    ok = mnesia:wait_for_tables([key_value,benchmark],timer:seconds(120)),
    %% boot up our subscriber
    register(mnesia_subscriber,spawn(?MODULE,subscriber,[])),
    start_write_jobs(),
    start_key_picker(),
    start_read_jobs(),
    ok.

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




Тестирование с удаленных компьютеров, моделирование узлов, сравнение с другими СУБД может быть не столь актуально просто по многим причинам.Понятия, мотивы и цели Mnesia очень отличаются от нескольких типов существующих типов баз данных, таких как: БД, ориентированные на документы, СУБД, объектно-ориентированные БД и т. Д. Фактически, Mnesia сравнивается с базой данных, такой как thisодин .Это распределенные DBM с гибридными / неструктурированными структурами данных, которые принадлежат языку Erlang.Сравнительный анализ Mnesia с базой данных другого типа может быть неправильным, поскольку его назначение сильно отличается от многих и его тесная связь с Erlang / OTP.Однако знание того, как работает mnesia, контексты транзакций, индексация, параллелизм, распространение, может быть ключом к хорошему дизайну базы данных.Mnesia может хранить очень сложную структуру данных.Помните, что чем сложнее структура данных с вложенной информацией, тем больше работы требуется для ее распаковки и извлечения необходимой информации во время выполнения, что означает больше циклов ЦП и памяти.Иногда нормализация с помощью mnesia может привести к снижению производительности, поэтому реализация ее концепций далека от других баз данных.
Хорошо, что вы заинтересованы в производительности Mnesia на нескольких машинах (распределенных), однако производительность такая же, как и в распределенном Erlang.Самое замечательное, что атомарность обеспечивается для каждой транзакции.Все еще одновременные запросы от удаленных узлов могут быть отправлены через вызовы RPC.Помните, что если у вас есть несколько реплик mnesia на разных машинах, процессы, запущенные на каждом узле, будут писать на этот самый узел, тогда mnesia продолжит свою репликацию.Mnesia очень быстро выполняет репликацию, за исключением случаев, когда сеть действительно работает плохо и / или узлы не подключены или сеть разбита на части во время выполнения.
Mnesia обеспечивает согласованность и атомарность операций CRUD.По этой причине реплицированные базы данных mnesia сильно зависят от доступности сети для повышения производительности.Пока узлы Erlang остаются подключенными, два или более узлов Mnesia всегда будут иметь одинаковые данные.Чтение на одном узле гарантирует, что вы получите самую свежую информацию.Проблемы возникают, когда происходит разъединение, и каждый узел регистрирует другой, как будто он отключен.Дополнительную информацию о производительности mnesia можно найти по следующим ссылкам

http://igorrs.blogspot.com/2010/05/mnesia-one-year-later.html
http://igorrs.blogspot.com/2010/05/mnesia-one-year-later-part-2.html
http://igorrs.blogspot.com/2010/05/mnesia-one-year-later-part-3.html
http://igorrs.blogspot.com/2009/11/consistent-hashing-for-mnesia-fragments.html

Как следствие, концепции, лежащие в основе mnesia, можно сравнить только с базой данных NDB Ericsson, найденной здесь: http://ww.dolphinics.no/papers/abstract/ericsson.html,, но не с существующими СУБД или БД, ориентированными на документы,и т.д. Это мои мысли :) давайте подождем, что скажут другие .....

0 голосов
/ 02 декабря 2011

Вы запускаете дополнительные узлы, используя следующую команду:

erl -name test1@127.0.0.1 -cookie devel \
    -mnesia extra_db_nodes "['devel@127.0.0.1']"\
    -s mnesia start

где 'devel@127.0.0.1' - это узел, где mnesia уже настроена. В этом случае все таблицы будут доступны с удаленного узла, но вы можете сделать локальные копии с помощью mnesia:add_table_copy/3.

Затем вы можете использовать spawn/2 или spawn/4, чтобы начать генерацию нагрузки на всех узлах с чем-то вроде:

lists:foreach(fun(N) ->
                  spawn(N, fun () ->
                               %% generate some load
                               ok
                           end
              end,
     [ 'test1@127.0.0.1', 'test2@127.0.0.1' ]
)
...