在 Elixir/Erlang 中的(本地)Mnesia 实例上实现最佳写入性能

Posted

技术标签:

【中文标题】在 Elixir/Erlang 中的(本地)Mnesia 实例上实现最佳写入性能【英文标题】:Achieving the best write performance on a (local) Mnesia instance in Elixir/Erlang 【发布时间】:2016-05-24 21:24:07 【问题描述】:

短而甜;我正在构建一个新项目,我可以支持 ETS,但我更愿意支持 Mnesia——因为可能会派上用场的内置交易之类的东西。我关心复制和扩展到其他节点,这就是为什么我认为 Mnesia 的性能在 ETS 上有开销。

zackehh:~/GitHub/my_project$ MIX_ENV=test mix bench
Settings:
  duration:      1.0 s

## BasicBench
[19:24:15] 1/4: retrieve key hit mnesia
[19:24:23] 2/4: retrieve key hit
[19:24:30] 3/4: insert new key mnesia
[19:24:33] 4/4: insert new key

Finished in 25.24 seconds

## BasicBench
insert new key                 10000000   0.63 µs/op
retrieve key hit               10000000   0.64 µs/op
retrieve key hit mnesia        10000000   0.69 µs/op
insert new key mnesia            500000   4.70 µs/op

我运行了一些(本地)基准测试,很明显 Mnesia 在读取性能方面具有可比性,但写入性能要慢得多。我想知道是否有任何方法可以加快速度(例如关闭复制检查等)。

附加信息:

Mnesia 表:

[
     :ram_copies, [node()] ,
     :local_content, true ,
     :attributes, [:key,:value] 
]

测试

ETS 操作使用:ets.lookup/2:ets.insert/2 Mnesia 操作使用:mnesia.dirty_write/1:mnesia.dirty_read/2

我已经搜索了几个小时的文档,但没有任何东西可以作为加快速度的潜在方法 - 所以我可能正盯着性能墙看,但如果有人可以澄清/确认/建议,那就是赞赏。

【问题讨论】:

【参考方案1】:

简答:

虽然 Mnesia 使用 ETS 作为非持久数据存储(ram_copies 表)的引擎,但 Mnesia 用于检查所选表是否被复制或具有索引的开销使其在写入新记录时比 ETS 慢。

来自Documentation:

对于非持久性数据库存储,首选 Ets 表而不是 Mnesia local_content 表。与 Ets 写入相比,即使是 Mnesiadirty_write 操作也会带来固定的开销。 Mnesia 必须检查表是否被复制或有索引,这涉及到每个dirty_write 至少有一个Ets 查找。因此,Ets 的写入速度总是比 Mnesia 的写入速度快。

但是,您可以考虑 mnesia:ets/2 从 Mnesia 表上的 ETS 性能中受益:

mnesia:ets(Fun, [, Args]) -> ResultOfFun | exit(Reason)

来自Documentation:

mnesia:ets/2 在不受事务保护的原始上下文中调用 Fun。 Mnesia函数调用在Fun中执行,直接在本地ets表上执行,假设本地存储类型为ram_copies,表不复制到其他节点。不会触发订阅,也不会更新检查点,但速度非常快。如果所有操作都是只读的,则此函数也可以应用于 disc_copies 表。有关详细信息,请参阅 mnesia:activity/4 和用户指南。


编辑:我编写了一个基准测试并在我的机器上运行它以阐明mnesia:ets/1mnesia:dirty_write/1ets:insert/2 之间的粗略区别。

mnesia_ets(Limit) ->
    application:ensure_started(mnesia),
    mnesia:create_table(foo, [ram_copies, [node()]]),
    WriteFun = fun() -> 
        [mnesia:write(foo, I, I) || I <- lists:seq(1, Limit)]
    end,
    timer:tc(fun() -> mnesia:ets(fun() -> WriteFun() end) end).

ets(Limit) ->
    ets:new(bar, [named_table, public]),
    WriteFun = fun() -> 
        [ets:insert(bar, bar, I, I) || I <- lists:seq(1, Limit)]
    end,
    timer:tc(fun() -> WriteFun() end).

mnesia(Limit) ->
    application:ensure_started(mnesia),
    mnesia:create_table(baz, [ram_copies, [node()]]),
    WriteFun = fun() -> 
        [mnesia:dirty_write(baz, I, I) || I <- lists:seq(1, Limit)]
    end,
    timer:tc(fun() -> WriteFun() end).

而写入10000000记录的结果如下:

ets:insert/2 为 4303992 微秒 mnesia:ets/1 为 15911681 微秒 mnesia:dirty_write/1 为 29798736 微秒

因此我可以得出结论,由于上述原因,mnesia:ets/1mnesia:dirty_write/1 快,但仍然比 ets:insert/2 慢。

【讨论】:

我看到了关于 :mnesia.ets 的东西,你能提供一个例子吗?我不太明白它在函数中想要什么 - Mnesia 调用? @zackehh 一些 Mnesia 函数是上下文相关的,例如 mnesia:write/1。因此,如果您将其包装在 fun 中并将函数传递给 mnesia:ets/1,它将直接在本地 ets 表上执行,而不会产生此类开销。例如:mnesia:ets(fun() -&gt; mnesia:write(table_name, key_1, val_1) end). 知道了,试过了,更慢 :p 感谢您的建议,但我想打开该上下文与节省使用上下文一样昂贵。哦,好吧! 顺便说一句,我不能在mnesia:ets/2 中使用mnesia:write/1 - 只能使用mnesia:dirty_write/1 有趣的是,我复制了您的基准测试并看到了类似的结果。看来 Benchfella 是在骗我!我看到的吞吐量几乎有 > 50% 的时间被修剪掉,这对我来说已经足够了(我正在通过 GenServer,所以那里浪费的时间比现在的写入时间要多)。谢谢!【参考方案2】:

正如 Hamidreza Soleimani 所指出的,Mnesia 总是比 ETS 慢,这在大多数情况下都不是问题。只是想指出另一种可能性。我不知道它是否会对您的情况有所帮助,但它总是值得一看。

Erlang Factory 的这张幻灯片展示了一种为 mnesia 实现自定义支持的方法。与 ETS 相比,它可能(也可能不会)允许您跳过降低 Mnesia 速度的检查。

http://www.erlang-factory.com/static/upload/media/143415340626199euc2015mnesialeveldb.pdf

【讨论】:

以上是关于在 Elixir/Erlang 中的(本地)Mnesia 实例上实现最佳写入性能的主要内容,如果未能解决你的问题,请参考以下文章

Elixir/Erlang 中的命名函数是不是有等效于 __MODULE__ 的方法?

Elixir/Erlang 并发状态访问

Elixir/Erlang

E1.获取Elixir/Erlang版本信息

Elixir/Erlang:变量列表与许多其他列表合并时出错

Elixir/Erlang Random/Rand Seed 是不是需要在每个进程上调用?