FastAPI 源码解析(一):同异步路由运行机制

1,069 阅读6分钟

引言

本文将深入探讨 FastAPI 同异步路由相关源码的实现机制。通过理解 FastAPI 的内部工作原理,我们可以更好地利用其功能,并在需要时进行定制和扩展。

基于 FastAPI 源码版本:0.101.0 来进行解析

同异步路由的实现

FastAPI 支持同步和异步路由处理函数。同步路由函数 FastAPI 会使用线程池进行运行防止堵塞,异步则正常在事件循环 loop 中以协程方式处理。接下来从路由注册、匹配、路由函数执行等看其源码是如何实现的。

路由注册

在 FastAPI 中,路由是通过 @app.get@app.post 等装饰器以及通过 APIRouter 来注册的。

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { fastapi 源码解析案例 }
# @Date: 2023/08/06 22:16
import asyncio
import time
from pprint import pprint

import uvicorn
from fastapi import FastAPI

app = FastAPI(description="sync async route and background task")


@app.get("/api/async")
async def async_route():
    """异步处理函数"""
    await asyncio.sleep(1)
    return {"message": "This is an async route"}


@app.get("/api/sync")
def sync_route():
    """同步处理函数"""
    time.sleep(1)
    return {"message": "This is a sync route"}


def main():
    pprint(app.routes)
    uvicorn.run(app)


if __name__ == '__main__':
    main()

这些路由装饰器会将路由信息封装成 fastapi.routing.APIRoute 类的实例。

前4个 Route 是 swaager ui 的接口文档路由,后面 APIRoute 是自己定义的路由。

FastAPI 应用的 app.routes 属性是一个 property 属性,其内部实际是通过 APIRouter 来统一维护 routes 路由信息。

Starlette: 一个轻量级的 ASGI 框架/工具包,是构建高性能异步服务的基石。

FastAPI 是通过 Starlette 基础上进行封装的。

这些装饰器的作用就是把路由信息封装成 APIRoute 添加到 APIRouterroutes 中。

路由装饰器封装链路 @app.get -> APIRouter.get -> APIRouter.api_route -> APIRouter.add_api_route

endpoint 参数就是路由处理函数、route_class 默认就是 APIRoute 类。最后添加到 routes 列表中。这就是路由注册的整体逻辑。

路由匹配

当一个 HTTP 请求到达时,FastAPI 会遍历其 routes 列表,依次匹配请求的路径和方法。FastAPI 的路由匹配是通过 Starlette 提供的 Routing 组件的 matches 方法来实现的。

匹配过程如下

  1. FastAPI.call

  2. Starlette.call

  3. APIRouter.call

    1. APIRoute.matches

    2. APIRoute.handle

可以发请求打断点调试下

通过 super().__call__() 调用 Starlette 的 __call__

会先判断 self.middleware_stack 中间件栈是否为存在,没有则调用 build_middleware_stack 方法构造

从断点调试来看,发请求过来时已经存在如下中间件栈?

  ServerErrorMiddleware
      app = ExceptionMiddleware
      
  ExceptionMiddleware
      app = AsyncExitStackMiddleware
      
  AsyncExitStackMiddleware
      app = APIRouter

这是 fastapi 应用启动时,初次调用了下 app() => __call__() 方法用来初始化一些东西,其中就包括 middleware_stack,运行程序时断点就会进来(可以看 unicron.run 的源码),此时 middleware_stack 就是 None 走 build_middleware_stack 进行构造。

请求过来都是按照顺序依次走中间件的调用栈 self.middleware_stack(scope, receive, send) 最后走 self.router()也就是 APIRouter.__call__() FastAPI 源码并没有重写这个方法则是调用父类 starlette.routing.Router__call__

请求调用栈执行都会传递如下三个参数

  • scope: 包含请求的元数据,如 HTTP 方法、路径、查询参数等。

  • receive: 一个异步可调用对象,用于接收请求的 body 数据。

  • send: 一个异步可调用对象,用于发送响应。

遍历 routes 列表进行 route.matches 路由匹配

路径与方法匹配成功返回 Match.FULL, child_scope

for route in self.routes:
    # Determine if any route matches the incoming scope,
    # and hand over to the matching route if found.
    match, child_scope = route.matches(scope)
    if match == Match.FULL:
        scope.update(child_scope)
        await route.handle(scope, receive, send) # 匹配成功进行路由函数调用
        return
    elif match == Match.PARTIAL and partial is None:
        partial = route
        partial_scope = child_scope

