浅谈FastAPI

作者: ELVENITO | 来源:发表于2020-09-09 15:36 被阅读0次

    未经许可请勿转载。
    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的原生类型的生命周期保持同步。

    参考

    相关文章

      网友评论

        本文标题:浅谈FastAPI

        本文链接:https://www.haomeiwen.com/subject/wjkjektx.html