用了四回这个数据库,这回有Django ORM帮忙,反而踩的坑最深。本以为照教程打开WAL模式就万事大吉,结果一个4000行的全文搜索花了5秒——这跟数据库本事无关,是我根本不懂怎么伺候它。
故事从那条慢得离谱的查询说起。表里总共才4000行数据,用自带的FTS5做全文检索,居然要憋5秒才出结果。电脑明明很快,这点数据量本该眨眼间完成,直觉告诉我不对劲。可这个库不像别的系统那样主动汇报问题,它只会安静地执行一个你从没看过的查询计划。
排查得满头雾水,直到我偶然想起那篇被遗忘的文档——ANALYZE命令。跑完这一行,同样的查询从5秒直落到0.05秒,快了差不多100倍。反差太大,我已经懒得精确计时了,反正快得让人没脾气。
至于查询计划到底错在哪,我至今没完全弄明白,但最合理的猜测是某种意外的O(n²)行为。数据库在缺少统计信息的时候,选了一条糟透的路径——也许把本该线性扫描的操作,做成了嵌套循环之类的蠢事。
ANALYZE能扭转局面,是因为它生成了统计信息。每个表大约有多少行、索引里值的分布情况……这些数字看起来琐碎,却正是查询优化器决定用哪个索引、按什么顺序联表的依据。没有统计,优化器就是个瞎子,全凭内置启发式规则瞎猜,猜对了欢天喜地,猜错了就变成5秒噩梦。所以哪怕数据量再小,让数据库心里有数总没错。
这件事最大的教训是:它终究是个正儿八经的数据库,数据库就很复杂,不是那种“装好就能忘”的嵌入式小玩意儿。只要让它干点正经活,就得拿出维护的态度来。之前有人告诉我,小站点用它完全够用,但没人提醒“够用”的前提是你得懂一点点运维,比如知道ANALYZE的存在,比如WAL模式只是起点。
说到WAL模式,几乎所有教你用它做网站的博客都会提:先打开WAL。WAL就是Write-Ahead Logging,把写入先写到单独的日志文件里,让读操作和写操作可以并发,不至于一个写锁把所有读都卡住。对大部分时间在“读”的网站来说,这几乎是救命配置。我也确实第一时间就开了,还在心里得意,以为数据库这就调优完毕了。
但WAL管不了长时间写锁。这个库在写操作上仍然是单写入者模型,同一时间只能有一个连接在写,其他试图写入的连接会直接收到SQLITE_BUSY错误或者等待超时。这点在清理数据时彻底爆发了。
场景很简单:我往表里塞了大量不该存在的行,比如已完成的任务记录,想删掉它们。一条DELETE语句跑下去,因为行数多,耗时超过5秒。偏偏这时候其他worker也想往里写东西,等了5秒后超时崩溃,整个虚拟机就跟着停了。5秒恰恰是我自己设的超时门槛,明明是自己给的限制,每次触发还是会愣一下,觉得自己坑了自己。
我不太确定那次DELETE为什么那么慢,也许事务里夹了大量Python代码,也许表上有复杂的触发器和外键约束,总之不是单纯的“数据多所以删得慢”。但不管原因是什么,结果都一样:一个长事务会阻塞所有其他写入,直到它提交或回滚为止。
这种并发写入限制在这时候格外刺眼。换成那些“重型”数据库,可以同时跑好几个写入者,清理大表不至于把整个应用挂掉,顶多就是个别的行锁冲突。而它用的却是库级锁,写操作一旦出手就独占整个数据库文件的写权限。之前只在理论上知道这种差异,等到生产环境真的被卡住,才对“单写入者”三个字有了切肤之痛。难怪有些人宁可多挂一层别的数据库,也不愿在它上面做任何可能耗时的批量写操作。
我的临时方案是分批删除。把一次大清理切成一小块一小块地跑,每批都在5秒内结束,这样其他worker的写入请求不会超时。缺点也摆在脸上:操作被拆散后要反复提交事务,整体时间反而更长,还得手动控制批次大小,毫无优雅可言。活像一个拙劣的排队系统,靠碎片化时间勉强干完一大件活。
另一个念头是在维护时段把整站下线,再放开手脚清理,这样能彻底避开竞争,一劳永逸。可我的小项目还没成熟到规划出专门的维护窗口,下线就得手动切流量、发通告,整套流程的成本比删数据本身还高。所以到现在我还在分批删数据的路上晃悠,颇有点苟且运维的味道。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.