挂载 ASGI 应用

ASGI (Asynchronous Server Gateway Interface) 是 WSGI (Web Server Gateway Interface) 的精神继承者,旨在为支持异步的 Python Web 服务器、框架和应用提供标准接口。ASGI 支持异步请求处理,允许多个请求同时处理,这使其适用于实时 Web 应用,例如 WebSockets、长轮询等。

BentoML 的服务器在 ASGI Web 服务层中运行 Service API,暴露 REST 端点用于推理 API(例如 POST /summarize)和用于监控的常见基础设施 API(例如 GET /metrics)。这个 ASGI 原生 Web 服务层允许直接挂载现有的 ASGI 应用,使其能够与 BentoML Services 并行服务。

本文档解释了如何将 ASGI 应用挂载到 BentoML Service 上。

为什么要挂载 ASGI 应用

将 ASGI 应用(例如使用 FastAPI 构建的应用)挂载到 BentoML Service 上有多种优势:

  • 功能扩展:它使您能够通过附加的 Web 功能来扩展机器学习服务,例如用于数据处理、用户管理或提供静态文件和 Web 用户界面的自定义 API,这些功能与模型服务没有直接关系。

  • 自定义认证和授权:通过集成 ASGI 应用,您可以实现高级的认证和授权机制,根据应用的特定需求定制安全措施。

  • API 文档:使用 FastAPI 等工具,您可以自动获得交互式 API 文档,使用户更容易理解和使用 API。

将 BentoML 与 ASGI 框架集成

BentoML 提供了与不同 ASGI 框架的无缝集成,使您能够在提供 ML 模型的同时,也提供自定义 Web 应用逻辑,例如异步操作、实时数据处理和复杂的 Web 应用功能。

集成 ASGI 框架时,您可以使用 bentoml.get_current_service() 来检索当前的 BentoML Service 实例。当您需要从 ASGI 应用路由内部访问 BentoML Service 实例,或使用依赖注入模式将 Service 实例注入 ASGI 应用路由时,这非常有用。

请参阅以下示例了解详情。

FastAPI

FastAPI 是一个基于 ASGI 的 Web 框架,用于构建 API,可以处理异步请求。要将 FastAPI 应用与 BentoML Service 集成,您可以在 Service 内部或外部定义 FastAPI 路由,如下所示。

注意

确保您已通过运行 pip install fastapi 安装 FastAPI。请参阅 FastAPI 文档 了解更多信息。

from fastapi import FastAPI, Depends
import bentoml

app = FastAPI()

@bentoml.service
@bentoml.asgi_app(app, path="/v1")
class MyService:
    name = "MyService"

    @app.get('/hello')
    def hello(self):  # Inside service class, use `self` to access the service
        return f"Hello {self.name}"

@app.get("/hello1")
async def hello(service: MyService = Depends(bentoml.get_current_service)):
    # Outside service class, use `Depends` to get the service
    return f"Hello {service.name}"

具体而言,按照以下步骤挂载 FastAPI

  1. 使用 FastAPI() 创建 FastAPI 应用。

  2. 使用 @bentoml.asgi_app 装饰器将 FastAPI 应用挂载到 BentoML Service,使它们可以一起提供服务。设置 path 参数以自定义前缀路径

  3. 使用 @app.get("/<route-name>") 在 Service 类内部或外部定义 FastAPI 路由。

    • 类内部:使用 self 访问 Service 实例的属性和方法。

    • 类外部:使用 FastAPI 的依赖注入系统(Depends)将 BentoML Service 实例注入路由函数。在上面的代码中,hello1 路由使用 Depends(bentoml.get_current_service) 注入 MyService 实例,允许路由访问 Service 的属性和方法。

  4. 在 FastAPI 路由内,添加您想要的实现逻辑。本示例使用 Service 的名称返回问候消息。

注意

除了 get,您还可以使用其他操作,如 postputdelete。请参阅 FastAPI 文档 了解更多信息。

设计选择:内部 vs. 外部

在 Service 类内部和外部访问 BentoML Service 实例,在构建 Service 逻辑和依赖项以及与之交互方面提供了灵活性。在这些上下文中访问 BentoML Service 实例的差异主要与作用域和预期用例有关。

Service 类内部

  • 直接访问:在定义 BentoML Service 的类中,您可以直接访问 self,它代表 Service 的实例。这允许您直接访问其属性和方法,而无需注入任何依赖。这是在 Service 自身定义中使用其功能最直接的方式。

  • 上下文使用:在类内部访问 Service 实例通常用于定义 Service 的内部逻辑,例如设置端点、执行模型操作以及处理与 Service 主要功能直接相关的请求。

