SQLite 适合在 VPS 上做生产数据库吗?
了解 SQLite 在单台 VPS 上用于生产的边界,配置 WAL 和 busy_timeout,使用 Litestream 复制,并识别何时单写入者和单机限制会迫使您迁移。
何时应在 VPS 生产环境中使用 SQLite
对于大多数小型应用,在 VPS 生产环境中运行 SQLite 是正确的选择,原因很简单:单台机器上的单个进程向单个文件写入数据,不需要数据库服务器。不需要管理守护进程,不需要配置防火墙端口,不需要轮换密码,也不需要维持另一台机器运行。查询是函数调用,而不是网络往返,因此一个页面执行 40 次查询,成本就是 40 次函数调用。
这种方案的限制很明确,而且确实存在。整个数据库文件同时只能有一个写入者,并且该文件不能在两台机器之间共享。对于运行单个应用的单台 VPS,这两个限制都不是问题。一旦超出这种架构,两个限制都会立即成为致命问题。本指南介绍如何设置 SQLite 以确保其在服务器上安全运行、如何使用 Litestream 执行持续备份,以及何时应停止使用 SQLite。
先安装命令行工具。以下操作均在 Ubuntu 24.04 上执行。
sudo apt update
sudo apt install -y sqlite3
sqlite3 --version该命令会输出一个以 3. 开头的版本号,后面跟着构建日期和源代码哈希值。截至 2026 年 10 月,Ubuntu 24.04 提供 SQLite 3.45.1。您的应用可能不会使用这个二进制文件:大多数语言运行时都会捆绑自己的 SQLite 库副本,而且通常版本更新,因此在依赖较新的功能前,请先检查数据库驱动报告的版本。
为什么首先要修改 WAL 模式
SQLite 默认使用回滚日志。修改页面前,它会将原始页面复制到 -journal 文件中,然后直接修改数据库。为确保操作安全,SQLite 会锁定整个文件并获取排他锁。因此,只要有写操作正在进行,所有读取操作都必须等待。在笔记本电脑上通常不会察觉这一点。在 Web 服务器上,一次缓慢的写操作就可能阻塞所有访问该数据库的请求。
WAL(预写日志)模式会反转这一顺序。写入者将新页面追加到单独的 -wal 文件中,而不修改主数据库。读取者继续从启动时对应的快照读取主文件,因此读取者不会阻塞写入者,写入者也不会阻塞读取者。之后,检查点操作会将 WAL 中累积的页面复制回主数据库。正是这一项修改,让 SQLite 基本可以用于 Web 应用后端。
启用 WAL 模式并确认设置已生效
mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"该命令会输出 wal。这不是无关紧要的信息。PRAGMA journal_mode 会返回数据库实际使用的模式,因此如果返回 delete,说明更改失败,数据库仍在使用回滚日志。
WAL 模式具有持久性。它是数据库头中的标志,而不是连接设置。因此,每个数据库文件只需设置一次,之后所有连接都会继承该设置,包括重启后建立的连接。通过新连接验证这一点。
sqlite3 ~/app/app.db "PRAGMA journal_mode;"现在创建一个表,然后查看磁盘上出现了什么。
sqlite3 ~/app/app.db <<'SQL'
CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
INSERT INTO notes (body) VALUES ('first row');
SQL
ls -l ~/app/现在共有 3 个文件:app.db、app.db-wal 和 app.db-shm。-wal 文件保存尚未完成检查点的已提交页面。-shm 文件是共享内存索引,所有连接都会映射它,以便对 WAL 中的内容保持一致。这两个文件都属于数据库,不是临时文件。应用运行期间单独复制 app.db,得到的文件会缺少所有最近的提交。删除 app.db 而保留另外两个文件,SQLite 就会将这些过时的 WAL 页面应用到之后以该名称创建的新文件中。尝试重置数据库时,这会导致新数据库损坏。
生产环境中的每个应用都需要的连接设置
只有 journal_mode 会存储在数据库中。下面的其他设置都针对单个连接。这意味着应用必须在每个打开的连接上执行这些设置,包括连接池在后台创建的每个连接。
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;busy_timeout = 5000 告诉 SQLite:数据库被锁定时,最多重试 5000 毫秒,然后返回 database is locked。默认值为 0,因此默认情况下,SQLite 在两个写入者首次发生重叠时会立即失败。设置这一个值,就能消除大多数被归咎于 SQLite 本身的锁错误。
synchronous = NORMAL 是 WAL 模式下的正确设置,但需要了解其中的取舍。在 FULL 下,SQLite 会在每次提交时对 WAL 调用 fsync。在 NORMAL 下,它会改为在检查点执行同步。SQLite 文档明确说明了需要放弃什么:发生断电或硬重置后,事务不再具有持久性。数据库不会因断电而损坏,但尚未写入磁盘的最后几次提交会丢失。对于 VPS,这通常是正确的取舍,因为这样可以避免每次写入都执行一次 fsync。
foreign_keys = ON 默认关闭,以保持向后兼容,并且它针对单个连接。包含大量 REFERENCES 子句的架构,在每个连接启用此设置之前,完全不会强制执行这些约束。
还有一个设置只会在后续产生影响。WAL 增长超过 1000 个页面后,SQLite 会自动执行检查点;具体由当时恰好完成事务的连接执行。这本身没有问题。运行 Litestream 时,情况就需要考虑,因为 Litestream 需要控制检查点的执行时机。
设置 busy_timeout 后为什么仍然会发生 database is locked
这是导致人们重新使用 Postgres 的故障,而且原因很明确。
busy timeout 会安装 busy handler,但 SQLite 不保证一定会调用它。
如果 SQLite 判断调用 busy handler 可能导致死锁,就会直接向应用返回 SQLITE_BUSY,而不会调用 busy handler。
它要避免的死锁发生在事务升级时。在 SQLite 中,单独使用 BEGIN 表示 BEGIN DEFERRED。如果它之后的第一条语句是 SELECT,则当前处于读事务中。当同一事务中后续的 UPDATE 需要变为写事务,而另一个连接自读取开始后已经执行过写操作时,SQLite 无法让当前连接等待。因为当前快照已经过期,继续等待只会导致两个连接相互死锁。文档直接说明了结果:
后续的写语句会在可能的情况下将事务升级为写事务,否则返回 SQLITE_BUSY。
您的 5000 毫秒超时设置根本不会生效。错误会立即返回,因此看起来就像设置没有任何作用。
修复方法只有一个词。
BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;BEGIN IMMEDIATE 会在事务开始时立即获取写锁,然后才读取数据。不存在事务升级,也就没有需要避免的死锁,因此 busy handler 会生效,连接会等待轮到自己,而不是直接失败。只读事务应继续使用 deferred 模式。包含写操作的事务都应使用 immediate 模式。
锁错误的第二个原因更难发现:写事务跨越耗时操作而一直保持打开状态。SQLite 会串行执行写入,因此,如果一个事务打开后通过网络调用外部 API,最后才提交,那么该调用持续期间,其他所有写入都会被阻塞。先读取所需数据,关闭事务,执行耗时操作,然后重新打开一个简短的写事务保存结果。
使用 Litestream 持续备份
每晚复制一次最多会丢失一天的写入数据,而对正在使用的 SQLite 数据库运行 cp 可能生成无法打开的副本。以下两种方式是安全的。sqlite3 app.db ".backup /path/to/backup.db" 使用 SQLite 的在线备份接口,可处理正在使用的数据库。Litestream 更进一步:它监控 WAL,并持续将变更上传到对象存储,将最坏情况下的数据丢失时间从一天降至约 1 秒。
Litestream 是一个与应用并行运行的 Go 二进制文件。它不位于应用与数据库之间。应用仍按原方式写入 SQLite,Litestream 读取 WAL 并上传发生变更的部分。
cd /tmp
curl -fsSL -O https://github.com/benbjohnson/litestream/releases/download/v0.5.14/litestream-0.5.14-linux-x86_64.deb
sudo dpkg -i litestream-0.5.14-linux-x86_64.deb
litestream version截至 October 2026,官方 Linux 安装页面仍记录 v0.5.14。之后已有更新版本:v0.5.15 于 21 July 2026 发布,最新标签 v0.5.17 于 31 August 2026 发布。请将两行中的版本改为 releases 页面上的当前标签。如果 VPS 使用 arm64,则改用匹配的 arm64 软件包。
配置文件位于 /etc/litestream.yml。先使用本地文件副本,因为这样无需云凭据即可验证整个流程。
dbs:
- path: /home/appuser/app/app.db
replica:
type: file
path: /var/backups/litestream/app注意,该字段是单数形式的 replica。Litestream 0.5 用单个副本块替换了 0.3 系列中的 replicas 数组,包含两个条目的配置现在会在启动时失败。许多第三方指南仍显示旧数组,因此请按照上面的结构复制,不要直接使用搜索结果中的第一个示例。0.5 系列还将 litestream wal 子命令重命名为 litestream ltx,因为磁盘上的备份格式发生了变化。
启用任何功能前,先检查配置是否能正确解析。
sudo litestream databases -config /etc/litestream.yml然后手动验证完整往返流程。此形式跳过配置文件,将一个数据库复制到一个路径。
mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/app该命令会在前台运行并持续执行。在第二个 shell 中写入一行数据,然后将副本恢复到新文件。
sqlite3 ~/app/app.db "INSERT INTO notes (body) VALUES ('written after replication started');"
litestream restore -o /tmp/restored.db file:///tmp/replica/app
sqlite3 /tmp/restored.db "SELECT count(*) FROM notes;"计数包含新写入的行。如果不包含,说明变更尚未同步:Litestream 按 sync-interval 推送,默认间隔为 1 秒,因此请等待后再次恢复。这个 1 秒也是恢复点间隔。发生崩溃时,最多会丢失上一个同步间隔内的写入数据,任何配置都无法将其降为 0。
实际使用时,将副本块替换为 S3 URL。该方式适用于 Amazon S3,也适用于其他提供商提供的 S3 兼容对象存储。
dbs:
- path: /home/appuser/app/app.db
replica:
url: s3://your-bucket-name/app
region: us-east-1
snapshot:
interval: 24h
retention: 24h不要将凭据写入该文件。Litestream 从环境变量中读取 LITESTREAM_ACCESS_KEY_ID 和 LITESTREAM_SECRET_ACCESS_KEY,因此请将它们写入由 root 所有且权限为 600 的 systemd drop-in 文件。
上面的快照值是默认值,保留时间的默认值容易造成误解。保留时间决定 Litestream 保留快照及其所属文件的时长,因此也决定可以恢复到多早之前。24 小时意味着,如果您在周三早上发现迁移错误,就已经无法从周一的状态恢复。在 Litestream 0.5 中,保留时间是根 snapshot 块中的全局设置,而不是每个副本单独设置。若要保留一周,请将该块改为 retention: 168h,并承担额外的存储成本。在副本块中使用 retention: 行是 0.3 系列的语法。Litestream 0.5 会忽略该行而不报错,并继续使用 24 小时的默认值。
在需要恢复前先验证恢复流程
litestream restore -o /tmp/check.db /home/appuser/app/app.db
sqlite3 /tmp/check.db "PRAGMA integrity_check;"
sqlite3 /tmp/check.db "SELECT count(*) FROM notes;"给定数据库路径,litestream restore 会在 /etc/litestream.yml 中查找匹配的副本并将其下载。对于正常的文件,PRAGMA integrity_check 会输出 ok;任何其他输出都表示恢复的副本不可用。使用systemd 服务和计时器按计划运行此检查,并读取输出。在至少成功恢复过一次备份之前,您无法确认备份可用。
在 systemd 下运行 Litestream
Debian 软件包会安装一个读取 /etc/litestream.yml 的 litestream 单元。
sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -f正常输出会列出配置中的每个数据库,然后除定期同步信息外保持安静。针对数据库路径报告 no such file or directory,表示配置中的路径错误,或进程无法读取该路径。该单元默认以 root 身份运行,这超出了此任务所需的权限。Litestream 必须能够读写数据库及其所在目录,因为它会处理数据库旁边的 -wal 和 -shm 文件。因此,应将其运行用户设置为应用程序已有的运行用户,本例中为 appuser。
# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuser使用 sudo systemctl daemon-reload 和 sudo systemctl restart litestream 应用这些设置。设置 具有最小权限的专用服务账户 只需几分钟;这决定了该主机上运行的是备份代理,还是另一个 root 进程。
如果需要从零重建机器,还要注意启动顺序。应先恢复数据库,再启动应用程序。litestream restore 接受 -if-db-not-exists;如果文件已经存在,该命令会以退出码 0 结束,因此可以在每次启动时安全运行。将它放在应用程序单元的 ExecStartPre 行中,新建的 VPS 就会在应用程序启动前下载数据库,已有数据库的机器则不执行任何操作。如果希望将其集中配置,litestream replicate 提供了对应的 -restore-if-db-not-exists 标志。
VPS 上 SQLite 失效的场景
网络文件系统。 这是无法通过配置规避的限制。WAL 模式要求使用数据库的每个进程共享一小块内存区域,-shm 文件正是用于提供该区域。SQLite 文档明确规定:
使用数据库的所有进程必须位于同一台主机上;WAL 无法通过网络文件系统工作。
因此,位于已挂载 NFS(网络文件系统)或 SMB 共享上的数据库可能发生损坏,任何 pragma 都无法避免这一问题。这里有一个容易混淆的区别。网络块设备是大多数 VPS 提供商附加额外存储时使用的设备。Linux 会将它识别为带有普通文件系统的普通磁盘,这没有问题。已挂载的文件共享则不同。
第二台应用服务器。 没有任何设置可以让这种场景正常工作。一旦需要两台机器提供同一份数据,就需要使用支持网络通信的数据库。应在仍有时间规划时决定是否迁移。
写入密集型负载。 同一时间只能有一个写入者,这是文件格式的特性,无法通过参数调节。短事务写入开销较低,因为每次提交都会追加到 WAL,因此吞吐量更接近磁盘的小块写入延迟,而不是 CPU 性能。请参阅 VPS 上的 NVMe 与 SATA SSD 存储对比,了解两者的差异。真正的问题是长事务,因为它们会让其他所有写入者排队等待。
分析查询。 SQLite 是面向事务构建的行存储数据库。扫描一亿行数据的仪表板属于另一类任务,应使用其他工具;DuckDB 与 SQLite 的服务器端工作对比介绍了两者的适用边界。
复制期间执行 VACUUM。 完整的 VACUUM 会重写整个数据库文件,从而使 Litestream 依赖的页面级跟踪失效。Litestream 文档建议在复制处于活动状态时不要直接执行该操作。文档建议使用 VACUUM INTO,该命令会将压缩后的副本写入单独文件,并保留原文件不变。如果必须直接执行 vacuum,文档规定的步骤比重启更复杂:停止复制器,执行 vacuum,删除数据库旁边 Litestream 的隐藏跟踪目录以及现有的远程副本数据,然后重新启动复制器,使其从全新快照开始。最后一步会丢弃现有的恢复历史。
同一数据库运行两个复制器。 不要让两个 Litestream 进程同时处理同一个数据库或使用同一个副本目标。文档明确指出,防止这种情况是你的责任;否则生成的副本将无法恢复。
Litestream 不负责的内容
Litestream 只保护数据库文件,不负责其他内容。上传的文件、应用配置、TLS(传输层安全)证书和 unit 文件仍需您自行处理。按计划将其与使用 restic 加密到异机的备份结合使用,即可覆盖这两部分内容。如果服务器是新建的,请参阅新 VPS 上的前 10 分钟,其中介绍了本指南默认已经完成的用户账户和防火墙配置。
FAQ
生产环境中的应用使用 SQLite 足够吗?
对于一台服务器上的单个应用,足够,前提是启用 WAL 模式、设置 busy timeout,并持续执行备份。需要关注的限制是结构性的:同一时间只能有一个写入者,并且只能使用一台主机。符合这些限制的应用可以使用无需网络跳转、无需单独进程监控的数据库。不符合这些限制的应用需要使用客户端-服务器数据库,任何调优都无法改变这一点。
设置 busy_timeout 后,为什么仍然会收到 database is locked?
因为等待可能导致死锁时,SQLite 会跳过 busy handler。以裸 BEGIN 开始的事务是延迟事务:开始执行的 SELECT 会使其进入读事务,后续写入则必须执行事务升级。如果期间另一个连接完成了写入,SQLite 会立即返回 SQLITE_BUSY,而不是调用 busy handler,因为当前读快照已经过期。所有会执行写入的事务都应使用 BEGIN IMMEDIATE 开始,这样可以预先获取写锁,超时设置才能生效。
可以将 SQLite 数据库放在网络存储上吗?
不能放在 NFS 或 SMB 等网络文件系统上。WAL 模式需要所有进程通过 -shm 文件共享内存,SQLite 文档规定,使用该数据库的每个进程都必须位于同一台主机上。云服务商提供的网络块设备则不同:Linux 会将其视为带有普通文件系统的普通磁盘,SQLite 可以在其上正常工作。
如果已经每天晚上执行备份,还需要 Litestream 吗?
这取决于您最多能承受丢失多少数据。每天执行一次备份意味着最多可能丢失 24 小时的写入数据。Litestream 大约每秒同步一次,因此崩溃时大约只会丢失最后 1 秒的数据。使用 cp 直接复制数据库文件可能在数据库写入过程中复制到不完整状态,因此 Litestream 也更安全。Litestream 只负责数据库,因此仍应同时运行通用文件备份。