SQLite 默认未开启 WAL ,这会显著限制并发性能。
PlanTodo 是一个计划管理软件,最近我为它的同步服务编写了性能测试,经过实测,仅开启 WAL 就让同步服务的吞吐量便提升至原来的 3 倍,
其实关于 SQLite 性能优化的文章早有珠玉在前,比如 Optimal SQLite settings for Django 和 Optimizing SQLite for servers ,所以本篇文章并没有独创性,只是为了让更多人了解 SQLite 的性能以及分享一个真实的性能测试用例。
PlanTodo 同步服务性能测试
性能测试分为三个:
- oo_upload (one user, one device for a user, only upload) ,就是一个用户一台设备仅上传
- oo_download 就是仅下载
- oo_cross 是上传和下载交错进行
oo_upload 和 oo_download 是为了查看上传和下载场景下的极限性能,是为了将来专门优化时用来参考的。而 oo_cross 则较为贴近真实使用场景:用户的某个设备上传几个更新,另一个设备被触发下载;因此可以拿它计算服务器能承受的用户量。
如果你不想看下面具体的测试数据,这里简单展示了吞吐量的变化:
- oo_upload ,18027 -> 61682 ,是原来的 3.42 倍
- oo_download ,17082 -> 49635 ,是原来的 2.90 倍
- oo_cross ,17085 -> 44203 ,是原来的 2.58 倍
一般查询是比写入要快的,因此下载应该比上传快,但 PlanTodo 的同步服务却反了过来,说明有很大的优化空间。
下面是开启 WAL 前的测试数据:
+ just -f services/sync/justfile headless_oo_upload --less-output
============================================================
Performance Summary for test_oo_upload
============================================================
Requests : 18,027
Failures : 0
Failure Rate : 0.00%
Average RT : 78.14 ms
P50 : 78 ms
P95 : 110 ms
P99 : 130 ms
Max : 272.81 ms
Endpoints
------------------------------------------------------------
POST /v1/sync/delta
Requests=18,011 Avg=78.1ms P95=110ms P99=130ms Max=272.8ms
POST /v1/clients
Requests=8 Avg=76.2ms P95=110ms P99=110ms Max=109.9ms
GET /v1/sync/full
Requests=8 Avg=57.2ms P95=120ms P99=120ms Max=120.3ms
+ just -f services/sync/justfile headless_oo_download --less-output
============================================================
Performance Summary for test_oo_download
============================================================
Requests : 17,082
Failures : 0
Failure Rate : 0.00%
Average RT : 82.63 ms
P50 : 82 ms
P95 : 110 ms
P99 : 130 ms
Max : 370.33 ms
Endpoints
------------------------------------------------------------
POST /v1/sync/delta
Requests=16 Avg=172.6ms P95=370ms P99=370ms Max=370.3ms
GET /v1/sync/delta?cursor=1780662404990&limit=100
Requests=2,087 Avg=83.4ms P95=110ms P99=130ms Max=316.1ms
GET /v1/sync/delta?cursor=1780662403978&limit=100
Requests=2,102 Avg=83.3ms P95=110ms P99=130ms Max=325.5ms
+ just -f services/sync/justfile headless_oo_cross --less-output
============================================================
Performance Summary for test_oo_cross
============================================================
Requests : 17,085
Failures : 0
Failure Rate : 0.00%
Average RT : 83.05 ms
P50 : 82 ms
P95 : 120 ms
P99 : 150 ms
Max : 245.89 ms
Endpoints
------------------------------------------------------------
GET /v1/sync/delta?cursor=1780662760888&limit=100
Requests=1 Avg=245.9ms P95=250ms P99=250ms Max=245.9ms
GET /v1/sync/delta?cursor=1780662760900&limit=100
Requests=1 Avg=245.4ms P95=250ms P99=250ms Max=245.4ms
GET /v1/sync/delta?cursor=1780662758760&limit=100
Requests=1 Avg=228.2ms P95=230ms P99=230ms Max=228.2ms
下面是开启 WAL 之后的测试数据:
+ just -f services/sync/justfile headless_oo_upload --less-output
============================================================
Performance Summary for test_oo_upload
============================================================
Requests : 61,682
Failures : 0
Failure Rate : 0.00%
Average RT : 22.30 ms
P50 : 22 ms
P95 : 32 ms
P99 : 41 ms
Max : 74.82 ms
Endpoints
------------------------------------------------------------
POST /v1/clients
Requests=8 Avg=33.8ms P95=48ms P99=48ms Max=48.1ms
POST /v1/sync/delta
Requests=61,666 Avg=22.3ms P95=32ms P99=41ms Max=74.8ms
GET /v1/sync/full
Requests=8 Avg=20.7ms P95=32ms P99=32ms Max=32.5ms
+ just -f services/sync/justfile headless_oo_download --less-output
============================================================
Performance Summary for test_oo_download
============================================================
Requests : 49,635
Failures : 0
Failure Rate : 0.00%
Average RT : 28.03 ms
P50 : 28 ms
P95 : 38 ms
P99 : 46 ms
Max : 305.64 ms
Endpoints
------------------------------------------------------------
POST /v1/sync/delta
Requests=16 Avg=127.0ms P95=310ms P99=310ms Max=305.6ms
GET /v1/sync/delta?cursor=1780663066610&limit=100
Requests=6,109 Avg=28.4ms P95=38ms P99=46ms Max=246.0ms
GET /v1/sync/delta?cursor=1780663068640&limit=100
Requests=6,064 Avg=28.3ms P95=38ms P99=46ms Max=95.8ms
+ just -f services/sync/justfile headless_oo_cross --less-output
============================================================
Performance Summary for test_oo_cross
============================================================
Requests : 44,203
Failures : 0
Failure Rate : 0.00%
Average RT : 31.80 ms
P50 : 28 ms
P95 : 45 ms
P99 : 150 ms
Max : 477.37 ms
Endpoints
------------------------------------------------------------
GET /v1/sync/delta?cursor=1780663421960&limit=100
Requests=1 Avg=477.4ms P95=480ms P99=480ms Max=477.4ms
GET /v1/sync/delta?cursor=1780663421963&limit=100
Requests=1 Avg=475.9ms P95=480ms P99=480ms Max=475.9ms
GET /v1/sync/delta?cursor=1780663421966&limit=100
Requests=1 Avg=475.4ms P95=480ms P99=480ms Max=475.4ms
每个测试只持续了 3 min ,因此数据量很小,在大数据量的情况下,性能可能会下降很多,这个会在将来补充。
在测试 oo_cross 里,3min 处理了 44203 个请求,也就是一秒 245 个。测试其实是不断在重复先上传再下载,而客户端也是上传下载成对出现,因此一秒能处理 122 ( 245 / 2 )台设备的请求,即便因为多用户、多设备带来的其他压力,至少能稳定在一秒 100 个请求。
考虑到,真实使用下,平均要几分钟到十几分钟才会更新一次内容,取 10 分钟一次的话,一个同步服务极限下能支撑 100 x 60 x 10 ,6 万个设备正常使用。
如何开启 WAL
不管是什么 ORM ,开启的方式都是一样的,就是在连接数据库后,执行一次 PRAGMA journal_mode=WAL。
开启了 WAL 后,在原本的 SQLite 数据库文件那里,会多出两个 .db-shm、.db-wal 的文件,可以以此判断是否成功开启。
SQLAlchemy
from sqlalchemy import create_engine, event
engine = create_engine(
DATABASE_URL,
echo=False,
connect_args={
"timeout": 5,
},
)
if engine.dialect.name == "sqlite":
@event.listens_for(engine, "connect")
def set_sqlite_pragma(dbapi_connection, _):
cursor = dbapi_connection.cursor()
# cursor.execute("PRAGMA foreign_keys=ON")
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA synchronous=NORMAL")
cursor.execute("PRAGMA temp_store=MEMORY")
cursor.execute("PRAGMA cache_size=2000")
cursor.execute("PRAGMA mmap_size=134217728")
cursor.close()
PlanTodo 的同步服务就是使用 FastAPI + SQLAlchemy 开发的。由于同步服务的特殊性,这里没有开启外键约束。
Django
在项目的 settings.py 文件里,添加 init_command:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": {
"init_command": (
"PRAGMA foreign_keys = ON;"
"PRAGMA journal_mode = WAL;"
"PRAGMA synchronous = NORMAL;"
"PRAGMA busy_timeout = 5000;"
"PRAGMA temp_store = MEMORY;"
"PRAGMA cache_size = 2000;"
"PRAGMA mmap_size = 134217728;"
),
},
}
}
drift
在数据库类的 migration get 方法里,在 beforeOpen 这个回调里增加执行命令:
class PtdDatabase extends _$PtdDatabase {
// 省略无关代码
@override
MigrationStrategy get migration {
return MigrationStrategy(
beforeOpen: (details) async {
// 在每次打开数据库,正式使用之前,执行的命令
await customStatement('PRAGMA journal_mode = WAL');
await customStatement('PRAGMA synchronous = NORMAL');
await customStatement('PRAGMA busy_timeout = 5000');
await customStatement('PRAGMA temp_store = MEMORY');
await customStatement('PRAGMA cache_size = -2000');
},
);
}
}
原文链接: https://yanh.tech/2026/06/sqlite-performance-optimization/
版权声明:本博客所有文章除特別声明外,均为 AhFei 原创,采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技焉洲 (yanh.tech) 。