Service 类外部

  • 依赖注入:在类外部访问 BentoML Service 实例通常需要依赖注入机制,例如 FastAPI 中的 Depends 函数。当您想在项目的其他部分使用 Service 实例时,这种方法是必要的。

  • 模块化和解耦设计:这种方法允许 BentoML 项目的不同组件与 Service 交互,而无需紧密集成到其类定义中。例如,您的 ML 逻辑可以封装在 BentoML Service 中,而其他方面,如自定义认证、补充数据处理或附加的 REST 端点,可以在外部进行管理,但仍能按需与 Service 交互。

以下是将 FastAPI 挂载到 你好世界 中 Summarization Service 上的一个更实际的示例。它通过分别从类内部和外部访问 Service,使用 FastAPI 定义了两个额外的端点。

from __future__ import annotations
import bentoml
from transformers import pipeline
from fastapi import FastAPI, Depends

EXAMPLE_INPUT = "Breaking News: In an astonishing turn of events, the small town of Willow Creek has been taken by storm as local resident Jerry Thompson's cat, Whiskers, performed what witnesses are calling a 'miraculous and gravity-defying leap.' Eyewitnesses report that Whiskers, an otherwise unremarkable tabby cat, jumped a record-breaking 20 feet into the air to catch a fly. The event, which took place in Thompson's backyard, is now being investigated by scientists for potential breaches in the laws of physics. Local authorities are considering a town festival to celebrate what is being hailed as 'The Leap of the Century."

# Create a FastAPI app instance
app = FastAPI()

@bentoml.service(
    resources={"cpu": "2"},
    traffic={"timeout": 10},
)
@bentoml.asgi_app(app, path="/v1")
class Summarization:
    def __init__(self) -> None:
        self.pipeline = pipeline('summarization')

    # Define a name attribute
    name = "MyService"

    # The original Service API endpoint for text summarization
    @bentoml.api
    def summarize(self, text: str = EXAMPLE_INPUT) -> str:
        result = self.pipeline(text)
        return result[0]['summary_text']

    # Access the Service instance inside the class
    @app.get("/hello-inside")
    def hello(self):
        # Add other logic here if needed
        return f"Hello {self.name}. You can access the Service instance inside the class."

# Access the Service instance outside the class
@app.get("/hello-outside")
async def hello(service: MyService = Depends(bentoml.get_current_service)):
    # Add other logic here if needed
    return f"Hello {service.name}. You can access the Service instance outside the class."

启动 BentoML Service 后,您可以通过 https://:3000/ 访问它,您将发现暴露了两个额外的端点 hello-insidehello-outside

Two API endpoints defined in BentoML

通过发送 GET 请求,您可以从这两个端点接收相应的输出。

Service 类内部的 FastAPI 路由

FastAPI route inside the BentoML Service class

Service 类外部的 FastAPI 路由

FastAPI route outside the BentoML Service class

Quart

Quart 是一个用于 Python 的异步 Web 框架,使您能够在 Web 应用中使用 async/await 特性来处理大量的并发连接。

以下是集成 Quart 与 BentoML 的示例。

注意

确保您已通过运行 pip install quart 安装 Quart。请参阅 Quart 文档 了解更多信息。

from quart import Quart

app = Quart(__name__)

@app.get("/hello")
async def hello_world():
    service = bentoml.get_current_service()
    return f"Hello, {service.name}"

@bentoml.service
@bentoml.asgi_app(app, path="/v1")
class MyService:
    name = "MyService"

具体而言,按照以下步骤挂载 Quart

  1. 使用 Quart() 创建 Quart 应用。

  2. 使用 @bentoml.asgi_app 装饰器将 Quart 应用挂载到 BentoML Service,使它们可以一起提供服务。设置 path 参数以自定义前缀路径

  3. 使用 @app.get(/"<route-name>") 在 Service 类外部定义 Quart 路由。使用 bentoml.get_current_service() 注入 MyService 实例,允许路由访问 Service 的属性和方法。

  4. 在 Quart 路由内,添加您想要的实现逻辑。本示例使用 Service 的名称返回问候消息。

注意

除了 get,您还可以使用其他操作,如 postputdelete。请参阅 Quart 文档 了解更多信息。

以下是将 Quart 挂载到 你好世界 中 Summarization Service 上的一个更实际的示例。它定义了一个额外的端点 hello

from __future__ import annotations
import bentoml
from transformers import pipeline
from quart import Quart

