FastAPI框架基本原理<上>

一、背景

        文档地址: https://fastapi.tiangolo.com/

        Github地址: https://github.com/fastapi/fastapi

        最近使用到FastAPI框架进行项目开发。 因为我们提供的相关RESTFUL API都是和LLM大模型相关的。 团队整体技术栈其实是Java,但是为什么选择Python呢?

        首先,我们项目初期的定位就是和LLM大模型紧密相关,在生态、私有化大模型等相关考虑下,Python还是这条赛道的主流编程语言,不排除后期可能和自研模型结合,如PyTorch、tensorflow、LangChain等等,Python的支持度还有成熟度都是有背书的。Java与AI结合,这块不是说不能做,但是确实不是太主流,后期如果切换成本太高。

        其次,我们的核心API接口没有那么多,Python的Web领域用得比较多的有Django、Flask, 这两者早期确实占据了Python Web领域的绝大部分市场, 一开始也有考虑到。 Django虽然很全,但是对于我们的需求,太重,没必要。 Flask虽然轻量,但是性能和后面调研的FastAPI相差太大。

         FastAPI这个框架主要是利用了Python的asyncio异步编程进行实现,通过协程并发的模型而不是多进程、多线程的并发模型。 在资源占用方面少,性能方面高,官方的说法是,性能可以媲美/接近Golang。

二、FastAPI的介绍

        FastAPI 是一个用于构建 API 的现代、快速(高性能)的 Web 框架,基于 Python 3.7+ 类型提示。它被设计为易于使用且具有强大的功能集,特别适合创建微服务或开发 RESTful APIs。

以下是 FastAPI 的一些关键特性:

  1. 高性能:根据官方文档,它的速度接近于 NodeJS 和 Go,在许多情况下,它比其他Python框架(如Flask或Django REST framework)更快。

  2. 快速编码:可以将编码时间减少约40%。这是因为其自动生成文档(支持OpenAPI和JSON Schema)以及自动完成和数据验证的功能。

  3. 少写代码:对于接收的数据进行校验和错误处理等功能,只需要定义好模型类,剩下的工作大部分都由 FastAPI 自动完成。

  4. 易于学习和使用:由于其简单直观的设计,即使是初学者也能快速上手。

  5. 安全:支持 OAuth2 和 JWT 等多种认证方式,内置了对常见安全问题的防护。

  6. 可测试性:提供了一个易于使用的测试客户端,类似于 Flask 的测试客户端,使得编写单元测试和集成测试变得容易。

  7. 异步支持:完全支持 async/await 语法,允许你编写异步的路径操作函数,这对于需要高并发的应用来说非常有用。

  8. 自动交互式API文档:提供了自动化的交互式API文档(Swagger UI 和 ReDoc),方便开发者测试和调试API。

  9. 广泛的社区支持和插件生态:虽然相对年轻,但发展迅速,并且与 Starlette 和 Pydantic 等库有良好的兼容性,这些库分别提供了 web 开发的基础和数据验证及设置管理的强大能力。

三、asyncio原理剖析与ASGI协议

1、asyncio原理剖析

        asyncio是官方内置模块,按照我的理解就是,提供了python协程的整体实现,包括协程调度器。 这个协程调度器就是event_loop(字面意思, 事件循环), 利用单线程对协程进行自动调度。 一旦遇到IO操作(如HTTP请求、连接操作数据库、连接操作Redis、读写文件等等), 就在等待IO的这个时间段,去调度执行其他非IO操作的协程, 等IO操作完毕的协程进入再次可以被调度的状态之后,再进行调度, 从而同一个时间内, 主线程的利用率变高, 不需要傻傻的等待IO操作而不去执行其它任务。

        基本执行原理图:

2、event_loop同步阻塞灾难

        协程调度带来了更高的并发效率,能够让遇到IO的时候无须等待这个IO执行完毕才能调度其他任务,而是充分利用这个时间间隙去执行其它非IO的代码,从而提高性能。

        但是这里要注意一个灾难性的问题!!!

        因为event_loop是单线程,那就意味着,如果你的IO操作必须也是支持异步的。否则如果你的主函数是async异步修饰,但是函数内部使用同步的库(不支持async、不支持协程调度的库)例如requests请求库, 这个http请求库,我们再熟悉不过了,但是requests是同步库,不支持被协程调度器event_loop调度。

        不支持被event_loop调度的同步库,会阻塞整个evet_loop线程,只要执行了这个同步代码,其它所有的协程都无法被调度,只能等待这个同步操作执行完毕才能被再次调度执行!!!!!

        这个就是同步灾难,等于整个event_loop只能干等着你的requests执行完毕,其它协程都处于等待状态了。 那么也就无法发挥出协程的威力, 甚至回退到比多线程、多进程更差的性能体验。 所以在使用异步编程的时候, 要考虑到,你使用到的第三方库、模块,必须也是支持异步的,才能发挥出协程的性能。

        这里做个类比就知道事情的严重性。  整个工厂就1条流水线在高速运转,停留1秒钟工厂都瞬损失严重,  但就因为你的操作是阻塞的(例如10几秒),OK, 那么这个流水线上其他人的事情也别做了, 等你的操作完毕, 流水线才恢复正常! 这你想,老板不得干死你....

        所以,使用异步框架编程,要十分小心同步灾难,写代码特别是用到第三方库,考虑第三方库要支持异步操作。 与单线程操作redis类似, 你的耗时操作会直接影响整个redis。

        那如果还真的无法避免遇到第三方库不支持异步的情况呢? 整个项目就黄了? 不慌,下一章会讲解这种情况怎么处理。

3、同步编程与异步编程的对比

          传统顺序执行, 总共耗时4秒。 首先http_request(1)函数执行完毕,才能执行第二个函数http_request(2), 一个函数执行耗时大概2s, 那么2s+2s = 4s

import time


def http_request(user_id):
    print(f"{user_id} start....")
    time.sleep(2)
    print(f"{user_id} end....")

def main():
    t1 = time.time()
    http_request(1)
    http_request(2)
    t2 = time.time()
    print(f"mian()耗时: {t2-t1}")

if __name__ == "__main__":
    main()

 

        利用异步编程方式,同时把2个协程任务提交给event_loop,并发执行。 event_loop遇到asyncio.sleep(2)[模拟遇到IO操作], 那么趁着IO等待的这个时间间隙,会先去调度别的非IO协程进行执行, 所以2个协程的asyncio.sleep(2) 这段代码几乎是同时执行的, 那么到最后结束也是耗时2s.

        因为2s时间是一起sleep的,而不是像同步代码,前面sleep(2)结束后, 后面任务再slee(2)导致耗时增加。

        这里异步的方式,最后总耗时2s。  这不就是性能得到极大提升了嘛。 如果这个任务是1000个, 那么是不是也是2s左右就完成了,但是如果是传统同步方式, 1000*2 = 2000s才能完成1000个任务, 差距不是一般的大。

import asyncio
import time


async def http_request(user_id):
    print(f"{user_id} start....")
    # 这里模拟IO耗时, 例如HTTP请求,查询mysql、读写文件等等
    await asyncio.sleep(2)
    print(f"{user_id} end....")

async def main():
    t1 = time.time()
    await asyncio.gather(http_request(1), http_request(2))
    t2 = time.time()
    print(f"mian()耗时: {t2-t1}")

if __name__ == "__main__":
    asyncio.run(main())

