6.4 生命周期上下文:服务端启动时建立的共享资源


文档摘要

6.4 生命周期上下文:服务端启动时建立的共享资源 本节摘要:本节讲第 6 章的收尾——生命周期(Lifespan)上下文。前面讲的 Context 和 Resolve 都是「请求级」的(每次请求新建),但有些资源是「服务端级」的——数据库连接池、HTTP 客户端、缓存,它们太昂贵,不能每次请求重建。生命周期上下文就是为这些共享资源设计的:在服务端启动时运行一段异步函数建立资源,放进 ,每次请求里通过 Context 访问。读完本节,你能避免「每次请求重建连接」的性能陷阱,写出贴近生产的服务端。 一、问题:每次请求重建资源的代价 先看一个性能反模式: 数据库连接的建立与关闭,涉及网络握手、认证、资源分配,每次都做代价巨大。

6.4 生命周期上下文:服务端启动时建立的共享资源

本节摘要:本节讲第 6 章的收尾——生命周期(Lifespan)上下文。前面讲的 Context 和 Resolve 都是「请求级」的(每次请求新建),但有些资源是「服务端级」的——数据库连接池、HTTP 客户端、缓存,它们太昂贵,不能每次请求重建。生命周期上下文就是为这些共享资源设计的:在服务端启动时运行一段异步函数建立资源,放进 lifespan_context,每次请求里通过 Context 访问。读完本节,你能避免「每次请求重建连接」的性能陷阱,写出贴近生产的服务端。

一、问题:每次请求重建资源的代价

先看一个性能反模式:

@mcp.tool() async def query_db(sql: str) -> list: """Query database.""" # 反模式:每次调用都新建连接 conn = create_connection() # 慢!建连接要握手、认证 try: return conn.execute(sql) finally: conn.close() # 又慢!关闭也要往返

数据库连接的建立与关闭,涉及网络握手、认证、资源分配,每次都做代价巨大。如果一秒处理 100 个请求,就是 100 次建连——这是真实生产里的性能杀手。

正确做法是:在服务端启动时建一次连接池,所有请求复用它。这就是生命周期上下文的用途。

二、生命周期上下文的工作机制

生命周期上下文的工作分三个阶段:

阶段一:服务端启动时 ┌─────────────────────────────────────┐ │ 运行 lifespan 异步函数 │ │ - 建立数据库连接池 │ │ - 建 HTTP 客户端 │ │ - 加载缓存 │ │ 把这些资源放进 lifespan_context │ └─────────────────────────────────────┘ │ ▼ 服务端开始接受请求 阶段二:每次请求 ┌─────────────────────────────────────┐ │ Context 携带 lifespan_context 引用 │ │ 工具通过 ctx.lifespan_context 访问 │ │ - 复用连接池(不重建) │ │ - 复用 HTTP 客户端 │ └─────────────────────────────────────┘ │ ▼ 服务端关闭时 阶段三:服务端关闭时 ┌─────────────────────────────────────┐ │ 清理 lifespan 函数建立的资源 │ │ - 关闭连接池 │ │ - 关闭 HTTP 客户端 │ └─────────────────────────────────────┘

关键点:资源在启动时建一次,所有请求复用,关闭时统一清理

三、lifespan 函数的写法

生命周期通过一个异步函数定义,它返回一个字典(就是 lifespan_context):

from contextlib import asynccontextmanager from mcp.server import MCPServer @asynccontextmanager async def app_lifespan(server: MCPServer): """应用生命周期:启动时建资源,关闭时清理。""" # 启动时:建立昂贵资源 db_pool = await create_db_pool(...) # 数据库连接池 http_client = create_http_client(...) # HTTP 客户端 cache = load_cache() # 缓存 # 把资源放进 lifespan_context(返回的字典) yield { "db_pool": db_pool, "http_client": http_client, "cache": cache, } # 关闭时:清理(yield 之后) await db_pool.close() await http_client.aclose() save_cache(cache) # 把 lifespan 挂到 MCPServer mcp = MCPServer("Demo", lifespan=app_lifespan)

注意结构:

  • yield 之前:启动逻辑(建资源)
  • yield 的字典:就是 lifespan_context,请求里可访问
  • yield 之后:关闭逻辑(清理资源)

四、在工具里访问 lifespan_context

工具通过 ctx.lifespan_context 访问启动时建立的资源:

from mcp.server.mcpserver import Context @mcp.tool() async def query_db(sql: str, ctx: Context) -> list: """Query database using the shared pool.""" # 从 lifespan_context 拿连接池 db_pool = ctx.lifespan_context["db_pool"] # 复用连接池(不重建) async with db_pool.acquire() as conn: return await conn.execute(sql)

对比反模式,这个工具不建连、不关连,直接用池里的连接——性能高几个数量级。

💡 技巧:lifespan_context 是个字典,你可以放任意资源。建议用清晰的键名("db_pool" 而非 "db"),并保持全书/全项目一致的命名,避免「这个服务端叫 db_pool,那个叫 pool」的混乱。

五、用 Resolve 封装 lifespan 资源

虽然能直接 ctx.lifespan_context["db_pool"] 访问,但更优雅的做法是用 Resolve 封装:

from typing import Annotated # 解析函数:从 lifespan_context 拿连接池 async def get_db_pool(ctx: Context) -> Pool: return ctx.lifespan_context["db_pool"] @mcp.tool() async def query_db( sql: str, db_pool: Annotated[Pool, Resolve(get_db_pool)], # Resolve 注入池 ) -> list: """Query database.""" async with db_pool.acquire() as conn: return await conn.execute(sql)

这样工具函数里直接用 db_pool 参数,不用关心它来自 lifespan_context。这与第 6.2 节的「关注点分离」一脉相承——资源获取(从 lifespan 拿)与资源使用(工具里用)分离

六、三种「资源获取方式」的层次

到这里,我们讲了三种让工具拿到资源的机制,它们的层次关系值得梳理:

资源获取的三层 ┌─────────────────────────────────────────────┐ │ 1. lifespan_context(服务端级,启动时建) │ ← 最重,建一次复用 │ - 数据库连接池、HTTP 客户端、缓存 │ ├─────────────────────────────────────────────┤ │ 2. Resolve(请求级,每次请求解析) │ ← 中,每次请求跑解析函数 │ - 当前用户、数据库会话 │ ├─────────────────────────────────────────────┤ │ 3. Context 的现成方法(请求级,即时) │ ← 轻,直接调方法 │ - read_resource、report_progress │ └─────────────────────────────────────────────┘

按「资源建立成本」选层:

  • 建立昂贵(连接池)→ lifespan,启动时建一次
  • 建立中等(会话,基于请求)→ Resolve,每次请求解析
  • 无需建立(读资源、报进度)→ Context,直接调方法

⚠️ 注意:这三层不是互斥的,一个工具可以同时用三层——比如「用 lifespan 的连接池(Resolve 注入)+ 读配置资源(Context)+ 上报进度(Context)」。理解了层次关系,你就能为每种资源选对获取方式。

七、lifespan 的常见资源

实战中,生命周期上下文里常放这几类资源:

资源 为什么放 lifespan 典型用法
数据库连接池 建连昂贵,必须复用 db_pool.acquire()
HTTP 客户端 建客户端要配置连接、线程池 http_client.get(...)
缓存(内存/Redis) 加载慢,应预热 cache.get(key)
文件句柄/锁 占用系统资源 共享读写
第三方 SDK 客户端 初始化重(认证、配置) sdk_client.call(...)

共同特征:建立成本高、可安全共享、需统一清理。如果你的资源符合这三点,就该放 lifespan。

本节要点回顾

  1. lifespan 解决「每次请求重建昂贵资源」的性能问题,启动时建一次,所有请求复用。
  2. 三阶段:启动时建资源 → 请求里复用 → 关闭时清理。
  3. 写法:@asynccontextmanager 异步函数,yield 前建、yield 字典是 lifespan_context、yield 后清理。
  4. 工具通过 ctx.lifespan_context["key"] 访问,或用 Resolve 封装更优雅。
  5. 三层资源获取:lifespan(服务端级)> Resolve(请求级)> Context 方法(即时)。
  6. 按建立成本选层:昂贵→lifespan,中等→Resolve,无需建立→Context。
  7. 适合 lifespan 的资源:建立重、可共享、需统一清理(连接池、HTTP 客户端、缓存)。

第 6 章结束。你已经掌握了一次请求内部的全部工程能力:Context、Resolve、生命周期上下文。第 7 章讲最有意思的交互式能力——工具执行中途向用户反问。


发布者: 作者: 灏天文库 转发
评论区 (0)
U