现象
Run mutation 会开启一个 serializable 事务,并在这个事务内部把配置发给 dae、等待重载完成:
// graphql/mutation.go
tx := db.BeginTx(context.TODO())
ret, err := config.Run(tx, args.Dry)
config.Run 内部把配置发到 dae.ChReloadConfigs 后阻塞等待回调,也就是说整个重载期间数据库事务一直开着。重载耗时取决于节点数量与 DNS 解析,节点多时可以到几十秒。
同时 db/db.go 里 sqlite.Open(path) 没有带任何 DSN 参数,默认是 rollback journal 模式——写事务开启期间读会被阻塞。两者叠加,重载期间任何并发查询都会等到超时:
database is locked (5) (SQLITE_BUSY)
[5033.408ms] [rows:0] SELECT * FROM `subscriptions` WHERE ...
登录用的 users 表查询也在其中,表现为密码正确却提示 incorrect username or password。
建议的两处改动
1. 重载移出事务:读配置用一个事务,发起重载前先 commit;重载结束后再开一个短事务写运行状态。
2. SQLite 开启 WAL:DSN 加上 journal_mode(WAL) 与 busy_timeout(30000),读不再被写事务阻塞。
实测数据
我维护着一个 OpenWrt 打包分支,在那里订阅定时更新完成后会自动应用配置,所以这条路径每天都会走到,问题也更容易暴露。在一台 OpenWrt x86_64 测试机上,三个订阅各 300 个节点、cron 设为每分钟同时触发、订阅内容每轮变化:
|
改动前 |
改动后 |
5 分钟内 database is locked |
52 次 |
0 次 |
| 登录查询耗时 > 1 秒 |
15 次 |
0 次 |
第 2 条与自动应用与否无关,对当前上游的手动应用流程同样有收益。
一个需要注意的副作用
启用 WAL 后,数据库的持久状态分布在 wing.db 和 wing.db-wal 两个文件里,只复制主库可能丢失已提交的数据(我们的测试机上出现过主库 4KB、WAL 1MB 的情况),备份与打包流程需要相应调整。
如果这两个方向可以接受,我可以提 PR。
现象
Runmutation 会开启一个 serializable 事务,并在这个事务内部把配置发给 dae、等待重载完成:config.Run内部把配置发到dae.ChReloadConfigs后阻塞等待回调,也就是说整个重载期间数据库事务一直开着。重载耗时取决于节点数量与 DNS 解析,节点多时可以到几十秒。同时
db/db.go里sqlite.Open(path)没有带任何 DSN 参数,默认是 rollback journal 模式——写事务开启期间读会被阻塞。两者叠加,重载期间任何并发查询都会等到超时:登录用的
users表查询也在其中,表现为密码正确却提示incorrect username or password。建议的两处改动
1. 重载移出事务:读配置用一个事务,发起重载前先 commit;重载结束后再开一个短事务写运行状态。
2. SQLite 开启 WAL:DSN 加上
journal_mode(WAL)与busy_timeout(30000),读不再被写事务阻塞。实测数据
我维护着一个 OpenWrt 打包分支,在那里订阅定时更新完成后会自动应用配置,所以这条路径每天都会走到,问题也更容易暴露。在一台 OpenWrt x86_64 测试机上,三个订阅各 300 个节点、cron 设为每分钟同时触发、订阅内容每轮变化:
database is locked第 2 条与自动应用与否无关,对当前上游的手动应用流程同样有收益。
一个需要注意的副作用
启用 WAL 后,数据库的持久状态分布在
wing.db和wing.db-wal两个文件里,只复制主库可能丢失已提交的数据(我们的测试机上出现过主库 4KB、WAL 1MB 的情况),备份与打包流程需要相应调整。如果这两个方向可以接受,我可以提 PR。