PAYATHON 2026

FastAPI 高负载登录如何处理 bcrypt/argon2 阻塞与扩展瓶颈?

数据小陈

可以使用线程池执行 bcrypt/argon2,主要是为了避免事件循环被阻塞。吞吐量仍取决于哈希成本和可用计算资源。通常应先限制哈希并发和请求积压,再根据压测结果配置 worker、CPU 和内存。

增加进程后有效、增加线程后无效,这一现象不足以单独证明瓶颈来自 GIL。缺少实际版本、CPU 配额和性能数据时,也无法断定必须改用进程池。

为什么使用 to_thread 后仍然遇到瓶颈?

问题中的代码:

await asyncio.to_thread(bcrypt.hashpw, password, salt)

这行代码把同步计算交给工作线程,让事件循环可以继续处理其他协程,但不会减少单次哈希的计算量。并发请求超过机器的处理能力后,请求会排队,尾延迟也会升高。

对于 CPU 密集型操作,线程能否利用多个核心,取决于扩展库是否在计算期间释放 GIL。Python 官方文档说明,释放 GIL 的扩展可以通过 asyncio.to_thread() 执行 CPU 密集型任务。Python 文档

不能仅凭扩展使用 C 实现,就推断它能在线程中并行,因为实现语言本身不能保证释放 GIL。另外,当前 pyca/bcrypt 的核心实现使用 Rust,其源码在主要哈希计算期间调用 py.detach(...),允许计算脱离 GIL 执行。实际部署时仍需核对安装版本及调用链。bcrypt 源码

当有效 CPU 资源已经用满时,继续增加线程通常只会加剧调度竞争。Argon2 还可能先受到内存容量或内存带宽限制。

怎样判断该增加线程还是 worker?

先分别测量密码哈希和完整 HTTP 请求:

  1. 固定密码、哈希参数和依赖版本,测量单次生成和验证的耗时。
  2. 在相同 CPU 和内存配额下,逐步提高线程并发,记录吞吐量、CPU 使用率和延迟。
  3. 再比较不同 worker 数量,并保持总哈希并发接近,避免将并发额度增加带来的改善误判为进程本身的优势。
  4. 最后压测完整登录链路,分别记录数据库查询、等待哈希执行、实际计算和响应生成的耗时。

重点检查以下情况:

  • 所有可用核心都已繁忙:考虑增加计算资源、减少无效登录请求,或在安全要求允许的范围内评估哈希参数。
  • 只有一个核心繁忙:检查 Python 层处理、锁和扩展库行为,以及压测客户端是否先达到上限。
  • 宿主机有空闲但应用吞吐量不变:检查容器 CPU quota、CPU affinity 和 CPU throttling。
  • 增加 worker 后改善明显:可能是多个事件循环分担了 Python 层工作,也可能是每个进程各自增加了线程池或连接池容量,需要进一步区分。

部署多个 worker 是正常的选择,但同一台机器的 CPU 已饱和时,增加进程也无法持续提高吞吐量。如果哈希库已经能释放 GIL,也不必仅因操作是 CPU 密集型就改用 ProcessPoolExecutor。

FastAPI 中如何限制哈希并发?

可以继续使用独立的 ThreadPoolExecutor。在 FastAPI 项目中,也可以使用 AnyIO 的 to_thread.run_sync(),配合专用的 CapacityLimiter 为密码计算设置单独的并发预算。

下面只展示密码计算组件,省略数据库查询、限流和完整登录路由:

from contextlib import asynccontextmanager

import anyio
import bcrypt
from fastapi import FastAPI, Request


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 示例值,必须根据每个进程的资源预算压测调整。
    app.state.password_limiter = anyio.CapacityLimiter(2)
    yield


app = FastAPI(lifespan=lifespan)


def hash_password_sync(password: bytes, cost: int) -> bytes:
    return bcrypt.hashpw(password, bcrypt.gensalt(rounds=cost))


async def hash_password(
    request: Request, password: bytes, cost: int
) -> bytes:
    return await anyio.to_thread.run_sync(
        hash_password_sync,
        password,
        cost,
        limiter=request.app.state.password_limiter,
    )


async def verify_password(
    request: Request, password: bytes, stored_hash: bytes
) -> bool:
    return await anyio.to_thread.run_sync(
        bcrypt.checkpw,
        password,
        stored_hash,
        limiter=request.app.state.password_limiter,
    )

使用时要注意:

  • 2 是演示并发控制的示例值,不能直接视为生产配置。
  • limiter 在每个 worker 进程中独立存在,需要跨进程计算总并发。
  • 自定义 limiter 提供独立的并发预算,但不保证使用独占的 OS 线程池。
  • limiter 只限制正在执行的哈希数量,请求仍可能在等待时不断积压,因此还需要请求准入控制。
  • AnyIO 的线程限制和 asyncio 默认 executor 是两套机制,修改其中一个不会自动调整另一个。AnyIO 文档

登录时使用 bcrypt.checkpw(password, stored_hash) 验证已有哈希。注册或修改密码时,才生成随机 salt 并创建新哈希。验证仍需执行昂贵计算,使用 checkpw 并不会让它变成低成本操作。

如何避免高负载把服务拖垮?

应同时控制请求进入速度、正在计算的数量和允许等待的数量。

  • 在执行昂贵哈希之前,按账户、来源及全局容量实施限流。
  • 设置有限的等待队列或直接拒绝超出容量的请求,避免无限排队。
  • 区分限流和服务过载:前者可返回 429,后者可返回 503,并根据实际策略决定是否提供 Retry-After。
  • 多实例部署时,进程内计数只对本进程有效;全局限制需要共享状态或网关协调。
  • 同时覆盖登录、注册、修改密码和密码重置等会触发哈希计算的入口。

还要注意取消行为:请求超时或协程取消通常不能终止已经在线程中运行的哈希。如果请求一结束就假定 CPU 已释放,容量统计会偏低,后续请求仍会不断进入。AnyIO 默认在等待工作线程完成期间屏蔽其取消作用域的取消;启用 abandon_on_cancel=True 后,底层线程仍会继续运行。AnyIO 取消说明

独立认证服务和后台 worker 是否有帮助?

独立认证服务适合需要隔离资源、集中实施认证策略,或者让认证容量独立扩展的系统。它可以防止登录计算挤占其他业务资源,但密码计算成本依然存在,还需处理网络调用、超时及服务可用性。

后台任务队列适合处理不影响当前认证结果的后续工作。登录时必须先完成密码验证,再签发会话或令牌。如果请求将验证交给后台 worker 后仍要等待结果,整体容量问题依然存在,还会增加排队和通信成本。

对于常规应用,可以先在 Web worker 内受控地将计算卸载到线程,并在入口限流。只有压测表明资源隔离或独立扩展确有收益时,再拆分认证服务。

改用 Argon2 会更快吗?

Argon2id 可以作为密码存储方案评估,但不能保证比 bcrypt 更快。密码哈希的目标包括提高离线猜测成本,选择算法和设置参数时,需要同时考虑安全要求、延迟、内存和吞吐量。OWASP 推荐优先使用 Argon2id,并将 bcrypt 主要用于现有系统。OWASP 密码存储指南

Argon2 的主要参数包括:

参数含义高并发时的影响
time_cost计算迭代成本增大后通常需要更多计算时间
memory_cost每次计算的内存预算,argon2-cffi 使用 KiB决定可同时承载多少次计算
parallelism单次计算内部的并行度需要与外层请求并发共同评估

这些参数的含义见 argon2-cffi API 文档。不要把 parallelism 理解为允许同时登录的用户数。

例如,假设每次 Argon2 计算配置为 64 MiB,同时执行 16 次,仅算法工作内存就约需 1 GiB,还未计入应用、数据库连接及其他开销。多 worker 部署时,还需要累加所有进程的并发预算。

调参时,应在实际部署环境中测量目标并发下的 P95/P99 延迟、CPU、内存和吞吐量。单次计算最快的配置不一定能提供最高的整体吞吐量。argon2-cffi 参数选择指南

如果从 bcrypt 迁移到 Argon2id,可以在用户成功验证旧密码后生成新哈希并更新记录,同时把这次额外计算计入容量预算。不要直接改用 SHA-256 等快速哈希来提高登录吞吐量,它们不适合直接存储密码。OWASP 密码存储指南

备注:内容仅供参考。