Node.js vs FastAPI 事件循环:深入探讨异步并发

发布: (2026年3月5日 GMT+8 05:34)
9 分钟阅读
原文: Dev.to

Source: Dev.to

生产环境中的 API 堆栈

两个堆栈在现代生产系统中反复出现:

  • Node.js
  • FastAPI

两者都因处理高并发工作负载而广为人知,并且在很大程度上依赖事件驱动架构。

快速对比

功能Node.jsFastAPI
并发模型单线程事件循环 + 非阻塞 I/O异步协程 + 多工作进程
典型运行时JavaScript (V8)Python (asyncio)
跨核心扩展需要 cluster 模式通过多进程自然实现
最佳适用场景I/O 密集型 API、实时应用、WebSocketAPI、机器学习推理后端、流式处理、微服务
潜在瓶颈CPU 密集型工作会阻塞事件循环通过使用独立进程避免 GIL

Node.js

核心理念

  • 单线程 上执行 JavaScript(与浏览器模型相同)。
  • I/O 操作交由 libuv 处理,这是一个高性能的 C 库。
  • 当 I/O 完成后,回调会被推回事件循环。

这种设计在 I/O 密集型工作负载下表现极佳,而这正是大多数 Web 服务器的主要场景。
代价是:CPU 密集型工作会阻塞 JavaScript 线程,从而拖慢整个服务器。

重要特性

  • 所有 JavaScript 代码都在同一个线程上运行。
  • I/O → 交由 libuv 处理(网络、文件系统、计时器、线程池调度)。
  • 事件循环阶段
阶段发生的事情
Timers执行通过 setTimeout() / setInterval() 计划的回调。
Pending Callbacks处理系统级回调(例如 TCP 错误)。
Poll循环的 核心 – 等待 I/O 事件,获取已完成的操作,运行回调。
Check执行 setImmediate() 回调。
Close Callbacks处理清理事件(例如 socket.destroy())。

线程池(libuv)

  • 负责文件系统访问、DNS 查询、压缩、加密等任务。
  • 默认大小: 4 个线程(可通过 UV_THREADPOOL_SIZE 配置)。
  • JavaScript 的执行仍然是单线程的 – 重 CPU 任务仍会阻塞循环。

简单示例(Node.js)

// Callback style (old)
db.query(sql, (err, result) => {
  if (err) throw err;
  console.log(result);
});

// Promise / async‑await (modern)
const result = await db.query(sql);
console.log(result);

即使使用 async/await,Node 仍然是通过回调队列调度操作的;该语法只是对 Promise 和回调的语法糖。

Source:

FastAPI

核心理念

  • FastAPI 只是一个框架;运行时来自 ASGI 服务器,例如:
    • uvicorn
    • hypercorn
    • gunicorn + uvicorn workers
  • 每个 worker 是:
    • 一个独立的操作系统进程
    • 它自己的 Python 解释器
    • 它自己的事件循环

因此并发来源于 两层

  1. 每个 worker 内部的 异步协程
  2. 跨 CPU 核心的 多个 worker 进程

典型端点

from fastapi import FastAPI

app = FastAPI()

@app.get("/users")
async def get_users():
    result = await db.fetch_all()
    return result
  • Python 会创建一个 协程对象(可暂停的计算)。
  • 事件循环 调度恢复 协程,根据需要执行。
  • 与回调地狱相比,这提供了 线性、可读 的编程模型。

回调 vs 协程模型

方面Node.js(回调)FastAPI(协程)
语法db.query(sql, (err, result) => {...})result = await db.query()
流程嵌套回调 → “回调地狱”(可通过 Promise/async‑await 缓解)可暂停函数 → 自然流程
调度方式回调队列(事件循环)协程的协作调度

基于进程的并行

  • FastAPI 部署 通常运行 多个 worker 进程
    • 每个 worker → 1 个 OS 进程1 个 Python 解释器1 个事件循环
  • 这实现了跨 CPU 核心的 真正并行,规避了 Python 的全局解释器锁(GIL),因为每个进程都有自己的解释器。
