Python中的键值存储,适用于可能达到100GB数据量的情况,且不需要客户端/服务器模式。

39

有许多解决方案可以序列化一个小字典: json.loads/json.dumps, pickle, shelve, ujson, 或者甚至使用 sqlite

但当处理可能达到100GB的数据时,再也无法使用这些模块了,因为关闭/序列化时可能会重写整个数据。

redis不是一个真正的选择,因为它使用了客户端/服务器方案。

问题: 哪种键值存储方式、无服务器、能够处理100GB以上数据,在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)似乎已经被弃用。 似乎有一个Python的LevelDB,但并不广为人知 - 并且我没有找到一个可以在Windows上使用的版本。 哪些是最常见的?


相关问题:使用SQLite作为key:value存储Flat file NoSQL解决方案


3
SQLite应该表现良好。你在使用它时遇到了什么问题吗? 这是一个小型的DBMS,但DB本身可以很大。请参见https://dev59.com/wWYq5IYBdhLWcg3weQgy - Himanshu
@Himanshu 事实上,使用SQLite并不像db[key] = valuedb.put('key', 'value')那样简单,而是使用SQL语句... 我想避免为了一个简单的键值对db[key] = value的设置/获取而使用INSERT INTO TABLE或SELECT...。 - Basj
你可能可以在dask中让它工作,但我从未真正使用过,这是我的待办事项。显然,它也可以在单个系统上运行。或者你总是可以使用MongoDB - 没有什么阻止你在本地主机上运行它。我不确定你对无服务器的要求来自哪里,对于单个PC上如此大的数据存储,你可能没有选择。 - roganjosh
@JohnZwinck 键始终为10个字节,值是长度在200到1000之间的字符串。例如,它应该能够处理1亿个键/值对。 - Basj
2
Python的dbm模块非常适合这个任务。我刚刚检查了一下,它不会将任何东西加载到内存中,并且具有与字典完全相同的接口。我无法发布答案,因为它已经关闭了,所以我决定在这里发布。 - Matthew D. Scholefield
显示剩余6条评论
7个回答

43
你可以使用提供对SQLite数据库键值接口的 sqlitedict
SQLite 限制页面 表示理论上的最大值取决于 page_sizemax_page_count,可以达到140TB。但是,默认值针对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

非常好的基准测试,谢谢@saaj!好奇:with closing(...) as ...:是什么意思? - Basj
关于图表中的CPU y轴,范围在0到200%之间,平均值为150%,这个y轴是否正确? - Basj
@Basj 1) contextlib.closing。2) 我认为是这样的,因为 sqlite3 创建了自己的线程,在 _sqlite3 二进制文件中操作时会释放 GIL。所以它超过了100%。 - saaj
非常棒的答案,我会相应地更新我的回答 https://dev59.com/5VYN5IYBdhLWcg3wlI3G#48298904 - amirouche
1
还有 diskcache,它是纯Python编写的,不需要服务器,速度快,也是基于SQLite构建的。已知最大的diskcache数据库为75GB。 - GrantJ

10

LMDB(Lightning Memory-Mapped Database)是一种非常快速的键值存储引擎,它具有Python绑定并且可以轻松处理大型数据库文件。

此外,还有lmdbm包装器,提供了类似于Python的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


谢谢您的回答。您能否在您的回答中包含示例代码(包括导入等),以便于将来参考和那些想要快速尝试此解决方案的人们。 - Basj
你能否也包含一些基准测试呢?感谢分享这个库! - 0x90
我在这里尝试了一个小型基准测试 https://gist.github.com/dagnelies/eae73f7341ef00068c3d27cd488f33bc 但性能看起来相当糟糕。我做错了什么吗?插入10万个小的键/值对花费了LMDB长达6分钟!?!作为基准比较,pysos只需要约4秒钟。 - dagnelies
1
@dagnelies 你可能想使用.update()进行批量插入,否则每个插入都将是一个事务。这应该可以提供至少10倍的性能。LMDB是一个具有ACID事务和并发访问的全功能数据库。它也不会将所有键存储在内存中。因此,写操作更加昂贵。您的基准测试并未涉及读取性能、内存使用、崩溃安全性或并发性。当我有时间时,我会尝试添加一些更全面的比较。 - C. Yduqoli
1
感谢您的出色回答!我正在处理一个拥有3亿个项目的一对多查找表。我尝试了sqlitedictwiredtigerpysoslmdb。我可以确认使用Lmdb.update(<dict>)是获得良好写入性能的关键。逐个插入元素,lmdb每秒能够写入约1k个项目,而对于sqlitedictpysos,分别为约3k/s和80k/s。在wiredtiger中,我遇到了一些棘手的错误,并且支持很少。最后,将100k个项目一次性插入到lmdb中,速度达到了惊人的100-500k/s。此外,与这里的其他库不同,它的内存占用较小。 - deeenes

7
我会考虑使用HDF5。 它有几个优点:
  • 可以从许多编程语言中使用。
  • 通过出色的h5py包可在Python中使用。
  • 经过大量数据集的测试。
  • 支持可变长度字符串值。
  • 值可以通过类似于文件系统的“路径”(/foo/bar)进行寻址。
  • 值可以是数组(通常是),但不必是。
  • 可选内置压缩。
  • 可选的“分块”以允许逐块写入。
  • 不需要一次将整个数据集加载到内存中。

它也有一些缺点:

  • 极其灵活,以至于很难定义一个单一的方法。
  • 复杂的格式,如果没有官方的HDF5 C库就不能使用(但有许多包装器,例如h5py)。
  • 华丽的C/C++ API(Python的API则不然)。
  • 对并发写入(或写入+读取)的支持很少。写入可能需要在粗粒度上进行锁定。

您可以将HDF5视为一种在单个文件(或多个这样的文件)内部层次结构中存储值(标量或N维数组)的方式。仅将值存储在单个磁盘文件中的最大问题是会使一些文件系统不堪重负;您可以将HDF5视为文件中的文件系统,在一个“目录”中放置一百万个值时也不会崩溃。


4
HDF5不是数据库,而是一种序列化格式。它需要将整个文件加载到内存中。 - amirouche
1
谢谢。你能否包含3或4行代码来展示如何使用它,就像在(编辑后的)问题中那样吗?即import ...然后创建数据库,然后d[key] = value,最后将其刷新到磁盘上。 - Basj
8
显然HDF5不是数据库。问题并没有要求使用数据库。HDF5不需要将任何东西全部加载到内存中--您可以在分层文件中加载切片、"超平面"、单个数组或属性等内容。它绝对不需要将任何比您想要的更多的东西加载到内存中。无论如何,OP的数据量大约为100GB,而这些天,在通用服务器甚至一些桌面计算机上都很容易找到100GB的主存储器。 - John Zwinck
1
@Basj:请查看h5py的优秀快速入门教程,链接在这里:http://docs.h5py.org/en/latest/quick.html - 它提供了你想要的代码。 - John Zwinck
这对文本数据来说是最优的吗? - SantoshGupta7

6

标准库中的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 模块(相同的接口,但只能使用字符串作为键/值)。
  • Windows 实现似乎性能不佳

因此,以下第三方选项(摘自此处)将是不错的选择:

  • semidb - 更快的跨平台 dbm 实现
  • UnQLite - 更具特色的无服务器数据库
  • 更多内容在链接中提到

1
谢谢你的回答!我添加了一个dbm示例供以后参考,希望你没问题。 - Basj

5

我知道这是一个老问题,但我很久以前写了类似于以下内容的东西:

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也需要相当长的时间。


好的,我要试一下!它是什么时候写入磁盘的?我们需要每隔一段时间执行db.flush()吗?另外,如果我们有100MB的数据,重新保存到磁盘是否意味着重写全部100MB还是只有自上次写入以来发生变化的部分? - Basj
每次设置值时,它都会写入键/值对,但只有键/值对。因此,添加/更新/删除项目的成本始终相同,尽管仅添加是“更好”的,因为大量更新/删除会导致文件中的数据碎片化(浪费垃圾字节)。在内存中保留映射(键->文件中的位置),因此您只需确保有足够的RAM来存储所有这些键。强烈建议使用SSD。100 MB很容易且快速。像最初发布的100 GB那样也是可行的,但需要一定时间进行原始读写。 - dagnelies
谢谢。您能否在答案中包含这个有用的信息,以备将来参考@dagnelies?因为这真的是重要的部分。 - Basj
pysos和https://docs.python.org/3/library/dbm.html#module-dbm.dumb有什么不同?它们似乎以相同的方式工作(仅在内存中保留映射,每次写入...)。 - C. Yduqoli
1
@C.Yduqoli:如果我没记错的话,当时pysos的性能比那个烂得要死的“愚蠢”dbm好太多了。但请记住,这是5年前的事情,这段时间内可能已经有所发展。如果您可以进行一些小型基准测试并发布结果,那将很不错。除此之外,pysos还将您的键/值对象编码为json,并且“db”文件是可读的,而dbm则处理以二进制文件编码的字节作为键/值。它们可能存在其他一些区别细节,但它们都具有核心键/值存储的相似性。 - dagnelies

3
首先,bsddb(或其新名称Oracle BerkeleyDB)并未被弃用。
从经验来看,LevelDB / RocksDB / bsddb比wiredtiger慢,这就是为什么我推荐wiredtiger的原因。
wiredtiger是mongodb的存储引擎,因此在生产中经过了充分的测试。除了我的AjguDB项目外,在Python中几乎没有使用wiredtiger;我使用wiredtiger(通过AjguDB)来存储和查询wikidata和concept,大约80GB。
以下是一个示例类,允许模仿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 $!

我生成了以下图表:

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


谢谢。您能给出一个使用wiredtiger的代码示例吗?像 import wiredtiger wt = wiredtiger.wiredtiger('myfile.db') wt['hello'] = 17 wt[183] = [12, 14, 24] wt.flush() 这样简单的API是否可用?即主要要求是:1)采用wt[key] = value语法,2)能够使用字符串、整数或浮点数作为键。3)能够将列表存储为值,4)易于在磁盘上刷新。 - Basj
这是针对Python 2还是Python 3的? - amirouche
使用WiredTiger引擎是个不错的想法。然而,Python绑定似乎维护得很差。 - C. Yduqoli
@C.Yduqoli,你所指的Python绑定是什么?它们由MongoDB维护。 - amirouche
1
@amirouche 这里是官方的链接 https://pypi.org/project/wiredtiger/ 例如,虽然wiredtiger C代码支持Windows,但没有Windows支持。 - C. Yduqoli
@C.Yduqoli 我在使用WiredTiger时遇到了一些问题,“微调”非常困难,就好像数据库正在泄漏内存一样。文档甚至建议使用与存储数据相同的大量RAM。无论如何,使用Python您将无法利用POSIX线程。我建议您使用SQLite LSM扩展,该扩展可以从sqlite3存储库构建为独立的共享库,或者使用FoundationDB(即使是不同的东西...)。 - amirouche

3

另一个值得关注的解决方案是DiskCache的IndexAPI文档)。它是原子性的、线程和进程安全的,并且具备事务功能(可以在这里查看特性比较)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接