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
赞同度 100% · 0 条评论 · 1 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。