Worker 1 → CPU core 1
Worker 2 → CPU core 2
...

并发策略概述

Node.js 并发模型

  • 单线程事件循环 + 非阻塞 I/O
  • 擅长:
    • APIs
    • 流媒体服务
    • 实时应用(WebSockets、聊天服务器、实时仪表盘)
  • 优势
    • 高 I/O 吞吐量
    • 实时系统
    • 巨大的生态系统与成熟的工具链
  • 劣势
    • CPU 密集型任务会阻塞循环(例如,大型 JSON 解析、图像处理、加密、机器学习推理)
    • 需要 worker 线程 或独立的微服务来处理重 CPU 工作。

FastAPI 并发模型

  • 异步协程 + 多工作进程
  • 即使某个工作进程被 CPU 工作阻塞,其他进程仍可继续提供服务,从而实现响应式处理。
  • 优势
    • 与机器学习框架无缝集成
    • 通过 Pydantic 提供强类型提示和自动验证
    • 出色的开发者体验
    • 对 I/O 密集型和 CPU 密集型工作负载均表现优异(通过进程扩展)
  • 常见误解:FastAPI 并不会自动将 所有 内容异步化;必须显式使用 async def 并在 I/O‑bound 调用前使用 await

要点

  • Node.js 在工作负载为 I/O‑密集 且能够保持 JavaScript 执行轻量时表现出色。
  • FastAPI混合工作负载 提供了更灵活的模型,利用 Python 丰富的生态系统和进程级并行。
  • 选择与您的 性能特征团队专长运营约束 相匹配的技术栈。

澄清 FastAPI 中的异步端点

声明: “那不是真的。”

实际情况

def endpoint():
    ...
  • 不是 异步的。
  • FastAPI 会在 线程池执行器 中运行它。

结果:

阻塞函数 → 在线程池中执行 → 事件循环保持空闲

若要直接使用事件循环,端点 必须 声明为:

async def endpoint():
    ...

理解全栈层

Node.js

V8 Engine → Node.js Runtime → libuv → Operating System
  • V8 执行 JavaScript。
  • libuv 处理异步 I/O。

FastAPI

Python Interpreter → FastAPI Framework → ASGI → Uvicorn Server → asyncio / uvloop → Operating System
  • 许多 FastAPI 部署使用 uvloop,这是一种用 C 编写的事件循环实现。
  • uvloop 本身是基于 libuv 构建的——与 Node.js 使用的同一库。

产生的等价关系

FastAPI + uvloop  =  Python + libuv

不同的语言,却使用相同的底层异步引擎。

并发哲学

Node.js 和 FastAPI 均遵循相同的基本哲学:

  • 事件驱动、非阻塞 I/O

但它们在并发扩展方式上有所不同。

FeatureNode.jsFastAPI
Execution modelSingle JavaScript threadasyncio coroutines
I/O handlinglibuv thread poolasyncio / uvloop
ParallelismCluster mode or worker threadsMultiple worker processes
Multi‑core scalabilityRequires explicit clusteringNatural multi‑core support

实用指南

  • 使用 Node.js 当构建实时系统并且希望保持在 JavaScript 生态系统中时。
  • 使用 FastAPI 当围绕 Python、数据科学、机器学习或 AI 工作负载构建服务时。

关键要点: 了解每种并发模型在负载下的表现至关重要,因为早期的架构决策会决定系统后续的可扩展性。

您的想法

您更喜欢哪种运行时用于高并发 API,为什么?

进一步阅读

如果您喜欢这篇文章,可以在此阅读我的更多博客:

https://writings.dipchakraborty.com

0 浏览
Back to Blog

相关文章

阅读更多 »

停止从零开始构建 API 仪表板

每个 API 开发者都有过这种经历。你发布了一个 API,有人开始使用它,接着问题就接踵而至: - “我们收到了多少请求?” - “谁是我们的 heavie……”