EXAMPLE_INPUT = "Breaking News: In an astonishing turn of events, the small town of Willow Creek has been taken by storm as local resident Jerry Thompson's cat, Whiskers, performed what witnesses are calling a 'miraculous and gravity-defying leap.' Eyewitnesses report that Whiskers, an otherwise unremarkable tabby cat, jumped a record-breaking 20 feet into the air to catch a fly. The event, which took place in Thompson's backyard, is now being investigated by scientists for potential breaches in the laws of physics. Local authorities are considering a town festival to celebrate what is being hailed as 'The Leap of the Century."

# Create a Quart app instance
app = Quart(__name__)

@app.get("/hello")
async def hello_world():
    service = bentoml.get_current_service()
    # Add other logic here if needed
    return f"Hello, {service.name}"

@bentoml.service(
    resources={"cpu": "2"},
    traffic={"timeout": 10},
)
@bentoml.asgi_app(app, path="/v1")
class Summarization:
    def __init__(self) -> None:
        self.pipeline = pipeline('summarization')

    # Define a name attribute
    name = "MyService"

    # The original Service API endpoint for text summarization
    @bentoml.api
    def summarize(self, text: str = EXAMPLE_INPUT) -> str:
        result = self.pipeline(text)
        return result[0]['summary_text']

启动 BentoML Service 后,您可以通过 https://:3000/ 访问它,您可以与暴露的端点 hello 进行交互。例如:

$ curl https://:3000/v1/hello

Hello, MyService

注意

与 FastAPI 不同,Quart 不原生支持 OpenAPI 规范,因此端点不会显示在 Swagger UI 上。您可以使用其他方式与之通信,例如 curl

自定义前缀路径

将 ASGI 工具挂载到 BentoML Service 上时,可以通过设置前缀来自定义路由路径。这对于组织 API 端点以及简化路由和命名空间管理非常有用。

要设置前缀路径,只需在装饰器 @bentoml.asgi_app 中设置 path 参数即可。这是一个 FastAPI 示例:

from fastapi import FastAPI, Depends
import bentoml

app = FastAPI()

@bentoml.service
@bentoml.asgi_app(app, path="/fastapi") # Add the prefix here
class MyService:
    name = "MyService"

    @app.get('/hello')  # This endpoint should be requested via "/fastapi/hello"
    def hello(self):
        return f"Hello {self.name}"

通过指定 path="/fastapi",整个 FastAPI 应用将在此前缀下提供服务。这意味着在 FastAPI 应用中定义的所有路由都将通过 /fastapi 访问。在此示例中,启动此 BentoML Service 后,您应该与 /fastapi/hello 端点进行交互。

添加自定义 ASGI 中间件

add_asgi_middleware 是 BentoML 提供的一个 API,用于应用自定义 ASGI 中间件。中间件作为一个层来处理请求和响应,允许您基于特定条件操纵它们或执行附加操作。它通常用于实现安全措施、自定义头部、管理 CORS、压缩响应等。

示例用法

from __future__ import annotations
import bentoml
from transformers import pipeline

from starlette.middleware.trustedhost import TrustedHostMiddleware

@bentoml.service(
    resources={"cpu": "2"},
    traffic={"timeout": 10},
)
class Summarization:
    def __init__(self) -> None:
        self.pipeline = pipeline('summarization')

    @bentoml.api
    def summarize(self, text: str) -> str:
        result = self.pipeline(text)
        return result[0]['summary_text']

# Add TrustedHostMiddleware to ensure the Service only accepts requests from certain hosts
Summarization.add_asgi_middleware(TrustedHostMiddleware, allowed_hosts=['example.com', '*.example.com'])

此示例确保 Summarization Service 只接受来自指定主机的请求,并防止主机头攻击。然后,您可以通过在请求中手动指定 Host 头部与 Service 交互:

curl -H "Host: example.com" https://:3000

注意

或者,您可以编辑 hosts 文件,将 example.com 映射到 127.0.0.1 (localhost),然后访问 http://example.com:3000/

虽然 add_asgi_middleware 用于向 BentoML 用于提供 API 的 ASGI 应用添加中间件,但 @bentoml.asgi_app 用于将整个 ASGI 应用集成到 BentoML Service 中。这适用于将具有自身路由逻辑的完整 Web 应用,如 FastAPI 或 Quart 应用,直接添加到您的 BentoML Service 旁边。

通过 add_asgi_middleware 添加的中间件适用于整个 ASGI 应用,包括 BentoML Service 和任何已挂载的 ASGI 应用。这确保了整个应用中所有请求的一致处理,无论它们是针对 BentoML Services 还是其他组件。