跳到主要内容

为什么你的数据库在AI功能上线后崩溃:LLM感知的连接池设计

· 阅读需 10 分钟
Tian Pan
Software Engineer

在AI功能上线之前,你的连接池一直运行良好。登录正常,仪表板加载顺畅,CRUD操作以个位数毫秒的延迟稳定运行。然后团队部署了一个RAG驱动的搜索、一个Agent驱动的工作流,或者一个LLM支持的摘要端点——几个小时内,你的核心产品开始超时。数据库并没有变慢,你的连接池只是被一种它从未被设计来处理的工作负载吞噬了。

这就是LLM连接池问题,随着AI功能从原型走向生产环境,它正在影响整个行业的团队。解决方案不是"增加更多连接"。事实上,这通常会让事情变得更糟。

根本性的不匹配:Web流量 vs AI工作负载

传统连接池是为Web应用流量设计的:短暂的请求获取一个连接,执行一个快速查询(通常1-10ms),然后释放它。一个20个连接的池可以轻松地每秒处理数百个请求,因为每个连接只被持有很短的时间窗口。

AI驱动的功能打破了这个模型所依赖的每一个假设。

一个RAG检索管道可能需要持有一个连接进行向量相似度搜索(50-200ms),然后再次持有它进行元数据查找,然后再次用于上下文窗口组装查询——这一切都发生在LLM开始生成token之前。Agent工作流更糟糕:它可能在多步推理链中执行5-15个数据库操作,在数秒而非毫秒的时间跨度内间歇性地持有连接。

数学算不过来了。如果你的平均连接持有时间从5ms跳到500ms,你的有效池吞吐量下降了100倍。那些每秒处理400个请求的20个连接现在只能处理4个。你的排队队列填满,超时级联扩散,而登录页面——只需要一个2ms的查询——因为所有连接都被一个正在决定下一步做什么的Agent占用而无法获得连接。

AI工作负载摧毁连接池的三种方式

扇出放大

AI功能中的单个用户操作通常会触发数据库查询的扇出。用户提出一个问题,在后台系统运行嵌入查找、检索10个候选文档、获取每个文档的元数据、检查访问权限、加载对话历史并写入审计日志。对负载均衡器来说看起来像一个请求的东西,实际上是15-20个数据库操作,每个都需要一个连接。

传统Web端点有可预测的扇出比率——通常每个请求1:1到1:3个查询。AI端点通常达到1:10或更高。当你乘以并发用户数时,连接需求的峰值是非线性的。十个并发AI请求不需要10个连接;它们需要150个。

外部调用期间持有连接

这是最隐蔽的模式。许多实现在等待LLM API响应时保持数据库连接打开——这个调用需要1-30秒。开发者并非有意为之;这是因为数据库查询和LLM调用在同一个事务、同一个请求处理器或同一个ORM会话范围内。

一个在LLM调用期间持有10秒的连接,意味着2000个正常查询无法使用这个连接。如果你的池有25个连接,其中5个卡在等待LLM响应上,你已经失去了20%的容量给本质上的空闲等待。

重试放大

AI功能比传统端点更容易失败。LLM超时、向量数据库没有返回结果、Agent的工具调用遇到速率限制。当这些失败触发重试时——它们几乎总是会——每次重试都会重新执行整个数据库扇出。一个带有3次重试和10个查询扇出的单个失败AI请求会产生40个数据库操作而不是10个。

结合更长的持有时间,重试放大会产生惊群效应。池已经处于压力之下,重试堆积更多负载,检出超时增加,这又触发更多上游重试,突然你陷入了一个反馈循环,导致整个应用程序崩溃——不仅仅是AI功能。

连接池分段:核心架构模式

解决方案不是更大的池。而是分离的池。连接池分段将AI工作负载与核心产品隔离,使一方无法饿死另一方。

最简单的版本使用两个池:

  • 核心池:专门为现有产品保留。登录、仪表板查询、CRUD操作。这个池有保证的最小连接数,任何AI工作负载都无法触及。
  • AI池:专用于LLM驱动的功能。这个池有自己的限制、自己的超时和自己的熔断器。如果它耗尽了,AI功能降级——但核心产品继续运行。

在实践中,这意味着你的AI服务层获得一个指向独立PgBouncer实例的单独数据库连接字符串(或同一个连接池中配置了独立限制的单独池)。AI池可能有default_pool_size = 15和激进的server_idle_timeout,而核心池有default_pool_size = 30和标准设置。

一些团队更进一步采用三层分段:核心、AI读取(用于检索和嵌入查找)和AI写入(用于审计日志、对话历史、反馈存储)。这可以防止突发的写密集型Agent活动阻塞依赖读取的检索管道。

准入控制:缺失的一环

连接池分段告诉你哪个池处理哪种工作负载。准入控制告诉你是否接受这个请求。

没有准入控制,一个不堪重负的AI池会无限期地排队请求,而这些排队的请求在应用层持有资源(内存、goroutine、线程),即使它们没有做任何有用的事情。最终,应用程序也会耗尽资源。

有效的AI工作负载准入控制有三个层次:

  • 连接检出超时:为AI操作设置激进的短超时——2-5秒而不是通常的30秒。如果AI功能不能快速获得连接,它应该快速失败并返回降级响应,而不是堆积在队列中。
  • 并发请求限制:在请求到达池之前,在应用层限制正在进行的AI请求数量。如果你的AI池支持15个连接,而平均AI请求使用10个查询,你的并发AI请求限制应该在10-12左右,而不是100。
  • 成本感知队列:并非所有AI操作都是相同的。一个简单的嵌入查找需要1个连接持续50ms。一个多步Agent工作流需要10个连接持续5秒。为每种操作类型分配"连接成本"并用于准入决策。当池利用率达到80%时,接受低成本操作并丢弃高成本操作。

"在调用LLM之前释放连接"规则

对大多数团队来说,影响最大的单一修复是架构层面的:永远不要在等待外部AI服务时持有数据库连接。

模式很直接:

  1. 查询数据库获取你需要的一切(上下文、历史、嵌入)。
  2. 将连接释放回池。
  3. 调用LLM API。
  4. 获取新连接来写入结果(如果需要)。

这听起来很明显,但它需要重构那些自然希望存在于单个事务或请求范围内的代码。具有会话范围连接管理的ORM使这特别棘手——你需要显式地分离对象或为LLM前的查询使用无状态读取模式。

回报是巨大的。如果你的LLM调用平均3秒,数据库查询平均50ms,这一个改变就能将每个请求的连接持有时间减少约98%。你的池实际上变大了50倍,而无需增加一个连接。

监控重要指标

标准的池指标——活跃连接、空闲连接、池大小——是必要的但不够的。AI工作负载需要额外的可观测性:

  • 按功能分段的连接持有时间分布:你的p50可能看起来很好,而你的p99(由Agent工作流驱动)高出100倍。你需要按功能划分的百分位数来看到这一点。
  • 按池划分的检出等待时间:如果你的AI池的检出等待时间在攀升而核心池持平,说明分段正在工作。如果两者都在攀升,你有泄漏。
  • 每个端点的扇出比率:跟踪每个API端点每个请求实际使用多少连接。这是你的预警系统。当开发者给Agent添加新工具时,该端点的扇出比率可能一夜之间从8跳到15。
  • 外部调用期间的连接持有时间:检测代码在出站HTTP请求(到LLM API)进行中时是否持有连接。这始终是一个bug。让它告警。

AI池大小调整

标准的池大小公式——pool_size = (average_query_duration × requests_per_second) × 1.2——不适用于AI工作负载,因为方差太大。一个Agent可能执行3个查询或30个,持有连接10ms或10秒。

相反,从你愿意接受的最坏情况来确定大小:

  1. 测量你最昂贵的AI操作的最大扇出。
  2. 决定你的最大并发AI请求数(这是一个产品决策,而不是基础设施决策)。
  3. 设置pool_size = max_fan_out × max_concurrent_requests
  4. 设置硬上限为数据库总连接容量的30-40%,这样即使在峰值负载下,AI池也永远不会占用超过其公平份额。
  5. 添加准入控制来强制执行步骤2中的并发请求限制。

对于大多数刚发布第一个AI功能的团队,这看起来像是:10最大扇出 × 5并发请求 = 50个连接,上限为其数据库容量的30%。数字很小,因为这就是重点——AI功能每个请求的成本很高,所以你需要限制并发性而不是堆砌连接。

更大的图景

连接池设计是一个更广泛挑战的缩影:AI功能具有与传统Web功能根本不同的资源消耗特征,将它们塞入为请求-响应Web流量设计的基础设施会造成级联故障。

成功发布AI功能的团队从第一天就将它们视为不同的工作负载类别。独立的池、独立的限制、独立的监控、独立的降级路径。不是因为他们偏执,而是因为他们已经学到——通常是以惨痛的方式——如果允许一个LLM驱动的功能与维持产品运转的功能平等地竞争资源,它就可以拖垮整个产品。

数据库只是这种紧张关系首先显现的地方。同样的模式适用于线程池、内存分配、API速率限制以及你技术栈中的每一个共享资源。首先为隔离而设计。你总是可以稍后放宽边界。但你无法让已经崩溃的数据库恢复原状。

References:Let's stay in touch and Follow me for more thoughts and updates