【发布时间】:2012-01-10 15:32:01
【问题描述】:
我设计了一个包含 5 个不同表的 mnesia 数据库。这个想法是模拟来自许多节点(计算机)的查询,而不仅仅是一个节点,此刻我可以从终端执行查询,但我只需要帮助我如何做到这一点,以便我从多台计算机请求信息。我正在测试可伸缩性,并想研究 mnesia 与其他数据库的性能。任何想法都将受到高度赞赏。
【问题讨论】:
标签: database erlang scalability mnesia erlang-otp
我设计了一个包含 5 个不同表的 mnesia 数据库。这个想法是模拟来自许多节点(计算机)的查询,而不仅仅是一个节点,此刻我可以从终端执行查询,但我只需要帮助我如何做到这一点,以便我从多台计算机请求信息。我正在测试可伸缩性,并想研究 mnesia 与其他数据库的性能。任何想法都将受到高度赞赏。
【问题讨论】:
标签: database erlang scalability mnesia erlang-otp
测试 mnesia 的最佳方法是在运行 mnesia 的本地 Erlang 节点和远程节点上使用密集线程作业。通常,您希望远程节点使用<b>RPC calls</b>,其中正在对 mnesia 表执行读取和写入。当然,高并发需要权衡;事务的速度会降低,很多可能会被重试,因为在给定的时间锁可能很多;但是 mnesia 将确保所有进程在他们进行的每个事务调用中都收到一个{atomic,ok} 。
概念
我建议我们有一个非阻塞重载,通过尽可能多的进程将写入和读取定向到每个 mnesia 表。我们测量了调用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: starting slave node,https://support.process-one.net/doc/display/ERL/Starting+a+set+of+Erlang+cluster+nodes,http://www.berabera.info/oldblog/lenglet/howtos/erlangkerberosremctl/index.html)。我不会在这里深入研究 Erlang 节点的丛林,因为这也是另一个复杂的话题,但我将专注于在运行 mnesia 的同一节点上进行测试。我已经提出了上面的 mnesia 测试概念,在这里,让我们开始实现它。
现在,首先,您需要为每个表(单独)制定一个测试计划。这应该包括写入和读取。然后您需要决定是要对表进行脏操作还是事务操作。您需要测试遍历 mnesia 表与其大小相关的速度。让我们举一个简单的记忆表的例子
我们希望有一个通用函数来写入我们的表格,如下所示:
写(记录)-> %% 使用 mnesia:activity/4 测试几个活动 %% 上下文(如果你的表是碎片化的) %% 喜欢下面的注释代码 %% %% 失忆症:活动( %% 交易,%% 同步交易 |异步脏 |等|同步脏 %% fun(Y) -> mnesia:write(Y) end, %% [记录], %% mnesia_frag %%) mnesia:transaction(fun() -> ok = mnesia:write(Record) end)。对于我们的阅读,我们将拥有:
读取(键)-> %% 使用 mnesia:activity/4 测试几个活动 %% 上下文(如果你的表是碎片化的) %% 喜欢下面的注释代码 %% %% 失忆症:活动( %% 交易,%% 同步交易 |异步脏|等|同步脏 %% fun(Y) -> mnesia:read({key_value,Y}) end, %% [钥匙], %% mnesia_frag %%) mnesia:transaction(fun() -> mnesia:read({key_value,Key}) end)。 现在,我们想在我们的小表中写入很多记录。我们需要一个密钥生成器。这个密钥生成器将是我们自己的伪随机字符串生成器。然而,我们需要我们的生成器告诉我们它生成密钥的那一刻,以便我们记录它。我们想看看写一个生成的密钥需要多长时间。让我们这样写:时间戳()-> erlang:now().为了进行非常多的并发写入,我们需要一个函数,该函数将由我们将产生的许多进程执行。在这个函数中,最好不要放置任何阻塞函数,例如
str(XX)-> integer_to_list(XX).
generate_instance_id()-> 随机:种子(现在()), guid() ++ str(crypto:rand_uniform(1, 65536 * 65536)) ++ str(erlang:phash2({self(),make_ref(),time()}))。
引导()-> 随机:种子(现在()), 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]) 结束, L = 列表:展平([F(N)|| N mnesia_subscriber ! {self(),{key,write,L,timestamp(),InstanceId}}, {L,InstanceId}。
sleep/1,通常实现为sleep(T)-> receive after T -> true end.。这样的函数将使进程执行挂起指定的毫秒。 mnesia_tm 执行锁定控制、重试、阻塞等。代表进程避免死锁。可以说,我们希望每个进程都写一个unlimited amount of records。这是我们的功能:
-定义(NO_OF_PROCESSES,20)。
start_write_jobs()->
[spawn(?MODULE,generate_and_write,[]) || _
%% 记得在函数 ?MODULE:guid/0 中,
%% 我们通知我们的 mnesia_subscriber 我们生成的密钥
%% 加上前一代的时间戳
%% 写入。
%% 订阅者将在 ETS 表中记下这一点,然后
%% 等待有关写入操作的 mnesia 事件。然后就会
%% 取事件时间戳并计算时间差
%% 从那里我们可以对性能做出判断。
%% 在这种情况下,我们让进程无限写入
%% 进入我们的记忆表。我们的订阅者将尽快捕获事件
%% 在 mnesia 中成功写入
%% 对于所有键,我们只写一个零作为它的值同样,让我们看看如何完成读取作业。 我们将有一个 Key 提供者,这个 Key 提供者会一直围绕着 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()-> 试试 ?MODULE:first() 的 '$end_of_table' -> io:format("\n\tTable 是空的,亲爱的!~n",[]), %% 让我们先扔一些东西 ?MODULE:write(#key_value{key = guid(),value = 0}), key_picker(); 键 -> wait_key_reqs(键) 抓住 退出:原因 -> error_logger:error_info(["Key Picker dies",{EXIT,REASON}]), 退出({退出,原因}) 结尾。 wait_key_reqs('$end_of_table')-> 收到 {来自,>} -> 键 = ?MODULE:first(), 从 ! {self(),键}, wait_key_reqs(?MODULE:next(Key)); {_,>} -> 退出(正常) 结尾; wait_key_reqs(键)-> 收到 {来自,>} -> 从 ! {self(),键}, NextKey = ?MODULE:next(Key), wait_key_reqs(NextKey); {_,>} -> 退出(正常) 结尾。 key_picker_rpc(命令)-> 试试 erlang:send(key_picker,{self(),Command}) 的 _ -> 收到 {_,回复} -> 回复 计时器后:秒(60)-> %% key_picker 挂起,或者太忙 erlang:throw({key_picker,hanged}) 结尾 抓住 _:_ -> %% key_picker 死了 start_key_picker(), 睡眠(定时器:秒(5)), key_picker_rpc(命令) 结尾。 %% 现在,这是阅读器进程所在的位置 %% 访问密钥。在他们看来,好像 %% 它是随机的,因为它的一个进程正在执行 %% 遍历。这一切都将是一场机会游戏 %% 取决于调度程序的选择 %% 他将有下一次阅读机会,将 %% 赢 !好吧,让我们继续往下看:) get_key()-> 密钥 = key_picker_rpc(>), %% 让我们向我们的“大量”mnesia 订阅者报告 %% 关于即将发生的读取 %% 加上时间戳。 实例 = generate_instance_id(), mnesia_subscriber ! {self(),{key,read,Key,timestamp(),Instance}}, {键,实例}。哇!!!现在我们需要创建启动所有阅读器的函数。
-定义(NO_OF_READERS,10)。 start_read_jobs()-> [spawn(?MODULE,constant_reader,[]) || _ {Key,InstanceId} = ?MODULE:get_key(), 记录 = ?MODULE:read(Key), %% 告诉 mnesia_subscriber 已完成读取,因此它会创建时间戳 mnesia:report_event({read_success,Record,self(),InstanceId}), 常数阅读器()。现在,最重要的部分; mnesia_subscriber !!!这是一个简单的订阅过程 到简单的事件。从 mnesia 用户指南中获取 mnesia 事件文档。 这是mnesia订阅者
-记录(读取实例,{ instance_id, before_read_time, after_read_time, read_time %% after_read_time - before_read_time })。 -记录(写实例,{ instance_id, before_write_time, after_write_time, write_time %% after_write_time - before_write_time })。 -记录(基准,{ id, %% {pid(),Key} read_instances = [], write_instances = [] })。 订户()-> mnesia:subscribe({table,key_value, simple}), %% 也让我们订阅系统 %% 事件,因为事件经过 %% mnesia:event/1 将通过 %% 系统事件。 记忆:订阅(系统), 等待事件()。 -include_lib("stdlib/include/qlc.hrl")。 等待事件()-> 收到 {From,{key,write,Key,TimeStamp,InstanceId}} -> %% 一个进程即将调用 %% mnesia:write/1 所以我们记下来 乐趣=乐趣()-> case qlc:e(qlc:q([X || X ok = mnesia:write(#benchmark{ id = {From,Key}, write_instances = [ #write_instance{ instance_id = InstanceId, before_write_time = 时间戳 }] }), 好的; [这里]-> WIs = Here#benchmark.write_instances, 新实例 = #write_instance{ instance_id = InstanceId, before_write_time = 时间戳 }, ok = mnesia:write(这里#benchmark{write_instances = [NewInstance|WIs]}), 好的 结尾 结尾, 记忆:交易(有趣), 等待事件(); {mnesia_table_event,{write,#key_value{key = Key,instanceId = I,pid = From},_ActivityId}} -> %% 进程已成功写入。所以我们查了一下 %% 获取timeStamp差异,并完成写的benchmark WriteTimeStamp = 时间戳(), F = 乐趣()-> [这里] = mnesia:read({benchmark,{From,Key}}), WIs = Here#benchmark.write_instances, {_,WriteInstance} = 列表: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(这里#benchmark{write_instances = [NewWI|lists:keydelete(I,2,WIs)]}), 好的 结尾, 记忆:交易(F), 等待事件(); {From,{key,read,Key,TimeStamp,InstanceId}} -> %% 一个进程即将进行读取 %% 使用 mnesia:read/1 所以我们记下来 乐趣 = 乐趣()-> case qlc:e(qlc:q([X || X ok = mnesia:write(#benchmark{ id = {From,Key}, 读取实例 = [ #read_instance{ instance_id = InstanceId, before_read_time = 时间戳 }] }), 好的; [这里]-> RIs = Here#benchmark.read_instances, 新实例 = #read_instance{ instance_id = InstanceId, before_read_time = 时间戳 }, ok = mnesia:write(这里#benchmark{read_instances = [NewInstance|RIs]}), 好的 结尾 结尾, 记忆:交易(有趣), 等待事件(); {mnesia_system_event,{mnesia_user,{read_success,#key_value{key = Key},From,I}}} -> %% 进程已成功读取。所以我们查了一下 %% 获取时间戳差异,并完成读取的基准标记 ReadTimeStamp = 时间戳(), F = 乐趣()-> [这里] = mnesia:read({benchmark,{From,Key}}), RIs = Here#benchmark.read_instances, {_,ReadInstance} = 列表: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(这里#benchmark{read_instances = [NewRI|lists:keydelete(I,2,RIs)]}), 好的 结尾, 记忆:交易(F), 等待事件(); _ -> wait_events(); 结尾。 time_diff({A2,B2,C2} = _After,{A1,B1,C1} = _Before)-> {A2 - A1,B2 - B1,C2 - C1}。好吧!那是巨大的:) 所以我们完成了订阅者。我们需要将所有代码放在一起并运行必要的测试。
安装()-> 失忆症:停止()。 mnesia:delete_schema([node()]), mnesia:create_schema([node()]), 失忆症:开始(), {atomic,ok} = mnesia:create_table(key_value,[ {属性,记录信息(字段,键值)}, {disc_copies,[node()]}现在,通过对基准表记录的适当分析,您将获得平均读取时间的记录,
平均写入时间等您可以根据进程数量的增加绘制这些时间的图表。
随着我们进程数量的增加,你会发现读写次数增加了
.获取代码,阅读并使用它。你可能不会全部使用,但我相信你可以拿起
当其他人在那里发送解决方案时,来自那里的新概念。使用 mnesia 事件是测试 mnesia 读取和写入而不阻塞执行实际写入或读取的进程的最佳方法。在上面的例子中,读写过程是不受任何控制的,事实上,它们将永远运行,直到你终止虚拟机。您可以使用良好的公式遍历基准表,以利用每个读取或写入实例的读取和写入时间,然后您将计算平均值、变化等。
因此,mnesia 背后的概念只能与爱立信的 NDB 数据库进行比较:http://ww.dolphinics.no/papers/abstract/ericsson.html,但不能与现有的 RDBMS 或 Document Oriented数据库等这些是我的想法:) 让我们等待其他人要说的话.....
【讨论】:
您可以使用如下命令启动其他节点:
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' ]
)
【讨论】: