使用Peewee的多写单读SQLite应用程序



我在多台机器上使用带有peewee的SQLite数据库,并且遇到了各种OperationalErrorDataBaseError。这显然是一个多线程的问题,但我不是这方面的专家,也不是SQL方面的专家。这是我的设置和我尝试过的。

设置

我正在使用peewee来记录机器学习实验。基本上,我有多个节点(比如不同的计算机)运行一个python文件,所有都将写入共享位置的相同base.db文件。最重要的是,我需要从我的笔记本电脑上进行一次读取访问,看看发生了什么。最多有大约50个不同的节点来实例化数据库并在上面写东西。

我尝试过的

起初,我使用了SQLite对象:

db = pw.SqliteDatabase(None)
# ... Define tables Experiment and Epoch
def init_db(file_name: str):
db.init(file_name)
db.create_tables([Experiment, Epoch], safe=True)
db.close()
def train():
xp = Experiment.create(...)
# Do stuff
with db.atomic():  
Epoch.bulk_create(...)
xp.save()

这很好,但有时我的工作会因为数据库被锁定而崩溃。然后,我了解到SQLite每个连接只处理一个写操作,这导致了问题。

所以我转向SqliteQueueDatabase,因为根据文档,如果";如果您希望从多个线程对SQLite数据库进行简单的读写访问"我还添加了我在其他帖子中发现的那些被认为有用的关键词。

代码看起来是这样的:

db = SqliteQueueDatabase(None, autostart=False, pragmas=[('journal_mode', 'wal')],
use_gevent=False,)
def init_db(file_name: str):
db.init(file_name)
db.start()
db.create_tables([Experiment, Epoch], safe=True)
db.connect()

并且对于除了CCD_ 7部分之外的节省内容也是相同的。然而,写查询似乎不仅会遇到错误,而且我实际上不再能够访问数据库进行读取:它几乎总是很忙。

我的问题

在这种情况下,什么是正确的对象?我认为SqliteQueueDatabase非常适合。pooled数据库更适合吗?我之所以问这个问题,是因为我不知道我是否很好地掌握了线程部分:多个database对象是从多台机器初始化的,这与在一台有多个线程的机器上有一个对象不同(就像这种情况)。正确的那有什么好办法处理事情吗?

很抱歉,如果这个问题已经在其他地方得到了回答,谢谢你的帮助!当然,如果需要,很乐意提供更多的代码。

Sqlite一次只支持一个写入程序,但在使用WAL模式时,多个读卡器可以打开数据库(即使连接了写入程序)。对于peewee,您可以启用wal模式:

db = SqliteDatabase('/path/to/db', pragmas={'journal_mode': 'wal'})

当使用多个写入程序时,另一件至关重要的事情是使写入事务尽可能短。以下是一些建议:https://charlesleifer.com/blog/going-fast-with-sqlite-and-python/在";事务、并发和自动提交";标题

另请注意,SqliteQueueDatabase适用于带有多个线程的单个进程,但如果您有多个进程,则对您毫无帮助。

Inded,在@BoarGules评论后,我意识到我混淆了两件截然不同的事情:

  • 在一台机器上拥有多个线程:这里,SqliteQueueDatabase非常适合
  • 拥有多台机器,有一个或多个线程:这就是互联网的基本工作方式

所以我最终安装了Postgre。一些链接,如果它对我之后的人有用的话,对于linux:

  • 安装Postgre。如果您没有根权限,您可以从源代码构建它,遵循官方文档中的第17章,然后是第19章
  • 可以使用pgloader导出SQLite数据库。但是,如果你没有合适的图书馆,也不想建造所有的东西,你可以手工完成。我做了以下操作,不确定是否存在更直接的解决方案
  1. 将表导出为csv(遵循@coleifer的注释):
models = [Experiment, Epoch]
for model in models:
outfile = '%s.csv' % model._meta.table_name
with open(outfile, 'w', newline='') as f:
writer = csv.writer(f)
row_iter = model.select().tuples().iterator()
writer.writerows(row_iter)
  1. 在新的Postgre数据库中创建表:
db = pw.PostgresqlDatabase('mydb', host='localhost')
db.create_tables([Experiment, Epoch], safe=True)
  1. 使用以下命令将CSV表复制到Postgre数据库:
COPY epoch("col1", "col2", ...) FROM '/absolute/path/to/epoch.csv'; DELIMITER ',' CSV;

对于其他表格也是如此。

这对我来说很好,因为我只有两张桌子。如果你拥有的不止这些,那可能会很烦人。在这种情况下,pgloader似乎是一个非常好的解决方案,如果您可以轻松安装的话。

更新

一开始我无法从peewee创建对象。我有完整性错误:Postgre返回的id(带有RETURNING 'epoch'.'id'子句)似乎返回了一个已经存在的id。据我所知,这是因为在使用COPY命令时没有调用增量。因此,它只返回id 1,然后返回id 2,依此类推,直到它到达一个不存在的id。为了避免经历所有这些失败的创建,您可以直接编辑控制RETURN子句的迭代器,使用:

ALTER SEQUENCE epoch_id_seq RESTART WITH 10000

并用SELECT MAX("id") FROM epoch中的值替换10000+1

我认为您可以增加sqlite的超时时间,然后解决您的问题

这里的问题是,默认的sqlite写入超时时间很低,即使有少量的并发写入,sqlite也会开始抛出异常。这是众所周知的。

默认值应该是大约5-10秒。如果超过了这个超时时间,那么要么增加它,要么将写入数据库的数据块增加。

以下是一个示例:我在这里返回一个DatabaseProxy,因为这个代理允许在不更改客户端代码的情况下将sqlite换成postgres。

import atexit
from peewee import DatabaseProxy  # type: ignore
from playhouse.db_url import connect  # type: ignore
from playhouse.sqlite_ext import SqliteExtDatabase  # type: ignore
DB_TIMEOUT = 5
def create_db(db_path: str) -> DatabaseProxy:
pragmas = (
# Negative size is per api spec.
("cache_size", -1024 * 64),
# wal speeds up writes.
("journal_mode", "wal"),
("foreign_keys", 1),
)
sqlite_db = SqliteExtDatabase(
db_path,
timeout=DB_TIMEOUT,
pragmas=pragmas)
sqlite_db.connect()
atexit.register(sqlite_db.close)
db_proxy: DatabaseProxy = DatabaseProxy()
db_proxy.initialize(sqlite_db)
return db_proxy

最新更新