meta 校验脚本在 1.9 万条记录上换掉 hash 算法后慢了 3 倍
meta 校验脚本原来用的是 md5,前段时间想着换成 sha256 更稳妥,结果同一批 1.9 万条记录,跑一遍从 4 分多钟涨到 14 分钟左右。环境是 Ubuntu 22.04,Python 3.10,磁盘是普通 SATA SSD,单线程读文件算摘要,没做任何并发。
先确认不是 IO 的问题。用 time 分别跑纯读和读+算:
time cat file_list | xargs -I{} dd if={} of=/dev/null bs=1M status=none
time cat file_list | xargs -I{} sha256sum {} > /dev/null
纯读大约 40 秒,sha256sum 那一步才是大头。问题在于我原来是一条一条 hashlib.md5(f.read()).hexdigest(),每次 read 默认块很小,加上 sha256 每字节的计算量本来就比 md5 高不少,单线程下直接被放大了。
改法是两个:把 read 的块调大,以及把摘要计算丢进线程池,读和算重叠起来。核心片段:
import hashlib, os
from concurrent.futures import ThreadPoolExecutor
CHUNK = 1 << 20
def digest(path, algo="sha256"):
h = hashlib.new(algo)
with open(path, "rb") as f:
while True:
b = f.read(CHUNK)
if not b:
break
h.update(b)
return h.hexdigest()
with ThreadPoolExecutor(max_workers=8) as ex:
results = list(ex.map(digest, paths))
改完回到 5 分半左右,比 md5 版本还是慢一点,但能接受。块大小从默认的几 KB 提到 1 MB 是收益最直接的一步,线程池的收益取决于盘,SATA SSD 上 8 个线程差不多到顶,再往上加没明显变化。
代价要说清楚:线程池会让同时打开的文件数变多,如果目录里有大量软链或权限异常的文件,异常处理要比单线程更小心;另外 ex.map 默认按顺序返回,但内部是乱序完成的,日志里如果按完成顺序打时间戳,看起来会乱。
哪里会失效:小文件占多数时线程池收益很小,甚至因为调度开销变慢;机械盘上多线程读反而可能因为寻道变差;如果校验对象是网络挂载,块调太大容易把延迟放大。我自己的坑是第一次把 max_workers 设成 32,结果打开文件数超了 ulimit,脚本直接抛 Too many open files,后来改成 8 并加了 ulimit -n 的检查。
现在还在纠结的是:校验脚本到底该不该默认 sha256。安全上更稳,但每次全量跑的时间成本是实打实的。你们那边如果只是做变更检测、不防篡改,会继续用便宜的算法吗?
0 条评论
还没有评论,写下第一条。