密钥:Python中的值存储,可能有100 GB的数据,没有客户端/服务器

时间:2017-11-11 01:53:58

标签: python dictionary serialization key-value key-value-store

有很多解决方案可以序列化一个小字典:json.loads / json.dumpspickleshelveujson,甚至可以使用{{1 }}

但是当处理可能100 GB的数据时,再也不可能使用这样的模块,这些模块在关闭/序列化时可能会重写整个数据。

sqlite实际上不是一个选项,因为它使用客户端/服务器方案。

问题:哪些密钥:值存储,无服务器,能够使用100多GB的数据,在Python中经常使用?

我正在寻找使用标准“Pythonic”redis语法的解决方案:

d[key] = value

注意:BsdDB(BerkeleyDB)似乎已被弃用。似乎有一个LevelDB for Python,但它似乎并不为人所知 - 我haven't found是一个可以在Windows上使用的版本。哪些是最常见的?

7 个答案:

答案 0 :(得分:9)

您可以使用sqlitedict为SQLite数据库提供键值接口。

SQLite limits page表示理论最大值为140 TB,具体取决于page_sizemax_page_count。但是,Python 3.5.2-2ubuntu0~16.04.4(sqlite3 2.6.0)的默认值为page_size=1024max_page_count=1073741823。这提供了大约1100 GB的最大数据库大小,符合您的要求。

您可以使用以下软件包:

from sqlitedict import SqliteDict

mydict = SqliteDict('./my_db.sqlite', autocommit=True)
mydict['some_key'] = any_picklable_object
print(mydict['some_key'])
for key, value in mydict.items():
    print(key, value)
print(len(mydict))
mydict.close()

更新

关于内存使用情况。 SQLite不需要您的数据集适合RAM。默认情况下,它会缓存最多cache_size个页面,这几乎不是2MiB(与上面相同的Python)。这是您可以用来检查数据的脚本。跑步前:

pip install lipsum psutil matplotlib psrecord sqlitedict

sqlitedct.py

#!/usr/bin/env python3

import os
import random
from contextlib import closing

import lipsum
from sqlitedict import SqliteDict

def main():
    with closing(SqliteDict('./my_db.sqlite', autocommit=True)) as d:
        for _ in range(100000):
            v = lipsum.generate_paragraphs(2)[0:random.randint(200, 1000)]
            d[os.urandom(10)] = v

if __name__ == '__main__':
    main()

./sqlitedct.py & psrecord --plot=plot.png --interval=0.1 $!一样运行它。在我的情况下,它生成此图表: chart

和数据库文件:

$ du -h my_db.sqlite 
84M my_db.sqlite

答案 1 :(得分:4)

我会考虑HDF5。它有几个优点:

  • 可以使用许多编程语言。
  • 可通过优秀的h5py软件包从Python中获取。
  • 战斗测试,包括大数据集。
  • 支持可变长度的字符串值。
  • 值可由类似文件系统的“路径”(/foo/bar)寻址。
  • 值可以是数组(通常是数组),但不一定是。
  • 可选的内置压缩。
  • 可选“分块”以允许逐步编写块。
  • 不需要立即将整个数据集加载到内存中。

它也有一些缺点:

  • 非常灵活,以至于难以定义单一方法。
  • 复杂的格式,没有官方的HDF5 C库就不可行(但有很多封装,例如h5py)。
  • 巴洛克式C / C ++ API(Python不是这样)。
  • 对并发作家(或作家+读者)的支持很少。写入可能需要以粗粒度锁定。

您可以将HDF5视为存储值(标量或N维)的一种方式  数组)在单个文件内的层次结构内(或实际上是多个这样的文件)。将值存储在单个磁盘文件中的最大问题是,您将压倒一些文件系统;您可以将HDF5视为文件中的文件系统,当您将一百万个值放在一个“目录”中时,该文件系统不会丢失。

答案 2 :(得分:3)

标准库中的shelve模块可以做到这一点:

import shelve
with shelve.open('myfile.db') as d:
    d['hello'] = 17  # Auto serializes any Python object with pickle
    d[str(183)] = [12, 14, 24]  # Keys, however, must be strings
    d.sync()  # Explicitly write to disc (automatically performed on close)

这使用python dbm模块从磁盘保存和加载数据,而不加载整个对象。

dbm示例:

import dbm, json
with dbm.open('myfile2.db', 'c') as d:
    d['hello'] = str(17)
    d[str(183)] = json.dumps([12, 14, 24])
    d.sync()

但是,搁置时有两个注意事项:

  • 它使用pickle进行序列化。这意味着数据与Python耦合在一起,可能还有用于保存数据的python版本。如果需要解决的话,可以直接使用dbm模块(相同的接口,但是只能将字符串用作键/值)。
  • Windows实施似乎表现不佳

因此,以下第三方选项copied from here将是不错的选择:

  • semidb-更快的跨平台dbm实现
  • UnQLite-更多功能丰富的无服务器数据库
  • the link中的更多内容

答案 3 :(得分:2)

我知道这是一个老问题,但是很久以前我写了这样的东西:

https://github.com/dagnelies/pysos

它像普通的python dict一样工作,但具有的优势是,它在Windows上比shelve效率高得多,并且它也是跨平台的,这与shelve数据存储不同基于操作系统。

要安装:

pip install pysos

用法:

import pysos
db = pysos.Dict('somefile')
db['hello'] = 'persistence!'

编辑:性能

仅给出一个大致数字,这是一个迷你基准测试(在Windows笔记本电脑上):

import pysos
t = time.time()
import time
N = 100 * 1000
db = pysos.Dict("test.db")
for i in range(N):
    db["key_" + str(i)] = {"some": "object_" + str(i)}
db.close()

print('PYSOS time:', time.time() - t)
# => PYSOS time: 3.424309253692627

生成的文件约为3.5 Mb。 ...因此,大致来说,您每秒可以插入1 mb的数据。

编辑:工作原理

每次设置值时都会写入,但仅键/值对会写入。因此,添加/更新/删除项目的成本始终是相同的,尽管仅添加是“更好的”,因为大量更新/删除会导致文件中的数据碎片(浪费的垃圾字节)。内存中保留的是映射(密钥->文件中的位置),因此您只需要确保有足够的RAM来存储所有这些密钥。强烈建议使用SSD。 100 MB轻松又快速。像最初发布的100 GB会很大,但是可行。即使是原始的100 GB读/写也要花费一些时间。

答案 4 :(得分:2)

LMDB(闪电内存映射数据库)是一种非常快速的键值存储,它具有Python bindings,可以轻松处理巨大的数据库文件。

还有lmdbm包装器,它提供Pythonic d[key] = value语法。

默认情况下,它仅支持字节值,但可以轻松扩展以将序列化器(json,msgpack,pickle)用于其他类型的值。

import json
from lmdbm import Lmdb

class JsonLmdb(Lmdb):
  def _pre_key(self, value):
    return value.encode("utf-8")
  def _post_key(self, value):
    return value.decode("utf-8")
  def _pre_value(self, value):
    return json.dumps(value).encode("utf-8")
  def _post_value(self, value):
    return json.loads(value.decode("utf-8"))

with JsonLmdb.open("test.db", "c") as db:
  db["key"] = {"some": "object"}
  obj = db["key"]
  print(obj["some"])  # prints "object"

一些基准。 lmdbm和sqlitedict使用了分批插入的插件(每个插件1000个)。对于非分批插入,写入性能会受到很大影响,因为默认情况下每个插入都会打开一个新事务。 dbm指的是stdlib dbm.dumb。在Win 7,Python 3.8,SSD上进行了测试。

以秒为单位的连续写入

