跳到内容

内存存储后端

ArcticDB 可以使用基于文件的 LMDB 后端或基于 RAM 的内存存储,作为 S3 和 Azure 等对象存储的替代方案。

对于临时内存解决方案,LMDB 可以设置为写入 tmpfs。正如本指南将探讨的,使用此解决方案,多个写入器可以并发访问数据库,并且与写入永久性磁盘文件系统的 LMDB 相比,性能也会提高。

在 Linux 上,设置 tmpfs 文件系统的步骤如下

$ mkdir ./tmpfs_mount_point
$ sudo mount -t tmpfs -o size=1g tmpfs tmpfs_mount_point

我们可以通过以下命令检查它是否存在

$ df -h
Filesystem               Size  Used Avail Use% Mounted on
tmpfs                    1.0G     0  1.0G   0% /somedir/tmpfs_mount_point

在 ArcticDB 中,您可以像往常一样使用 LMDB 连接到此文件系统

import arcticdb as adb
import pandas as pd

ac_lmdb = adb.Arctic('lmdb:///somedir/tmpfs_mount_point')
lib = ac_lmdb.get_library('lib', create_if_missing=True)
lib.write('symbol', pd.DataFrame({'a': [1, 2, 3]})
print(lib.read('symbol').data)

结果是

   a
0  1
1  2
2  3

内存存储的等效实例化使用 URI 'mem://',如下所示

ac_mem = adb.Arctic('mem://')

ac_mem 实例拥有存储,存储仅在其 ac_mem Python 对象的生命周期内存在。在幕后,使用 Folly Concurrent Hash Map 作为键/值存储。对于测试用例和实验,内存后端是一个不错的选择。

如果多个进程想要并发访问相同的数据,我们建议使用基于 tmpfs 的 LMDB。

如何处理并发写入器?

再次需要注意的是,当配置了对象存储后端(例如 S3)时,ArcticDB 才能达到最高的性能和可伸缩性。尽管如此,应用程序可能希望并发写入 LMDB 存储。内存存储不适合此用例。

以下 Python 代码使用 multiprocessing 启动 50 个进程,这些进程并发写入不同的符号。(不支持针对同一符号的非分阶段并行写入)。

# Code tested on Linux
import arcticdb as adb
import pandas as pd
from multiprocessing import Process, Queue
import numpy as np

lmdb_dir = 'data.lmdb'
num_processes = 50
data_KB = 1000
ncols = 10

nrows = int(data_KB * 1e3 / ncols / np.dtype(float).itemsize)

ac = adb.Arctic(f'lmdb://{lmdb_dir}')
ac.create_library('lib')
lib = ac['lib']

timings = Queue()
def connect_and_write_symbol(symbol, lmdb_dir, timings_):
    ac = adb.Arctic(f'lmdb://{lmdb_dir}')
    lib = ac['lib']
    start = pd.to_datetime('now')
    lib.write(
        symbol,
        pd.DataFrame(
            np.random.randn(nrows, ncols),
            columns=[f'c{i}' for i in range(ncols)]
        )
    )
    timings_.put((start, pd.to_datetime('now')))

symbol_names = {f'symbol_{i}' for i in range(num_processes)}
concurrent_writers = {
    Process(target=connect_and_write_symbol, args=(symbol, lmdb_dir, timings))
    for symbol in symbol_names
}

# Start all processes
for proc in concurrent_writers:
    proc.start()
# Wait for them to complete
for proc in concurrent_writers:
    proc.join()

assert set(lib.list_symbols()) == symbol_names

timings_list = []
while not timings.empty():
    timings_list.append(timings.get())

pd.DataFrame(timings_list, columns=['start', 'end']).to_csv('timings.csv')

使用 matplotlib 绘制每个进程的生命周期,我们得到

图解:每个线段代表一个进程向共享 LMDB 后端写入的执行。LMDB 在 lib.write(..) 调用过程中反复获取和释放文件锁。

LMDB 文档主页指出多线程并发也是可能的。然而,正如本页所述,我们不应该在同一个进程中多次调用 mdb_env_open()。因此,由于此函数在 Arctic 实例化中调用,上述代码无法转移到多线程应用程序中。

tmpfs 上的 LMDB 比磁盘上的 LMDB 快吗?

请参阅下面的计时结果和附录,以获取生成此数据的代码。差异并不大,tmpfs 仅在符号写入操作中优于磁盘。尽管如此,tmpfs 显然是临时 LMDB 后端更好的选择。我们还可以看到,正如预期的那样,内存存储在整体上要快得多。

注意:范围是基于每个数据大小重复五次的 95% 置信区间。硬件限制是使用 np.savenp.load 函数写入磁盘(而非 tmpfs)的限制。未寻求 RAM 的硬件限制数据。

附录:性能分析脚本

用于生成上述图表的性能分析脚本,以及使用 fio 确定理论最大读写速度的讨论已移至此 Wiki 页面