爬虫存结果该多少行提交一次?批太小慢,批太大占内存、进程被杀时丢得多,一行出错整批回滚。这篇在 sqlite 上把批大小、触发条件、被杀和出错时的行为逐项测一遍。
批大小 1:中位 24167 行/秒(23000–24741);提交次数 50000;单个批事务(含组装参数)耗时 中位 0.04 ms,最大 50.6 ms 批大小 10:中位 130194 行/秒(127739–142395);提交次数 5000;单个批事务(含组装参数)耗时 中位 0.06 ms,最大 8.4 ms 批大小 100:中位 324420 行/秒(316855–326310);提交次数 500;单个批事务(含组装参数)耗时 中位 0.20 ms,最大 9.5 ms 批大小 1000:中位 480833 行/秒(438218–508628);提交次数 50;单个批事务(含组装参数)耗时 中位 1.51 ms,最大 12.6 ms 批大小 10000:中位 426959 行/秒(397520–439595);提交次数 5;单个批事务(含组装参数)耗时 中位 21.67 ms,最大 46.8 ms 批大小 50000:中位 452612 行/秒(445481–460314);提交次数 1;单个批事务(含组装参数)耗时 中位 110.47 ms,最大 112.2 ms
每次提交都要付出固定的开销,批越大分摊得越多。实验里 200 字节的小行从批大小 1 到 100 快了十几倍,到 1000 以后持平,10000、50000 并不更快。单个批事务耗时随批大小增加(1000 行 1.51 ms,50000 行 110 ms),批太大还会让别的连接等更久。
批大小 1:中位 9027 行/秒(7084–9099),相当于 172 MiB/秒;一批在内存里的正文共 0.0 MiB 批大小 10:中位 12327 行/秒(12192–14148),相当于 235 MiB/秒;一批在内存里的正文共 0.2 MiB 批大小 100:中位 13295 行/秒(12595–15397),相当于 254 MiB/秒;一批在内存里的正文共 1.9 MiB 批大小 1000:中位 12307 行/秒(11252–13568),相当于 235 MiB/秒;一批在内存里的正文共 19.1 MiB
每行 20 KB 时,提交次数的影响变小(批大小 1 到 100 只快了约 1.5 倍),一批在内存里要占 19.1 MiB(批大小 1000)。行越大,批可以越小。
executemany 中位 507275 行/秒(292117–536995) 循环 execute(同一个事务里) 中位 445479 行/秒(422051–447144) 多行 VALUES(每条 300 行) 中位 536813 行/秒(476374–589346)
executemany、循环 execute、多行 VALUES 差别在 20% 以内,批大小的影响比这大得多。
只按条数,每 500 行提交:生产 1629 行,提交 4 次;行从产生到落盘的等待:中位 325 ms、p99 2609 ms、最大 2629 ms(生产速度:1 秒 1000 行/s → 2 秒 20 行/s → 1 秒 1000 行/s;时间由 sleep 控制,实际速度略低) 每 500 行或每 0.2 秒,先到者:生产 1623 行,提交 20 次;行从产生到落盘的等待:中位 98 ms、p99 202 ms、最大 207 ms(生产速度:1 秒 1000 行/s → 2 秒 20 行/s → 1 秒 1000 行/s;时间由 sleep 控制,实际速度略低) 只按条数,每 50 行提交:生产 1638 行,提交 33 次;行从产生到落盘的等待:中位 32 ms、p99 1341 ms、最大 2034 ms(生产速度:1 秒 1000 行/s → 2 秒 20 行/s → 1 秒 1000 行/s;时间由 sleep 控制,实际速度略低)
只按条数攒批,在生产变慢时一批要等很久才能凑齐(最大等待 2629 ms);加一个超时就能保证最长等待(约 0.2 秒),代价是慢的时候提交次数更多。实际的触发条件常见的是“条数或时间先到者”。
批大小 1 :生产 [1593, 1611, 1645] 行,kill 之后库里少了 [0, 0, 0] 行;integrity_check:ok 批大小 100 :生产 [1796, 1791, 1801] 行,kill 之后库里少了 [96, 91, 1] 行;integrity_check:ok 批大小 1000 :生产 [1808, 1803, 1813] 行,kill 之后库里少了 [808, 803, 813] 行;integrity_check:ok 批大小 1000(或 0.2 秒):生产 [1806, 1803, 1803] 行,kill 之后库里少了 [36, 36, 31] 行;integrity_check:ok
没提交的那批在进程里,kill -9 就丢了:批大小 1000 时丢了约 800 行,批大小 1 时 0 行;库本身没有损坏(integrity_check 为 ok)。能不能接受丢这一批,取决于这些数据能不能重新抓(第 34 篇讲了断点续抓)。
整批 executemany(500 行,第 251 行重复):IntegrityError: UNIQUE constraint failed: r.url;回滚之后库里 1 行(只有开始前插入的那 1 行,另外 499 行也一起没了) 整批失败后改成同一个事务里逐行 SAVEPOINT:成功 499 行、跳过 1 行,库里 500 行(重复行带来的开销:每行多两条语句) 用 INSERT OR IGNORE 一次插入 500 个新行加 1 个重复行:库里 1000 行(重复的被静默跳过,不报错;连接的 total_changes 增加了 500,比 501 少 1,靠它能算出跳过了几行)
一批里有一行违反约束,整批回滚。逐行 SAVEPOINT 能保住其余的行,代价是每行多两条语句;INSERT OR IGNORE 会静默跳过,可用连接的 total_changes 算出跳过了几行。