4、同步、异步的解释

        以前很多人理解同步、异步其实都有过混淆的或者理解不清楚的地方。 有部分的理解就很疑惑, 例如我正常代码逻辑就是1、2、3、4这几个步骤往下执行, 例如第2步代码依赖第1步,第3步依赖第2步,第4步依赖第3步如此才能完成我的正确业务逻辑✅。

        我在第2步,例如执行await 之后,会不会影响我后续的代码逻辑执行?   完全不影响。 我们不要理解错了, await提交异步任务给event_loop只是让它趁着这个IO时间间隙去调度其它非IO任务,等你的IO任务执行完毕,又被调度到了之后,代码3、4步骤还是会得到执行的,不会影响你的代码逻辑执行顺序。

        还有说一下后面提到的FastAPI和ASGI协议,  每个请求过来都是执行了一个async 协程函数, 意味着每个客户端的一次请求就对应执行一个协程,  协程的内部代码逻辑会正常执行, 只是这些协程不会相互影响, 不会出现阻塞的情况。  协程1(请求1) 、协程2(请求2)  都可以同时被event_loop调度执行, 而不是等协程1(请求1)执行完毕,才能处理协程2(请求2), 所以并发能力得到提升。

5、WSGI与ASGI协议

1、WSGI协议

        WSGI 是一种规范,它描述了web服务器如何与web应用程序交互以及web应用程序如何处理请求。它旨在促进web框架和web服务器之间的可移植性。这意味着你可以在不同的WSGI服务器(如Gunicorn、uWSGI)上运行任何符合WSGI标准的Python web应用(如Flask、Django),而无需修改代码。

        WSGI、uWSGI大家肯定比较熟悉的。 本质就是每个HTTP请求对应的就是一个进程处理或者一个线程处理, 整体并发模型是多进程、多线程。  每个HTTP请求过来执行入口函数, 入口函数签名如下:

def application(environ, start_response):
    # 处理请求并返回响应
  • environ:这是一个字典,包含了所有由web服务器提供的关于HTTP请求的信息。这些信息包括请求方法(GET、POST等)、路径、查询字符串、请求头以及其它环境变量。

  • start_response:这是一个可调用对象(callable),用于开始HTTP响应。它接受两个必需的参数:

    • 状态码(status):一个字符串,表示HTTP响应的状态码和原因短语,如 "200 OK" 或 "404 Not Found"
    • 响应头(response_headers):一个包含元组的列表,每个元组代表一个HTTP头部及其值,例如 [('Content-Type', 'text/html')]

        该函数应该返回一个可迭代的对象,其中每个元素都是HTTP响应体的一部分。通常情况下,这只是一个包含整个响应体的单个字符串的列表。

        无论你是用Django、Flask或者其它Python HTTP框架, 是的就是这么简单, 就是一个函数入口, 至于你说的框架结构啊、路由注册啊等等,只是在这个入口函数内做了大量的逻辑处理,最后返回HTTP响应内容。   大家想想是不是这个道理。

         所以这里就存在一个逻辑,那就是每个HTTP进来,要么是单进程单线程去执行这个入口函数,要么就是多进程多线程的方式, 一个HTTP请求对应一个线程来执行这个函数。  应用的并发能力其实取决于底层的进程池、线程池, 进程池、线程池满了,那么新的请求只能等待资源池空闲,才能得到处理,否则排队等待, 给用户的直观感受就是,为啥查看这个页面或者查询数据这么慢,其实这个时候就是在排队等空闲的线程,才能处理请求。

 2、ASGI协议

        ASGI是作为WSGI的一个扩展被提出的,以支持异步编程模型,包括WebSocket、HTTP/2等需要长时间连接的协议。ASGI不仅能够处理传统的HTTP请求,还可以处理异步请求和流式数据传输。 ASGI是2018年左右才提出的, 相比WSGI出现比较晚,所以现在的nginx都没还没支持ASGI协议。

        ASGI协议的实现有uvicorn, 使用uvicorn可以运行我们的异步HTTP编程接口函数。 ASGI入口函数签名如下:

async def app(scope, receive, send):
    # 根据scope中的类型处理请求,并使用receive和send来接收和发送消息
  • scope:这是一个字典,类似于WSGI的environ,但是更加通用,可以包含HTTP请求信息、WebSocket连接信息等。scope中的type字段指明了当前连接的类型,可能是'http''websocket'等。

  • receive:这是一个异步可调用对象,用于从客户端接收消息。每次调用都会等待下一个传入的消息。消息本身是一个字典,格式取决于scope['type']

  • send:这是另一个异步可调用对象,用于向客户端发送消息。你将传递给它的也是一个字典,格式同样依赖于scope['type']

        对于HTTP请求,典型的流程是从receive读取一个或多个HTTP请求消息,然后通过send发送HTTP响应。对于WebSocket连接,则可能涉及到多次调用receivesend来处理不同的事件,比如建立连接、接收消息、关闭连接等。

        与WSGI协议类似, 这个也存在入口函数。每个请求过来就执行这个协程函数,得到响应数据,HTTP协议返回给客户端。 这个入口函数通过async进行修饰, 表示是一个协程函数, scope可以获取协议请求相关信息如HTTP请求头等。 每个请求就是对应一个协程的执行, 通过event_loop协程并发模型,而非WSGI的多进程、多线程, 不必担心线程池、进程池满了。

        FastAPI框架也就是封装了这么一个符合ASGI规范的入口函数罢了,无外乎它帮我们做了很多封装工作,例如获取请求头信息,如何响应数据,如何进行请求拦截等等。  归根结底,每个请求就是这么一个入口协程函数的执行和调度而已。

四、总结

        这章先给大家抛砖引玉,如果异步编程和注意事项不清楚,还是使用传统编写Flask、Django的方式直接使用FastAPI, 那么你可能会遇到同步灾难, 一个接口不小心写同步阻塞了,你整个项目的其它接口都会收到影响,这可不是开玩笑的, 毕竟event_loop就是一个单线程。

        下一章讲解FastAPI路由阻塞同步测试,让大家有这个感受和代码的注意事项。

 

### FastAPI 中异步处理的工作原理 FastAPI 是一种基于 Python 的现代 Web 框架,其核心优势在于支持高效的异步编程模型。这种特性主要得益于它构建在 **Starlette** 和 **Pydantic** 之上,并充分利用了 Python 的原生异步功能——即协程和事件循环。 #### 协程与事件循环的作用 Python 的 `async`/`await` 关键字允许开发者编写异步代码,而这些代码最终会被解释器编译成协程对象并交由事件循环管理[^1]。具体来说,在 FastAPI 中: - 当一个请求到达时,事件循环会调度相应的协程来执行对应的业务逻辑。 - 如果该逻辑涉及 I/O 操作(如数据库查询、文件读写或网络调用),则会在等待期间自动释放线程资源给其他任务使用,从而提高系统的整体吞吐量。 #### 异步函数的定义与调用 在 FastAPI 中声明路径操作函数时可以指定它们为异步形式,只需简单地加上 `async def` 就能启用这一模式[^2]。例如下面展示了一个典型的例子: ```python from fastapi import FastAPI app = FastAPI() @app.get("/") async def read_root(): return {"message": "Hello World"} ``` 上述代码片段中的 `read_root()` 方法被标记为了异步方法。当客户端访问根 URL (`"/"`) 时,FastAPI 不仅能够识别这是一个异步处理器,而且还能确保整个生命周期内的所有阶段都尽可能保持非阻塞状态。 #### 并发能力提升的关键因素 除了基本的支持外,真正让 FastAPI 实现高并发的原因还与其内部架构紧密相连: - 它继承自 Starlette 提供的强大基础结构,后者本身已经针对 ASGI (Asynchronous Server Gateway Interface) 进行优化; - 同时利用 Pydantic 来简化数据验证流程,减少不必要的计算开销; 因此即使面对大量同时在线用户的场景下也能维持较低延迟表现。 --- ### 总结 综上所述,FastAPI 的高性能来源于对 Python 异步特性的深入挖掘以及合理运用,通过巧妙结合协程技术同事件驱动型程序设计思路一起构成了如今我们所熟知的那个快速灵活又易于扩展维护版本控制系统下的 web 开发利器之一.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员Rocky

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值