未经许可请勿转载。
Please do not reprint this article without permission.
众所周知,Python Web开发常用的三大框架是Django、Flask和Tornado。笔者在面试过程中也被常问到这几个框架的特点和区别,具体可参考Python Web 框架:Django、Flask 与 Tornado 的性能对比等等。本文对此不会作深入讨论,而是要介绍一款据说性能更高、更适用于高并发场景的框架——FastAPI。笔者在面试过程中接触到了这个框架,并马上搜索了相关的文档,发现介绍其原理的中文文章并不多,因此借着为秋招复习这个机会,探究一下FastAPI究竟为什么这么快。
什么是FastAPI?
FastAPI是一款现代化、高性能的Web框架,用于构建基于Python3.6及以上的API,其具有以下特征:
- 速度快:非常高的性能,与NodeJS和Go不相上下,是最快的Python框架之一;
- 编码快:将开发特性所需的速度提高大约200%到300%;
- 错误少:减少大约40%的人为(开发)错误;
- 直观:强大的编辑器支持,支持多场景开发,调试所花的时间更少;
- 简单:被设计为易于使用和学习,减少阅读文档的时间;
- 代码少:最小化重复,更少的错误;
- 健壮:代码可随时部署到生产环境,并自动提供交互文档;
- 标准:基于(并完全兼容)api的开放标准:OpenAPI(以前称为Swagger)和JSON模式。
具体的使用方法详见中文文档。
Starlette & ASGI
根据上面的官方介绍,我们看到FastAPI的速度得益于使用了Starlette——一个轻量级的ASGI框架。
ASGI,全称为Asynchronous Server Gateway Interface,为了规范支持异步的Python Web服务器、框架和应用之间的通信而定制,同时囊括了同步和异步应用的通信规范,并且向后兼容WSGI。由于最新的HTTP协议支持异步长连接,而传统的WSGI应用支持单次同步调用,即仅在接受一个请求后返回响应,从而无法支持HTTP长轮询或WebSocket连接。在Python3.5增加async/await特性之后,基于asyncio和协程的异步应用编程变得更加方便。ASGI协议规范就是用于asyncio框架的最低限度的底层服务器/应用程序接口。
异步非阻塞I/O & 协程
阻塞I/O,非阻塞I/O,I/O多路复用都属于同步I/O。而异步I/O则不一样,当进程发起I/O操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说I/O完成。在这整个过程中,进程完全没有被阻塞。在非阻塞I/O中,虽然进程大部分时间都不会被阻塞,但是它仍然要求进程去主动的查询,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom()来将数据拷贝到用户内存。
相对于线程,协程是程序级的I/O调度,是对一个线程进行分片,使得线程在代码块之间来回切换执行,而非逐行执行,因此能够支持更快的上下文切换。协程本身并不能实现高并发,但与I/O切换结合后能够大大提高性能。每当发生I/O,自动切换协程,让出CPU资源,即可减少高并发场景下服务的响应时间。因此,结合async/await语法,将代码块定义为协程,使用异步服务器即可实现程序级I/O切换和协程调度。
...
async def app(request: Request) -> Response:
try:
body = None
if body_field:
if is_body_form:
body = await request.form()
else:
body_bytes = await request.body()
if body_bytes:
body = await request.json()
except json.JSONDecodeError as e:
raise RequestValidationError([ErrorWrapper(e, ("body", e.pos))], body=e.doc)
except Exception as e:
raise HTTPException(
status_code=400, detail="There was an error parsing the body"
) from e
solved_result = await solve_dependencies(
request=request,
dependant=dependant,
body=body,
dependency_overrides_provider=dependency_overrides_provider,
)
values, errors, background_tasks, sub_response, _ = solved_result
if errors:
raise RequestValidationError(errors, body=body)
else:
raw_response = await run_endpoint_function(
dependant=dependant, values=values, is_coroutine=is_coroutine
)
if isinstance(raw_response, Response):
if raw_response.background is None:
raw_response.background = background_tasks
return raw_response
response_data = await serialize_response(
field=response_field,
response_content=raw_response,
include=response_model_include,
exclude=response_model_exclude,
by_alias=response_model_by_alias,
exclude_unset=response_model_exclude_unset,
exclude_defaults=response_model_exclude_defaults,
exclude_none=response_model_exclude_none,
is_coroutine=is_coroutine,
)
response = response_class(
content=response_data,
status_code=status_code,
background=background_tasks,
)
response.headers.raw.extend(sub_response.headers.raw)
if sub_response.status_code:
response.status_code = sub_response.status_code
return response
...
可以看到app通过async语法定义为协程,在收到请求后,对于需要I/O操作的地方,会使用await关键字让出资源,待I/O完成后等待资源,最终返回响应。
事件循环
因为Python是单线程的,同一时间只能执行一个方法,所以当一系列的方法被依次调用的时候,Python会先解析这些方法,把其中的同步任务按照执行顺序排队到一个地方,这个地方叫做执行栈。主线程之外,还存在一个"任务队列"(task queue)。当遇到异步任务时,异步任务会被挂起,继续执行执行栈中任务,等异步任务返回结果后,再按照执行顺序排列到"事件队列中"。一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到事件队列中。主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个过程是循环往复的,这个过程被称为Event Loop,即事件循环。
由于异步非阻塞框架基本为单线程运行,因此要利用协程实现事件循环。FastAPI推荐使用uvicorn
来运行服务,uvicorn
是基于uvloop
和httptools
构建的闪电般快速的ASGI服务器。Python3.5+
的标准库asyncio
提供了事件循环用来实现协程,并引入了async/await
关键字语法以定义协程。同是异步非阻塞框架的Tornado
通过yield
生成器实现协程,它自身实现了一个事件循环,其在Python3之后也支持async/await
关键字语法,以使用标准库asyncio
。而FastAPI则是利用了uvloop
,相对于asyncio
,更进一步地提升了速度。uvloop
是用Cython
编写的,并建立在libuv
之上。libuv
是一种高性能的、跨平台异步的I/O类库,nodejs
也使用到了它。由于nodejs
是如此的广泛和流行,可以知道libuv
是快速且稳定的。uvloop
实现了所有的asyncio
事件循环APIs。高级别的Python对象包装了低级别的libuv
结构体和函数方法。 继承可以使得代码保持DRY
(不要重复自己),并确保任何手动的内存管理都可以与libuv
的原生类型的生命周期保持同步。