浅谈FastAPI

未经许可请勿转载。
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是基于uvloophttptools构建的闪电般快速的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的原生类型的生命周期保持同步。

参考

  • Python Web 框架:Django、Flask 与 Tornado 的性能对比
  • FastAPI
  • ASGI介绍
  • Python 异步 ASGI 服务器及框架
  • Torando适配Uvloop与Asyncio下的性能简测
  • python Event_loop(事件循环)
  • uvloop —— 超级快的 Python 异步网络框架

你可能感兴趣的:(浅谈FastAPI)