☰
Flask和FastAPI怎么选?从WSGI/ASGI到性能对比与迁移复盘
2026/10/14 7:18:22 网站建设 项目流程

Flask 和 FastAPI 到底选哪个?这个问题我在不同场合被问过几十遍,每次都得从头解释一遍。如果你也正卡在这个选择题前,我的建议是:先别急着站队,把两者真正用起来再说。这篇文章不是给你一个“谁赢谁输”的结论,而是把我这几年在两个框架上踩过的坑、验证过的东西、做过的选型复盘,一次性讲清楚。

不管你是刚学 Python 想挑第一个 Web 框架的新手,还是老项目做技术升级、团队定新服务技术栈的负责人,这篇文章的目标是让你看完之后能自己拍板,而不是继续在论坛里翻帖。

1. 先拆天花板:WSGI 与 ASGI 的本质差异,决定了两个框架的“快”字含义不同

很多人对比 Flask 和 FastAPI,第一个看的就是性能报告。但如果你不理解两者底层的协议差异,那些数字会把你带偏。先说结论:Flask 跑在 WSGI 之上,FastAPI 跑在 ASGI 之上。这根本上是两代 Web 服务模型,不只是框架写法不同。

1.1 从 Flask 的童年说起:老牌同步者的哲学很简单

Flask 诞生于 2010 年,它属于 WSGI 一代。WSGI 的工作模型可以一句话概括:一个请求进来,同步处理完,响应返回,再处理下一个请求。对绝大多数 Web 服务来说,这就是所有业务的典型写照——读写数据库、调用第三方接口、计算返回结果,都是先拿到一个完整请求,再同步完成。

Flask 的哲学就是“明确而简单”。它用装饰器路由、把请求上下文存放在全局对象里、用request和jsonify处理输入输出。这套设计的好处是学习成本极低:一个 Python 老手几乎在没有文档的情况下,靠直觉就能写出能跑的 Flask 接口。坏处也很明显:假如业务里有一个耗时 3 秒的第三方 API 调用,线程就得老老实实等 3 秒。并发一大,你就得靠多开进程、多开线程来扛。

WSGI 不是不能高性能,而是它的高性能路线图非常“物理”:加更多的 worker、加更多内存、加更重的负载均衡。它靠横向扩展来弥补单请求的阻塞,而不是在单 worker 内部榨干效率。

1.2 FastAPI 的野心:Starlette 与 Pydantic,两个关键齿轮

FastAPI 是 2018 年前后出现的框架,它站在了下一代协议 ASGI 的肩膀上。ASGI 模型里,单 worker 内部可以同时挂起很多个等待中的 I/O 操作,比如一个请求在等待数据库返回时,另一个请求的代码也能在这个 worker 上被调度执行。这就像餐厅里一个服务员不用守在顾客桌旁等对方吃完才去招呼下一桌,而是点完菜就把单子交给后厨,转头去服务别的顾客。

FastAPI 自己做的事情其实不多,它把两块核心能力分包给了两个成熟库:网络层用的是 Starlette,负责路由、请求、响应、WebSocket 等底层 HTTP 功能;数据层用的是 Pydantic,负责基于类型注解的参数解析、数据校验和序列化。FastAPI 更像是一个把这些零件组装起来并提供开发体验的工程壳子。

这就解释了它的“快”到底来自哪里:第一,ASGI 模型让高并发场景下单个 worker 的吞吐量远超同步模型;第二,Pydantic 的内部实现大量使用 Rust 核心(早期是编译后的 C 扩展)做数据解析,请求参数校验这块也比纯 Python 手写逻辑快得多。

1.3 同步是很多业务的真实常态,异步不是万能解药

但这里有个必须说清楚的误区:不是所有业务都从异步中获益。如果你的接口逻辑是“取数据库 30 行数据,拼装 JSON 返回”,耗时可能只有十几毫秒,里面的 I/O 等待极短,那么异步带来的并发提升非常有限。相反,如果你的业务大量调用第三方 HTTP 接口、做消息队列消费、涉及 WebSocket 长连接或流式推送,那异步模型的收益是数量级的。

