键:值存储在 Python 中,可能有 100 GB 的数据,没有 client/server

Key: value store in Python for possibly 100 GB of data, without client/server

序列化小型字典有很多解决方案:json.loads/json.dumpspickleshelveujson,甚至可以使用sqlite.

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

redis 并不是一个真正的选项,因为它使用了 client/server 方案。

问题:哪个 key:value 存储,无服务器,能够处理 100+ GB 的数据,经常用于 Python?

我正在寻找具有标准 "Pythonic" d[key] = value 语法的解决方案:

import mydb
d = mydb.mydb('myfile.db')
d['hello'] = 17          # able to use string or int or float as key
d[183] = [12, 14, 24]    # able to store lists as values (will probably internally jsonify it?)
d.flush()                # easy to flush on disk 

注意:BsdDB (BerkeleyDB) seems to be deprecated. There seems to be a LevelDB for Python, but it doesn't seem well-known - and I haven't found 一个可以在 Windows 上使用的版本。哪些是最常见的?


相关问题:,

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

  • 可用于多种编程语言。
  • 可通过优秀的 h5py 软件包从 Python 开始使用。
  • 实战测试,包括大数据集。
  • 支持可变长度字符串值。
  • 值可以通过类似文件系统的方式寻址 "path" (/foo/bar)。
  • 值可以是数组(通常是),但不一定是。
  • 可选的内置压缩。
  • 可选 "chunking" 以允许增量写入块。
  • 不需要一次将整个数据集加载到内存中。

它也有一些缺点:

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

您可以将 HDF5 视为一种存储值(标量或 N 维 数组)在单个文件(或实际上是多个此类文件)的层次结构中。仅将您的值存储在单个磁盘文件中的最大问题是您会淹没某些文件系统;您可以将 HDF5 视为文件中的文件系统,当您将一百万个值放入一个文件中时,它不会崩溃 "directory."

首先,bsddb(或其新名称 Oracle BerkeleyDB)并未被弃用。

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

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

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

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 $!

我生成了下图:

$ du -h wt
60M wt

当预写日志处于活动状态时:

$ du -h wt
260M    wt

这没有性能调整和压缩。

Wiredtiger 直到最近才知道限制,文档更新如下:

WiredTiger supports petabyte tables, records up to 4GB, and record numbers up to 64-bits.

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

您可以使用 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 $!。在我的例子中,它产生了这个图表:

和数据库文件:

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

标准库中的 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()

但是使用shelve有两个注意事项:

  • 它使用pickle进行序列化。这意味着数据与 Python 以及可能用于保存数据的 python 版本耦合。如果这是一个问题,可以直接使用 dbm 模块(相同的接口,但只能使用字符串作为 keys/values)。
  • Windows 实现似乎性能不佳

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

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

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

https://github.com/dagnelies/pysos

它像普通的 python dict 一样工作,但它的优点是它比 windows 上的 shelve 更有效,而且它也是跨平台的,不像shelve 其中数据存储因 OS 而异。

安装:

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 的数据。

编辑:工作原理

它会在您每次设置值时写入,但只会写入 key/value 对。因此 adding/updating/deleting 一个项目的成本总是相同的,尽管只添加是“更好的”,因为很多 updating/deleting 会导致文件中的数据碎片(浪费的垃圾字节)。保存在内存中的是映射(键 -> 文件中的位置),因此您只需确保有足够的 RAM 来存储所有这些键。也强烈推荐 SSD。 100 MB 既简单又快速。最初发布的 100 GB 会很多,但可行。即使是原始 reading/writing 100 GB 也需要相当长的时间。

LMDB (Lightning Memory-Mapped Database) is a very fast key-value store which has 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"

一些基准。批量插入(每个 1000 个项目)用于 lmdbm 和 sqlitedict。对于这些 non-batched 插入,写入性能会受到很大影响,因为默认情况下每个插入都会打开一个新事务。 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

另一个值得研究的解决方案是 DiskCache's Index (API docs). It's atomic, thread and process-safe and it has transactions (see features comparison here)。