| items | lmdbm | pysos |sqlitedict|   dbm   |
|------:|------:|------:|---------:|--------:|
|     10| 0.0000| 0.0000|   0.01600|  0.01600|
|    100| 0.0000| 0.0000|   0.01600|  0.09300|
|   1000| 0.0320| 0.0460|   0.21900|  0.84200|
|  10000| 0.1560| 2.6210|   2.09100|  8.42400|
| 100000| 1.5130| 4.9140|  20.71700| 86.86200|
|1000000|18.1430|48.0950| 208.88600|878.16000|

几秒钟内随机读取

| items | lmdbm | pysos |sqlitedict|  dbm   |
|------:|------:|------:|---------:|-------:|
|     10| 0.0000|  0.000|    0.0000|  0.0000|
|    100| 0.0000|  0.000|    0.0630|  0.0150|
|   1000| 0.0150|  0.016|    0.4990|  0.1720|
|  10000| 0.1720|  0.250|    4.2430|  1.7470|
| 100000| 1.7470|  3.588|   49.3120| 18.4240|
|1000000|17.8150| 38.454|  516.3170|196.8730|

有关基准脚本,请参见https://github.com/Dobatymo/lmdb-python-dbm/blob/master/benchmark.py

答案 5 :(得分:1)

首先,不推荐使用bsddb(或其新名称Oracle BerkeleyDB)。

根据经验,LevelDB / RocksDB / bsddb比wiredtiger慢,这就是我推荐有线网格的原因。

wiredtiger是mongodb的存储引擎,因此它在生产中得到了很好的测试。在AjguDB项目之外的Python中很少或根本没有使用wiretiger;我使用wiredtiger(通过AjguDB)来存储和查询大约80GB的wikidata和概念。

这是一个允许模仿python2 shelve模块的示例类。基本上, 它是一个wiretiger后端字典,其中键只能是字符串:

import json

from wiredtiger import wiredtiger_open


WT_NOT_FOUND = -31803


class WTDict:
    """Create a wiredtiger backed dictionary"""

    def __init__(self, path, config='create'):
        self._cnx = wiredtiger_open(path, config)
        self._session = self._cnx.open_session()
        # define key value table
        self._session.create('table:keyvalue', 'key_format=S,value_format=S')
        self._keyvalue = self._session.open_cursor('table:keyvalue')

    def __enter__(self):
        return self

    def close(self):
        self._cnx.close()

    def __exit__(self, *args, **kwargs):
        self.close()

    def _loads(self, value):
        return json.loads(value)

    def _dumps(self, value):
        return json.dumps(value)

    def __getitem__(self, key):
        self._session.begin_transaction()
        self._keyvalue.set_key(key)
        if self._keyvalue.search() == WT_NOT_FOUND:
            raise KeyError()
        out = self._loads(self._keyvalue.get_value())
        self._session.commit_transaction()
        return out

    def __setitem__(self, key, value):
        self._session.begin_transaction()
        self._keyvalue.set_key(key)
        self._keyvalue.set_value(self._dumps(value))
        self._keyvalue.insert()
        self._session.commit_transaction()

来自@saaj的改编测试程序回答:

#!/usr/bin/env python3

import os
import random

import lipsum
from wtdict import WTDict


def main():
    with WTDict('wt') as wt:
        for _ in range(100000):
            v = lipsum.generate_paragraphs(2)[0:random.randint(200, 1000)]
            wt[os.urandom(10)] = v

if __name__ == '__main__':
    main()

使用以下命令行:

python test-wtdict.py & psrecord --plot=plot.png --interval=0.1 $!

我生成了以下图表:

wt performance without wal

$ du -h wt
60M wt

当预写日志激活时:

wt performance with wal

$ du -h wt
260M    wt

这没有性能调整和压缩。

Wiredtiger直到最近还没有已知的限制,文档已更新为以下内容:

  

WiredTiger支持PB级表,最大4GB记录,最多64位记录。

http://source.wiredtiger.com/1.6.4/architecture.html

答案 6 :(得分:0)

另一个值得一看的解决方案是DiskCache's Index (API docs)。它是原子、线程和进程安全的,并且具有事务(请参阅功能比较 here)。