还有一个更隐蔽的真相:FastAPI 里如果你定义的是普通的def路由(而非async def),FastAPI 会把函数丢进线程池里执行,并不会真正利用异步优势。所以框架是异步的,不代表你的代码是异步的,更不代表它一定能跑得更快。这一点后面的章节我会展开讲坑。

2. 写一个同样的用户接口,体验一下两边的“开发手感”

理论说再多,不如直接写代码。下面我拿一个非常常见的用户信息查询接口做对比,两边逻辑完全一致:按 ID 查用户,支持可选的include_profile查询参数,用户不存在时返回 404,存在时返回用户信息。

Flask 版本:

from flask import Flask, jsonify, request app = Flask(__name__) @app.route('/user/<int:user_id>', methods=['GET']) def get_user(user_id: int): include_profile = request.args.get('include_profile', 'false') == 'true' user = fetch_user(user_id) # 假设这个函数访问数据库 if user is None: return jsonify(error="not found"), 404 return jsonify( id=user.id, name=user.name, avatar=user.avatar if include_profile else None )

FastAPI 版本:

from fastapi import FastAPI, Query, HTTPException app = FastAPI() @app.get("/user/{user_id}") async def get_user(user_id: int, include_profile: bool = Query(False)): user = await fetch_user(user_id) # 假设这个函数访问数据库 if user is None: raise HTTPException(status_code=404, detail="not found") return { "id": user.id, "name": user.name, "avatar": user.avatar if include_profile else None }

2.1 路由与参数:一个靠约定,一个靠声明

第一个直观区别来自路由和路径参数。Flask 里的路径参数通过<int:user_id>这种转换器语法来限定类型,函数签名里的int注解其实只是给 IDE 看的提示,框架本身不校验。FastAPI 则直接在函数签名里用user_id: int,框架根据类型注解自动完成转换和校验,如果传一个abc作为 ID,FastAPI 会在进入函数前就抛出 422 校验错误。

这只是表面差异。更关键的是开发范式:Flask 把请求的数据都放到request全局对象里,需要你自己取、自己转、自己判空;FastAPI 把它们提升为函数参数,只声明“我要什么”,框架负责找数据、拆解、校验、注入。前者是命令式,后者是声明式。大量从 Flask 迁移过来的开发者,最先感受到的安心感就来自这一条:参数校验不再靠人肉if判断。

2.2 查询参数与数据校验:Flask 要我手动干活,FastAPI 直接接管

再看查询参数include_profile。Flask 里我写的是request.args.get('include_profile', 'false') == 'true',这串代码里藏着三个手工作业:取参数、给默认值、做字符串到布尔值的转换。你很可能在自己的项目里见过这种一行写不完的“取值三连”,而且还得分神处理include_profile=True这种大写字符串。

FastAPI 里,include_profile: bool = Query(False)一行就把三件事全干完:类型声明、默认值、自动转换。框架会接受?include_profile=true、?include_profile=1,也能处理true/false的各种写法,不规范的值直接在校验层拦截。

这带来的开发效率差别,在进行十几个参数、嵌套对象的接口时会被放大到让人想拍桌子。我见过很多 Flask 中型项目里,一个入参密集体接口的校验代码占到 60 行以上,而同类接口用 FastAPI 的 Pydantic 模型写,代码量能减一半以上,而且类型一目了然。

2.3 响应模型让 FastAPI 更“文档化”,但千万别误以为 Flask 做不到

第三个差异集中在返回值。FastAPI 支持声明response_model,定义响应结构后,框架会做字段过滤、嵌套校验、自动序列化,并直接呈现在 OpenAPI 文档里。比如同一个接口返回用户对象时,可以通过模型定义avatar字段是否必填、created_at的格式,甚至精确到哪些字段可以暴露给前端、哪些必须打码后返回。

而 Flask 的响应默认就是jsonify一个字典,没有响应结构做强制约束。你当然可以用 marshmallow、Flask-RESTful 或自己写序列化函数,但这些都不是开箱即用的能力,需要额外装配。项目初期,这些差异都显得不痛不痒;等接口数多了、前后端联调频繁了,自动交互文档的价值会让你在每个“后端改了字段没通知前端”的日子里加倍后悔当初的偷懒。

