用于映射大数据的Python共享内存字典

时间:2018-03-22 21:41:35

标签: python dictionary bigdata python-multiprocessing

我一直很难使用大字典(~86GB,17.5亿个键)来处理Python中的多处理大数据集(2TB)。

上下文:将字符串映射到字符串的字典从pickled文件加载到内存中。加载后,创建工作进程(理想情况下> 32),必须在字典中查找值,但修改其内容,以便处理~2TB数据集。数据集需要并行处理,否则任务将需要一个月。

以下是两个 三个 四个 五个 六个 七个 我尝试过的八种 九种方法(都失败了):

  1. 将字典存储为Python程序中的全局变量,然后派生~32个工作进程。从理论上讲,这个方法可能有效,因为字典被修改,因此Linux上fork的COW机制意味着数据结构将被共享而不会被复制到进程之间。但是,当我尝试此操作时,我的程序会在os.fork() multiprocessing.Pool.map内从OSError: [Errno 12] Cannot allocate memory崩溃。我确信这是因为内核配置为永远不会过度使用内存(/proc/sys/vm/overcommit_memory设置为2,我无法在机器上配置此设置,因为我没有root访问权限)。

  2. 将字典加载到multiprocessing.Manager.dict的共享内存字典中。通过这种方法,我能够在不崩溃的情况下分叉32个工作进程,但后续数据处理比不需要字典的任务的另一个版本慢了几个数量级(唯一的区别是没有字典查找)。我认为这是因为包含字典的管理器进程与每个工作进程之间的进程间通信,这是每次单个字典查找所必需的。虽然字典没有被修改,但它被访问了很多次,通常是由许多进程同时访问。

  3. 将字典复制到C ++ std::map并依赖Linux的COW机制来防止它被复制(如#C中的字典除外#1)。使用这种方法,将字典加载到std::map并随后在ENOMEM os.fork()上崩溃需要花费很长时间。就像之前一样。

  4. 将字典复制到pyshmht。将字典复制到pyshmht

  5. 需要太长时间
  6. 尝试使用SNAP的HashTable。 C ++中的底层实现允许在共享内存中制作和使用它。不幸的是,Python API不提供此功能。

  7. 使用PyPy。崩溃仍然发生在#1。

  8. multiprocessing.Array之上的python中实现我自己的共享内存哈希表。这种方法仍导致#1中出现内存不足错误。

  9. 将字典转储到dbm。在尝试将字典转储到dbm数据库中四天并看到“33天”的ETA之后,我放弃了这种方法。

  10. 将字典转储到Redis中。当我尝试使用redis.mset将字典(86GB dict从1024个较小的dicts中加载)转储到Redis时,我通过对等错误重置连接。当我尝试使用循环转储键值对时,需要很长时间。

  11. 如何有效地并行处理此数据集,而无需进行进程间通信,以便在此字典中查找值。我欢迎任何解决这个问题的建议!

    我在Ubuntu上使用Anaconda的Python 3.6.3在1TB RAM的机器上。

    修改:最终有效:

    我能够使用Redis来实现这一点。为了解决#9中发布的问题,我不得不将大的键值插入和查询查询分成“一口大小”的块,这样它仍然可以批量处理,但是没有超出查询的超时时间。这样做允许插入86GB字典在45分钟内执行(128个线程和一些负载平衡),并且后续处理不会受到Redis查询查询(在2天内完成)的性能影响。

    感谢大家的帮助和建议。

10 个答案:

答案 0 :(得分:7)

您应该使用一个系统,该系统用于与许多不同的进程共享大量数据 - 例如数据库。

获取您的巨型数据集并为其创建架构并将其转储到数据库中。你甚至可以将它放在一台单独的机器上。

然后,根据需要在所需数量的主机上启动任意数量的进程,以并行处理数据。几乎任何现代数据库都能够处理负载。

答案 1 :(得分:3)

正如这里的大多数人已经提到过:
不要使用那么大的字典,而是将其转储到数据库上!!!

将数据转储到数据库后,使用索引将有助于减少数据检索时间。
PostgreSQL数据库here的良好索引说明 You can optimize your database even further(我给出了一个PostgreSQL示例,因为这是我最常用的,但这些概念几乎适用于每个数据库)

假设您已经完成了上述操作(或者如果您想以任何方式使用字典...),您可以使用Python的asyncio实现并行和异步处理例程(需要Python版本> = 3.4 )。

基本思想是创建一个映射方法,将异步任务分配(映射)到iterable的每个项目,并将每个任务注册到asyncio的event_loop

最后,我们将使用asyncio.gather收集所有这些承诺,我们将等待收到所有结果。

这个想法的骨架代码示例:

import asyncio

async def my_processing(value):
    do stuff with the value...
    return processed_value

def my_async_map(my_coroutine, my_iterable):
    my_loop = asyncio.get_event_loop()
    my_future = asyncio.gather(
        *(my_coroutine(val) for val in my_iterable)
    )
    return my_loop.run_until_complete(my_future)

my_async_map(my_processing, my_ginormous_iterable)

<小时/> 您可以使用gevent代替asyncio,但请记住,asyncio是标准库的一部分。

Gevent实施:

import gevent
from gevent.pool import Group

def my_processing(value):
    do stuff with the value...
    return processed_value

def my_async_map(my_coroutine, my_iterable):
    my_group = Group()
    return my_group.map(my_coroutine, my_iterable)

my_async_map(my_processing, my_ginormous_iterable)

答案 2 :(得分:2)

如果您可以在第1点成功将数据加载到单个进程中,那么您很可能通过使用https://bugs.python.org/issue31558中引入的gc.freeze来解决fork执行副本的问题

你必须使用python 3.7+并在fork之前调用该函数。 (或在您对进程池执行映射之前)

由于这需要整个内存的虚拟副本才能使CoW正常工作,因此您需要确保overcommit settings允许您这样做。

答案 3 :(得分:2)

也许您应该尝试在数据库中执行此操作,并且可能尝试使用Dask来解决您的问题,让Dask关注如何在低级别进行多处理。您可以专注于要使用大数据解决的主要问题。 这可能是您想要查看的链接Dask

答案 4 :(得分:2)

使用压缩数据但仍具有快速查找的数据结构,而不是使用字典。

例如:

  • keyvi:https://github.com/cliqz-oss/keyvi keyvi是一种基于FSA的键值数据结构,针对空间和空间进行了优化。查找速度。从keyvi读取的多个进程将重用内存,因为keyvi结构是内存映射的,它使用共享内存。由于您的工作流程不需要修改数据结构,我认为这是您最好的选择。

  • marisa trie:https://github.com/pytries/marisa-trie Python的静态trie结构,基于marisa-trie C ++库。与keyvi一样,marisa-trie也使用内存映射。使用相同trie的多个进程将使用相同的内存。

编辑:

要将keyvi用于此任务,您可以先使用pip install pykeyvi进行安装。然后像这样使用它:

from pykeyvi import StringDictionaryCompiler, Dictionary

# Create the dictionary
compiler = StringDictionaryCompiler()
compiler.Add('foo', 'bar')
compiler.Add('key', 'value')
compiler.Compile()
compiler.WriteToFile('test.keyvi')

# Use the dictionary
dct = Dictionary('test.keyvi')
dct['foo'].GetValue()
> 'bar'
dct['key'].GetValue()
> 'value'
marisa trie只是一个特里,所以它不能作为一个开箱即用的映射,但你可以为我们提供一个分隔符char来将键与值分开。

答案 5 :(得分:1)

我相信Redis或数据库是最简单,最快捷的解决方案。

但根据我的理解,为什么不从第二个解决方案中减少问题呢?也就是说,首先尝试将十亿个密钥的一部分加载到内存中(比如50万个)。然后使用多处理,创建一个池来处理2 TB文件。如果表中存在行的查找,则将数据推送到已处理行的列表。如果它不存在,请将其推送到列表中。完成数据集的读取后,选择列表并刷新存储的密钥。然后加载下一个百万并重复该过程,而不是从列表中读取。完成后,阅读所有的pickle对象。

这应该可以解决您遇到的速度问题。当然,我对您的数据集知之甚少,并且不知道这是否可行。当然,您可能会留下没有获得正确字典键读取的行,但此时您的数据大小将会大大减少。

不知道这是否有任何帮助。

答案 6 :(得分:1)

已经提到的keyvi(http://keyvi.org)听起来对我来说是最好的选择,因为&#34; python共享内存字典&#34;确切地描述了它是什么。我是keyvi的作者,称我有偏见,但给我机会解释:

共享内存使其可扩展,特别是对于GIL问题迫使您使用多处理而不是线程的python。这就是为什么基于堆的进程内解决方案无法扩展的原因。共享内存也可以比主内存大,部分可以交换进出。

基于外部流程网络的解决方案需要额外的网络跃点,您可以通过使用keyvi避免这种情况,即使在本地计算机上也会产生很大的性能差异。问题还在于外部流程是否是单线程的,因此再次引入了瓶颈。

我想知道你的字典大小:86GB:keyvi很有可能很好地压缩它,但很难说不知道数据。

关于处理:请注意,keyvi在pySpark / Hadoop中运行良好。

您的用例BTW正是keyvi在生产中的用途,即使是在更高的规模上。

redis解决方案听起来不错,至少比某些数据库解决方案更好。为了使内核饱和,您应该使用多个实例并使用一致的散列来划分密钥空间。但是,我确信,使用keyvi可以更好地扩展。如果你必须重复任务和/或需要处理更多数据,你应该尝试一下。

最后但同样重要的是,您可以在网站上找到更好的资料,并详细解释上述内容。

答案 7 :(得分:0)

另一个解决方案可能是使用一些现有的数据库驱动程序,它可以根据需要分配/淘汰页面并快速处理索引查找。

dbm提供了一个很好的字典界面,并且页面的自动缓存可能足以满足您的需求。如果没有修改任何内容,您应该能够有效地将整个文件缓存在VFS级别。

请记住禁用锁定,在非同步模式下打开,仅打开'r',这样就不会影响缓存/并发访问。

答案 8 :(得分:0)

由于您只想创建一个只读字典,因此您可以通过滚动自己的简单版本来获得比现有数据库更快的速度。也许你可以尝试类似的东西:

import os.path
import functools
db_dir = '/path/to/my/dbdir'

def write(key, value):
    path = os.path.join(db_dir, key)
    with open(path, 'w') as f:
        f.write(value)

@functools.lru_cache(maxsize=None)
def read(key):
    path = os.path.join(db_dir, key)
    with open(path) as f:
        return f.read()

这将创建一个包含文本文件的文件夹。每个文件的名称是字典键,内容是值。自己计时我每次写入大约300us(使用本地SSD)。从理论上讲,使用这些数字写入17.5亿个密钥的时间大约是一周,但这很容易并行化,因此可能能够更快地完成它。

对于阅读,我使用热缓存和5ms冷缓存每次读取大约150us(我的意思是这里的OS文件缓存)。如果您的访问模式是重复的,您可以使用lru_cache记录您的读取函数,如上所述。

您可能会发现在文件系统中无法将这么多文件存储在一个目录中,或者操作系统效率低下。在这种情况下,您可以像.git / objects文件夹一样:将密钥abcd存储在名为ab / cd的文件中(即在文件夹ab中的文件cd中)。

基于4KB的块大小,上面的内容将占用15TB的磁盘。您可以通过尝试按前n个字母将密钥组合在一起,使磁盘和操作系统缓存更高效,以便每个文件更接近4KB块大小。这样做的方法是你有一个名为abc的文件,它存储所有以abc开头的键的键值对。如果您首先将每个较小的字典输出到已排序的键/值文件中,然后在将它们写入数据库时​​进行合并,以便您一次编写一个文件(而不是重复打开和追加),则可以更有效地创建它

答案 9 :(得分:0)

虽然 “的大多数建议在这里使用数据库” 是明智且经过验证的,但听起来您可能会因为某种原因而避免使用数据库(而且您发现数据库中的负载是禁止的,所以基本上它似乎是IO绑定的,和/或处理器绑定的。您提到您正在从1024个较小的索引加载86GB索引。如果您的密钥相当规则且均匀分布,您是否可以返回1024个较小的索引并对字典进行分区?换句话说,例如,如果您的键长度均为20个字符,并且由字母a-z组成,则创建26个较小的字典,一个用于以“a”开头的所有键,一个用于以“b”开头的键,依此类推。您可以将此概念扩展到大量专用于前2个字符或更多字符的较小字典。因此,例如,您可以为以'aa'开头的键加载一个字典,为从'ab'开始的键加载一个字典,依此类推,因此您将拥有676个单独的字典。使用17,576个较小的字典,相同的逻辑适用于前3个字符的分区。基本上我想我在这里说的是“首先不加载你的86GB字典”。而是使用自然分配数据和/或加载的策略。