SSD Nodes Learn 8GB 内存 — 每年 $66
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-01

SQLite 适合在 VPS 上用于生产环境吗?

了解 SQLite 在单台 VPS 上运行小型应用的适用边界,掌握 WAL、busy_timeout 与 Litestream 配置,并识别单写入者和跨机器共享等限制。

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

SQLite 适合作为 VPS 生产数据库的场景

对于大多数小型应用,在 VPS 上的生产环境中运行 SQLite 是正确的选择。原因很简单:一台机器上的一个进程向一个文件写入数据,不需要数据库服务器。无需监管守护进程,无需配置防火墙放行端口,无需轮换密码,也无需维持第二台机器运行。查询是函数调用,而不是网络往返。因此,运行四十次查询的页面只需进行四十次函数调用。

它的限制明确且实际。整个数据库文件同一时间只能有一个写入者,而且该文件不能在两台机器之间共享。对于运行单个应用的单台 VPS,这两个限制都没有问题。一旦超出这种部署形态,这两个限制都会成为致命问题。本指南介绍如何配置 SQLite 以确保服务器上的运行安全,如何使用 Litestream 执行持续备份,以及应在何时停止使用 SQLite。

首先安装命令行工具。以下操作均在 Ubuntu 24.04 上执行。

sudo apt update
sudo apt install -y sqlite3
sqlite3 --version

该命令会输出一个以 3. 开头的版本字符串,后面跟着构建日期和源代码哈希。以 2026 年 7 月为准,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.dbapp.db-walapp.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

截至 July 2026,官方 Linux 安装页面记录的版本是 v0.5.14,v0.5.15 于 21 July 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_IDLITESTREAM_SECRET_ACCESS_KEY 中读取凭据,因此请将它们放入由 root 所有、权限为 600 的 systemd drop-in 文件中。

上面的快照设置使用默认值,但保留期限的默认值容易造成误解。保留期限表示 Litestream 保留快照及其所属文件的时间,也决定了可以恢复到多早的时间点。24 小时意味着,如果你在 Wednesday morning 发现迁移错误,就已经无法从 Monday 的状态恢复。将 retention: 168h 设置为一周,并承担额外的存储成本。

在需要之前验证恢复

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.ymllitestream 单元。

sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -f

正常输出会列出配置中的每个数据库,随后除定期同步信息外保持安静。如果针对数据库路径出现 no such file or directory 错误,说明配置中的路径不正确,或进程无权读取该路径。该单元默认以 root 身份运行,其权限超出了此任务的需要。Litestream 必须能够读写数据库及其所在目录,因为它会处理数据库旁边的 -wal-shm 文件。因此,应让它使用应用程序已有的账号。

# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuser

使用 sudo systemctl daemon-reloadsudo systemctl restart litestream 应用这些设置。设置 具有最小权限的专用服务账号 只需几分钟。这样可以避免在服务器上运行第二个 root 进程,而只运行备份代理。

如果需要从零重建机器,还必须注意启动顺序。应先恢复数据库,再启动应用程序。litestream restore 接受 -if-db-not-exists;如果文件已经存在,它会以退出状态 0 结束,因此每次启动时运行都是安全的。将它放在应用程序单元的 ExecStartPre 行中。这样,新建的 VPS 会下载数据库,而已有数据库的实例不会执行任何操作。litestream replicate 提供了对应的 -restore-if-db-not-exists 标志;如果希望集中配置,也可以使用该标志。

SQLite 在 VPS 上的限制

网络文件系统。 这是无法通过配置绕开的限制。WAL 模式要求使用数据库的每个进程共享一小段内存区域,该区域由 -shm 文件提供。SQLite 文档明确规定:

使用数据库的所有进程必须位于同一台主机上;WAL 不支持网络文件系统。

因此,位于已挂载 NFS(network file system)或 SMB 共享上的数据库可能损坏,并且没有任何 pragma 可以避免这一问题。这里有一个常被忽略的区别。网络设备是大多数 VPS 提供商用于附加存储的设备。它在 Linux 中表现为带有普通文件系统的普通磁盘,因此没有问题。已挂载的文件共享则不同。

第二台应用服务器。 没有任何设置可以让这种架构正常工作。一旦需要两台机器提供同一份数据,就需要使用支持网络通信的数据库。应在仍有时间规划时完成迁移决策。

写入密集型负载。 一次只能有一个写入者,这是文件格式的特性,不是可调参数。短写入开销较低,因为每次提交都会追加到 WAL 中。因此,吞吐量更接近磁盘的小块写入延迟,而不是 CPU 性能。请参阅 VPS 上的 NVMe 与 SATA SSD 存储,了解这种差异的表现。真正的问题是长事务,因为它们会使其他所有写入者排队等待。

分析查询。 SQLite 是为事务处理构建的行存储数据库。扫描一亿行的仪表板属于不同工具处理的另一类任务,DuckDB 与 SQLite 在服务器场景中的比较介绍了两者的适用边界。

复制环境中的 VACUUM 完整的 VACUUM 会重写整个数据库文件,因此 Litestream 必须再次上传全部文件。Litestream 文档建议不要在复制处于活动状态时执行该操作。停止复制器,执行 vacuum,然后重新启动复制器,并预期会生成新的完整快照。

同一数据库上的两个复制器。 绝不要让两个 Litestream 进程同时处理同一个数据库或同一个副本目标。文档明确指出,防止这种情况发生是您的责任;否则会生成无法恢复的副本。

Litestream 不涵盖的内容

Litestream 只保护数据库文件,不处理其他内容。上传的文件、应用配置、TLS(传输层安全)证书和 unit 文件仍需您自行处理。按计划将其与使用 restic 进行加密的异机备份结合,即可覆盖这两部分。如果服务器是新建的,请先阅读新 VPS 上的前十分钟,完成本指南默认已经完成的用户账户和防火墙配置。

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 秒的数据。使用 Litestream 也比通过 cp 复制数据库文件更安全,因为后者可能在数据库写入过程中复制文件。Litestream 只保护数据库,因此仍应同时运行通用文件备份。

#sqlite#wal#litestream#备份#production