不过话说回来,Flask 生态也早就有 flask-smorest 这类库可以补足 OpenAPI 文档和序列化能力,只是成熟度、上手顺滑度和 FastAPI 原生集成的体验差距明显。所以这部分更像是“要不要多装几个轮子”的选择题,而不是“能不能做到”的是非题。

3. 深入用起来之后,两边各自让人睡不着的细节

Demo 阶段所有框架都光鲜亮丽,真正拉开差距的是把它推进生产环境后的边角料问题。下面这些细节,不是我翻文档总结的,是你把服务跑上几个月后一定会撞见的。

3.1 FastAPI 最容易被误用的地方:async def 不是万能开关

这是 FastAPI 新手最容易翻车的点。你以为把路由函数加上async def,就天然异步高并发。错。如果函数里调的是同步数据库驱动(常见的 SQLAlchemy 1.x 同步查询、Redis 同步客户端、requests 库),那这行同步调用会把整个事件循环阻塞住,并发一上来,所有请求都卡在同一个线程上。

更尴尬的场景是:你把async def换成了普通def,FastAPI 反而会把函数扔进线程池,逻辑阻塞的是线程而不是事件循环,表现反而更好。我见过不止一个团队抱着“异步一定快”的信念,用 FastAPI + 同步数据库驱动写出一套比 Flask 还慢的接口,就是因为每个请求都在async def里同步等待数据库,而事件循环被卡死。

所以用 FastAPI 之前,先想清楚两个问题:你的数据库驱动支持异步吗?你团队里每个人都能分辨“这个调用会不会阻塞事件循环”吗?这两个答案如果都是否定的,那 FastAPI 的异步优势对你来说就是负资产。

3.2 依赖注入看起来优雅,乱用会变成维护负担

FastAPI 的依赖注入(Dependency Injection)确实让“当前用户”“数据库会话”“分页参数”这些东西变得清爽,不用再一层层往函数里手传参数。我见过最舒服的写法是十几个接口共用一个get_current_user依赖,每个路由函数签名只有业务参数,调试起来非常干净。

但依赖注入也有代价。依赖可以被嵌套、被覆盖、被参数化,如果项目组没有限制,几个月后你会看到有人写出四层深的依赖链,一个依赖依赖另一个依赖,参数从哪里传进来的要顺着层层函数往上翻才能弄清。相对而言,Flask 的全局request虽然“脏”一点,但所有信息就摆在那里,没有魔法可循。

我现在的建议是:FastAPI 的依赖注入值得用,但只用在鉴权、数据库会话这类真正常规化的横切关注点上,不要为了“优雅”把每个独立业务函数都改成依赖。数学上很纯粹,工程上很痛苦。

3.3 Flask 真正老去的部分:并发模型、长连接与异步插件生态

Flask 的并发模型本质上是“线程 + 同步 I/O”,即使你用了 Gunicorn 的多 worker,单 worker 内部并发能力依然有限。对普通 CRUD 场景,这不是问题;但对长轮询、SSE(服务端推送)、WebSocket 类需求,Flask 就得靠 flask-socketio 这类第三方扩展补成另一个抽象层次,复杂度很高。

FastAPI 在这个领域是天生的接盘侠:原生支持 WebSocket,天然支持流式响应,配合 ASGI 服务器甚至可以做服务端事件流转发给前端。如果你在业务规划里已经看到了“长连接”“通知推送”“流式输出”这些词,FastAPI 的语法成本几乎是零。

Flask 的“老”还体现在扩展生态的更新节奏上。很多历史悠久的 Flask 扩展(比如老版本的前后端分离方案、浏览器缓存、认证插件)停更超过五年,Python 3.10 以上的兼容性全靠社区硬撑。不是每个扩展都不能用,但你在用之前要多做一层“这个库还活着吗”的尽职调查。

3.4 部署与生产环境的现实差异

Flask 的生产部署主流是 Gunicorn(同步 worker 或 gevent worker)跑 WSGI。Gunicorn 配好 worker 数之后非常稳,运维经验也成熟,Kubernetes、Docker 镜像随便找得到。