if partial is not None:
    #  Handle partial matches. These are cases where an endpoint is
    # able to handle the request, but is not a preferred option.
    # We use this in particular to deal with "405 Method Not Allowed".
    scope.update(partial_scope)
    await partial.handle(scope, receive, send)
    return

路由处理函数的调用

在 FastAPI 中,路由处理函数的调用是通过 APIRoute.handle 方法实现的。FastAPI 在这个方法中添加了对同步和异步函数的区分处理。

self.app 属性如下

因为最终返回的是 get_request_handler 下面的闭包函数 app, 这 app 是异步函数所以一直判断是 True。

最终路由函数调用都是走如下代码

通过 asyncio.iscoroutinefunction(dependant.call) 来进行协程(异步)函数判断,dependant.call 引用的就是路由处理函数。最后执行路由处理函数 run_endpoint_function

async def run_endpoint_function(
    *, dependant: Dependant, values: Dict[str, Any], is_coroutine: bool
) -> Any:
    # Only called by get_request_handler. Has been split into its own function to
    # facilitate profiling endpoints, since inner functions are harder to profile.
    assert dependant.call is not None, "dependant.call must be a function"

    if is_coroutine:
        # 异步路由函数处理
        return await dependant.call(**values)
    else:
        # 同步路由函数处理(使用线程池进行处理)
        return await run_in_threadpool(dependant.call, **values)

async def run_in_threadpool(
    func: typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs
) -> T:
    if kwargs:  # pragma: no cover
        # run_sync doesn't accept 'kwargs', so bind them in here
        func = functools.partial(func, **kwargs)
    return await anyio.to_thread.run_sync(func, *args)

async def run_sync(
    func: Callable[..., T_Retval],
    *args: object,
    cancellable: bool = False,
    limiter: CapacityLimiter | None = None,
) -> T_Retval:
    """
    Call the given function with the given arguments in a worker thread.

    If the ``cancellable`` option is enabled and the task waiting for its completion is cancelled,
    the thread will still run its course but its return value (or any raised exception) will be
    ignored.

      :param func: a callable
      :param args: positional arguments for the callable
      :param cancellable: ``True`` to allow cancellation of the operation
      :param limiter: capacity limiter to use to limit the total amount of threads running
    (if omitted, the default limiter is used)
      :return : an awaitable that yields the return value of the function.

    """
    # 这里源码就跳转不进去了
    return await get_asynclib().run_sync_in_worker_thread(
        func, *args, cancellable=cancellable, limiter=limiter
    )

异步函数直接 await 返回,同步函数放到线程池中进行处理。具体怎么加进去的源码看不到了,但大致逻辑应该都差不多。在异步的上下文环境中一般都是通过如下方式让事件循环指定具体的执行器进行处理。

import asyncio
from concurrent.futures import ThreadPoolExecutor


def sync_bg_task():
    print('sync bg task running')
    time.sleep(3)
    print('sync bg task completed')

async def main():
    loop.run_in_executor(ThreadPoolExecutor(), sync_bg_task, "loop.run_in_executor")

异步装饰器

我之前也封装过一个异步装饰器,可以将同步函数转异步,大家可以参考下

def run_on_executor(executor: Executor = None, background: bool = False):
    """
    异步装饰器
    - 支持同步函数使用 executor 加速
    - 异步函数和同步函数都可以使用 `await` 语法等待返回结果
    - 异步函数和同步函数都支持后台任务,无需等待
    Args:
        executor: 函数执行器, 装饰同步函数的时候使用
        background: 是否后台执行,默认False

    Returns:
    """

    def _run_on_executor(func):
        @functools.wraps(func)
        async def async_wrapper(*args, **kwargs):
            if background:
                return asyncio.create_task(func(*args, **kwargs))
            else:
                return await func(*args, **kwargs)

        @functools.wraps(func)
        def sync_wrapper(*args, **kwargs):
            loop = asyncio.get_event_loop()
            task_func = functools.partial(func, *args, **kwargs)  # 支持关键字参数
            return loop.run_in_executor(executor, task_func)

        # 异步函数判断
        wrapper_func = async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
        return wrapper_func

    return _run_on_executor

具体设计可以查看 同步、异步无障碍:Python异步装饰器指南