Erlang mnesia 数据库访问

Posted

技术标签:

【中文标题】Erlang mnesia 数据库访问【英文标题】:Erlang mnesia database access 【发布时间】:2012-01-10 15:32:01 【问题描述】:

我设计了一个包含 5 个不同表的 mnesia 数据库。这个想法是模拟来自许多节点(计算机)的查询,而不仅仅是一个节点,此刻我可以从终端执行查询,但我只需要帮助我如何做到这一点,以便我从多台计算机请求信息。我正在测试可伸缩性,并想研究 mnesia 与其他数据库的性能。任何想法都将受到高度赞赏。

【问题讨论】:

【参考方案1】:

测试 mnesia 的最佳方法是在运行 mnesia 的本地 Erlang 节点和远程节点上使用密集线程作业。通常,您希望远程节点使用<b>RPC calls</b>,其中正在对 mnesia 表执行读取和写入。当然,高并发需要权衡;事务的速度会降低,很多可能会被重试,因为在给定的时间锁可能很多;但是 mnesia 将确保所有进程在他们进行的每个事务调用中都收到一个atomic,ok

概念 我建议我们有一个非阻塞重载,通过尽可能多的进程将写入和读取定向到每个 mnesia 表。我们测量了调用write 函数与我们庞大的mnesia 订阅者获得Write 事件所需的时间之间的时间差。这些事件在每次成功的事务后由 mnesia 发送,因此我们不需要中断工作/重载过程,而是让“强大的”mnesia 订阅者等待异步事件报告成功的删除和写入一旦发生。 这里的技术是我们在调用写入函数之前获取时间戳,然后记下record keywrite <b>CALL</b> timestamp。然后我们的 mnesia 订阅者会记下record keywrite/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 表与其大小相关的速度。让我们举一个简单的记忆表的例子

-record(key_value,key,value,instanceId,pid)。

我们希望有一个通用函数来写入我们的表格,如下所示:

写(记录)-> %% 使用 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)-&gt; receive after T -&gt; 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 中成功写入 %% 对于所有键,我们只写一个零作为它的值 键,实例 = guid(), write(#key_valuekey = Key,value = 0,instanceId = Instance,pid = self()), 生成和写入()。

同样,让我们​​看看如何完成读取作业。 我们将有一个 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_valuekey = 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(这里#benchmarkwrite_instances = [NewInstance|WIs]), 好的 结尾 结尾, 记忆:交易(有趣), 等待事件(); mnesia_table_event,write,#key_valuekey = 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(这里#benchmarkwrite_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(这里#benchmarkread_instances = [NewInstance|RIs]), 好的 结尾 结尾, 记忆:交易(有趣), 等待事件(); mnesia_system_event,mnesia_user,read_success,#key_valuekey = 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(这里#benchmarkread_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()] ]), atomic,ok = mnesia:create_table(benchmark,[ 属性,记录信息(字段,基准), disc_copies,[节点()] ]), 失忆症:停止(), 好的。 开始()-> 失忆症:开始(), ok = mnesia:wait_for_tables([key_value,benchmark],timer:seconds(120)), %% 启动我们的订阅者 注册(mnesia_subscriber,spawn(?MODULE,subscriber,[])), start_write_jobs(), start_key_picker(), start_read_jobs(), 好的。

现在,通过对基准表记录的适当分析,您将获得平均读取时间的记录, 平均写入时间等您可以根据进程数量的增加绘制这些时间的图表。 随着我们进程数量的增加,你会发现读写次数增加了 .获取代码,阅读并使用它。你可能不会全部使用,但我相信你可以拿起 当其他人在那里发送解决方案时,来自那里的新概念。使用 mnesia 事件是测试 mnesia 读取和写入而不阻塞执行实际写入或读取的进程的最佳方法。在上面的例子中,读写过程是不受任何控制的,事实上,它们将永远运行,直到你终止虚拟机。您可以使用良好的公式遍历基准表,以利用每个读取或写入实例的读取和写入时间,然后您将计算平均值、变化等。



从远程计算机进行测试、模拟节点、针对其他 DBMS 进行基准测试可能仅仅因为许多原因而没有那么相关。 Mnesia 的概念、动机和目标与现有的几种数据库类型非常不同,例如:面向文档的 DB、RDBMS、面向对象的 DB 等。事实上,mnesia 可以与诸如 this one 之类的数据库进行比较。它是一个具有混合/非结构化数据结构的分布式 DBM,属于 Erlang 语言。用另一种类型的数据库对 Mnesia 进行基准测试可能是不正确的,因为它的目的与许多不同,而且它与 Erlang/OTP 紧密耦合。但是,了解 mnesia 的工作原理、事务上下文、索引、并发性、分布可能是良好数据库设计的关键。 Mnesia 可以存储非常复杂的数据结构。请记住,包含嵌套信息的数据结构越复杂,解压缩它并在运行时提取所需信息所需的工作就越多,这意味着更多的 CPU 周期和内存。有时,使用 mnesia 进行规范化可能只会导致性能不佳,因此其概念的实现与其他数据库相去甚远。 您对跨多台机器(分布式)的 Mnesia 性能感兴趣,这很好,但是,性能与 Distributed Erlang 一样好。最棒的是每笔交易都确保了原子性。来自远程节点的并发请求仍然可以通过 RPC 调用发送。请记住,如果您在不同机器上有多个 mnesia 副本,则在每个节点上运行的进程将在该节点上写入,然后 mnesia 将从那里继续复制。 Mnesia 的复制速度非常快,除非网络真的很糟糕和/或节点没有连接,或者网络在运行时被分区。 Mnesia 确保 CRUD 操作的一致性和原子性。因此,复制的 mnesia 数据库高度依赖网络可用性以获得更好的性能。只要 Erlang 节点保持连接,两个或多个 Mnesia 节点将始终具有相同的数据。在一个节点上读取将确保您获得最新信息。当发生断开连接并且每个节点注册其他节点时,就会出现问题,就好像它关闭一样。有关 mnesia 性能的更多信息,请访问以下链接http://igorrs.blogspot.com/2010/05/mnesia-one-year-later.htmlhttp://igorrs.blogspot.com/2010/05/mnesia-one-year-later-part-2.htmlhttp://igorrs.blogspot.com/2010/05/mnesia-one-year-later-part-3.html@ 987654342@

因此,mnesia 背后的概念只能与爱立信的 NDB 数据库进行比较:http://ww.dolphinics.no/papers/abstract/ericsson.html,但不能与现有的 RDBMS 或 Document Oriented数据库等这些是我的想法:) 让我们等待其他人要说的话.....

【讨论】:

非常感谢,你真的给了我几乎所有我需要的信息。我真的很感激,我会看看你提供的代码和所有链接。非常感谢【参考方案2】:

您可以使用如下命令启动其他节点:

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/2spawn/4 在所有节点上开始负载生成,例如:

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

【讨论】:

以上是关于Erlang mnesia 数据库访问的主要内容,如果未能解决你的问题,请参考以下文章

一个erlang数据库模式生成器

与 Mnesia 保持关系完整性

Erlang:为 mnesia 指定工作目录?

Erlang:Mnesia:连续更新单个字段值

Erlang-如何在没有记录的情况下使用 Mnesia

erlang r19里面的mnesia_ext