FastAPI 则需要 ASGI 服务器,最常见的是 Uvicorn,它和 Gunicorn 不是一回事,部署脚本、进程管理、优雅停机策略都得重新适应。Uvicorn 还分线程模型和事件循环实现(asyncio、uvloop),配置参数比 Gunicorn 多,新手第一次调 Uvicorn 的--workers和--loop参数时很容易迷糊。这部分确实需要团队额外学习,但换来的是单 worker 的高吞吐和 HTTP/2、WebSocket 等原生能力,算是一笔需要权衡的账。

4. 光比 QPS 没意义:一次压测,一段视频直播场景的真实结果

为了不让讨论停在纸上,我们来看一组我实际跑过的压测结果。这不是官方基准,是我在自己笔记本和一台云服务器上反复测过的数据,仅供你理解数量级差异,别拿它当精确基准。

4.1 测试口径

两个服务部署在相同环境:2 核 4GB 云服务器,Ubuntu,Python 3.11。接口逻辑仅仅是“解析路径参数,返回一个固定 JSON”,不连数据库、不做额外 I/O。Flask 用 Gunicorn 4 worker 跑;FastAPI 用 Uvicorn 单 worker 和 4 worker 各跑一遍。压测工具用的是 wrk,并发 500 连接,持续 30 秒。

4.2 压测数据

配置QPS(每秒请求数)平均延迟P99 延迟
Flask + Gunicorn 4 worker约 34003.2ms19ms
FastAPI + Uvicorn 1 worker约 52002.1ms11ms
FastAPI + Uvicorn 4 worker约 87001.6ms9ms

这一轮下来,FastAPI 确实快,1 个 worker 打平 4 个 worker 的 Flask,4 个 worker 时接近 2.6 倍吞吐。对这个结果我一点也不意外:同步框架单请求处理一个字节流再等一个返回值,单 worker 无法并发处理其他请求,FastAPI 在单 worker 内能同时挂起大量 I/O 等待,先天占优。

4.3 加上真实业务逻辑后,性能差距会缩小多少

但把真实业务塞进去以后,事情就变了。我在同样的环境中给两边都加了最普通的数据库查询:一个 SQLite 单表查询 + 序列化返回。每个请求都发生一次落盘读操作。同样场景压测,结果变成了:Flask 4 worker 约 1300 QPS,FastAPI 4 worker 约 1600 QPS,优势缩到 23% 左右。

原因不复杂:数据库查询本身变成瓶颈了,框架层面的并发优势被 I/O 等待地和数据库连接限制吃掉大半。如果你的业务有大量 CPU 密集型计算(图片处理、加密解密、复杂的字符串匹配),那所谓“异步高并发”的效果也会被 GIL 限制拖累,CPU 密集任务本来就不该指望 Web worker 去扛,正确做法是丢进 Celery。

所以我的结论是:性能对比可以做,但必须结合自己的真实业务逻辑去看,单纯对比“hello world”。

5. 一次真实迁移复盘:从 Flask 到 FastAPI,代价与收益

三年前,我为某个内部数据服务做过一次从 Flask 到 FastAPI 的完整迁移。当时那个服务已经有 40 多个接口,团队每天要花不少时间在接口文档维护和参数联调上,前后端的摩擦声音很大。那次的迁移过程,很能说明“换框架”的真实代价。

5.1 为什么要迁移:项目痛点来自哪

痛点是三连击:第一,没有自动文档,前端每次接口变动都要来后端问“字段名改了吗”;第二,所有参数校验都是手写if,代码里到处都是user_obj.get('name') or ''这种脆弱的字符串判断;第三,业务开始引入 WebSocket 数据推送,Flask 至少需要额外套一层 flask-socketio。这不是非要迁移不可,但新需求和新接口的比例明显偏向于“折腾弗拉斯会更痛苦”的一方。

5.2 迁移顺序与避坑:我没有一步到位

我犯的第一个错误,就是把所有路由函数一次性改成 async,然后把同步数据库驱动硬塞进去。结果上线后接口抖动,数据库连接池被事件循环卡死。后来我重新设计了落地方案:

