用 Node 的 argon2 做密码哈希,我把并发数调错了

前段时间把一个老项目的密码哈希从 bcrypt 换到 argon2,用的是 argon2 这个 npm 包,Node 20.11,Ubuntu 22.04,4 核 8G 的机器。上线前压测,登录接口 QPS 一上去,整个进程的事件循环延迟直接从 8ms 飙到 400ms 以上,健康检查开始超时。

先说配置。我一开始照着文档默认值来:

const argon2 = require('argon2');

const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 65536,   // 64MB
  timeCost: 3,
  parallelism: 4,
});

问题出在 parallelism: 4。argon2 内部会开 4 条线程算一个哈希,而 argon2 包的 hash 是异步的、底层走 libuv 线程池。线程池默认只有 4 个,一个哈希就占满,别的异步任务(包括 DNS 解析、文件读取)全在排队。压测时 10 个并发登录,等于 40 份计算量挤 4 个槽位,事件循环没被阻塞,但线程池被独占,表现就是「进程没死,什么都慢」。

我的改法是两步。第一,把 parallelism 降到 1,把强度放到 memoryCost 和 timeCost 上;第二,显式调大线程池,并且给哈希单独限流。

process.env.UV_THREADPOOL_SIZE = '8';

const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 65536,
  timeCost: 3,
  parallelism: 1,
});

UV_THREADPOOL_SIZE 必须在进程启动前设置,写在启动命令里最稳:

UV_THREADPOOL_SIZE=8 node server.js

为什么这样取舍:parallelism 提高的是单次哈希的墙钟速度,但代价是瞬时占用的线程数成倍上涨;服务端的瓶颈从来不是单次哈希慢 30ms,而是并发时别把公共资源吃干净。memoryCost 64MB 对 8G 机器、并发 10 以内是能接受的,但如果你的容器内存限制是 512MB,这个值就要往下压。

哪里会失效:一是把 UV_THREADPOOL_SIZE 设成 8 之后,同一时间最多只能跑 8 个哈希,第 9 个请求还是在等,所以我在登录入口加了一个并发闸门(用 p-limit 之类),超过 20 个直接返回 429,而不是让它排到超时;二是 memoryCost 是每个哈希独立分配的,10 个并发就是 640MB,容器里很容易被 OOM kill,这个我踩过一次,进程直接退出,日志里只有一行 Killed;三是 argon2 的哈希结果和参数绑定,改参数后老密码照样能验证(参数存在哈希串里),但新老混用会让压测数据失真,我一开始就被这个骗了。

还有一个小事:argon2.verify 失败时抛的是异常还是返回 false,不同版本行为不一致,我在 0.30 上遇到过参数格式错误直接 reject,最好包一层 try/catch,别只判断返回值。

你们线上跑 argon2 时,memoryCost 和并发闸门一般定在多少?我这边 4 核机器现在压到 32MB、闸门 16,还在观察。

0
赞同度 100% · 0 条评论 · 1 阅读

0 条评论

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

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