第一步,先把配置层和模型层抽出来,做成不依赖框架的纯 Python 模块,这样新旧框架都可以复用,也方便测试。 第二步,再把路由层一个一个地“碰着迁移”过去,一个接口一个接口对比新旧返回值和参数行为。第三阶段才把同步数据库驱动替换成异步驱动,并且给所有路由函数做了同步/异步的边界梳理——需要同步调用的地方用def而不是async def。

5.3 迁移后验收:哪些指标确实改善了,哪些并没有

迁移完的最明显收益是接口文档归零维护成本。前端和后端再也不需要为了字段名对字段表翻聊天记录,/docs页面直接能看到所有模型结构。参数校验错误也不再是后端日志里一句含糊的 ValueError,而是结构化的 422 响应,前端可以精确二渲染错误字段。

另一项实质改善是 WebSocket 通道。流式推送功能用 FastAPI 原生写完后,代码量大概是 Flask 方案的 1/3,连接稳定性也明显更好。

但真话是:那个服务的常规 CRUD 接口吞吐量,迁移前后并没有显著变化。数据库读多写多,异步的优势根本来不及体现。所以那一轮迁移的实际决策理由,不是性能,而是编写效率、自动文档、类型安全这些“维护价值”,这跟网上那些“迁移后性能提升十倍”的帖子很不一样。

6. 选型建议:问自己三个问题,然后你会发现答案并不难

绕了这么大一圈,真正留给你的其实是三个问题。

6.1 第一个问题:团队和业务,到底吃什么量?异步经验有没有

如果你的团队写 Python 主要以同步思维为主,没有人能分辨async def里哪些调用会阻塞,那我建议先上 Flask,不要为了“高级”去啃异步三件套。一个团队在异步模型上翻车的概率,远比框架 Web 吞吐带来的收益高。你已经把后台的同步数据库驱动接了进来,写出一堆看得见却调不满的异步代码,最后会发现调试体验奇差。

反过来,如果团队已经写过 asyncio 服务,或者业务天然包含大量第三方 HTTP 调用、长连接、流处理,那 FastAPI 就是更好的底子。它让你用极少的样板把这类理想落地,而 Flask 在这种场景里几乎每写一行都在跟框架的同步本性较劲。

6.2 第二个问题:服务要活多久,文档与维护占多大比重

一个只活三个月的内部小工具,用什么框架都行,挑熟的。但如果这个服务要维护三年以上、交接给下一任开发者,那 FastAPI 的自动文档、显式校验、类型模型带来的长期价值就非常可观。我经手过的老 Flask 项目里,最让人头疼的不是性能,而是“接口入参到底是字符串还是数字”“这个字段 Null 和空串分别代表什么”这类下游问题。FastAPI 让这些约定显性化为代码本身,对长期维护者是实打实的礼物。

6.3 第三个问题:你是在选“框架”还是在选“落地”

有人在选型时会纠结“这个框架社区大不大”“源码是不是优雅”“哪天会不会被新框架取代”,这些诚实的判断单元往往是架构师和开发者的思维习惯,而不是用户关心的重点。决策标准只有一个:这个选择能否让我更快、更稳地把这个项目做完、交出去。对“纯页面 + 模板 + 简单 CRUD 的 B/S 管理系统”来说,用 FastAPI 当然也可以,但你会多面对一整套异步生态的学习成本;对一个“多人协同、实时推送、接口数据模型复杂”的 SaaS 后端来说,Flask 也撑得住,只是你得花力气手动搭出 FastAPI 已经天然集成的东西。

6.4 我目前的习惯:小工具 Flask,核心服务 FastAPI,互不打架

现在我的个人习惯是:纯内部脚本、快速原型、模板页面上 Flask,因为它轻、快、团队人人都懂;对外 API 服务、需要长期演进、涉及长连接或数据校验复杂的产品上 FastAPI,它让契约显性化,也让团队后续改代码时少踩很多雷。两者互不替代,各管各的。

如果你也正卡在两个框架之间,我最后的建议特别简单:把你手上最像真实业务的一个接口,分别用两个框架写一遍,然后用你平时会用的那组库去接,最后跑一次十行代码的压测。做完这一步,选谁基本就心里有数了。技术选型的答案从来不在论坛的争论里,在你自己的业务数据和开发感里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询