【金仓数据库征文】AI Agent SQL 执行超时与资源保护——在线问数不能把数据库当成“无限工具”

发布时间:2026/8/11 4:21:45
【金仓数据库征文】AI Agent SQL 执行超时与资源保护——在线问数不能把数据库当成“无限工具” 文章目录每日一句正能量1. 背景与问题SQL 是只读的也可能把生产库拖慢2. 环境与数据先定义服务预算再讨论参数2.1 为什么不能直接照抄一个 30 秒超时3. 复现过程为什么只设 statement_timeout 仍然会出问题3.1 第一版完全没有超时3.2 第二版只设置 statement_timeout锁等待不是执行计算SQL 完成不代表事务结束3.3 第三版把连接池大小当成 SQL 并发限制4. 方案实施KFS MCP Server 的资源治理闭环4.1 第一层端到端 Request Budget4.2 第二层在获取数据库连接之前做并发限制4.3 第三层并发限制要分全局、租户、用户4.4 第四层使用短队列禁止无限等待4.5 第五层数据库原生四类超时4.6 第六层结果行数、内存和临时磁盘同样要限制4.7 完整 query_readonly 工具调用4.8 MCP 2026-07-28 的无状态核心与扩容5. 结果对比不要把“所有请求都跑完”当成性能目标5.1 端到端 P955.2 峰值数据库并发5.3 Queue Wait5.4 Resource Rejection Rate5.5 Blast Radius稳态测试峰值测试慢 SQL 混合测试6. 风险与复盘超时是止损不是 SQL 优化6.1 三秒超时不会把三十秒 SQL 变成三秒 SQL6.2 超时后必须回滚并正确释放连接6.3 客户端超时不等于数据库 SQL 被取消6.4 无限自动重试会制造更大压力6.5 单实例 Semaphore 不等于数据库总并发6.6 并行查询会放大资源消耗6.7 本文参数不能直接照抄到生产最终复盘第一层限制“有多少请求能进入数据库”第二层限制“请求能等待多久”第三层限制“SQL 能执行多久、消耗多少”第四层限制“异常会影响多少其他用户”附录 A最小并发保护代码附录 B数据库执行预算附录 C推荐错误码每日一句正能量读书可以经历一千种人生不读书只能活一次。物理上我们被局限在单一的时间线里但精神上书籍是穿越时空的虫洞。透过书本我们可以与古人对话体验异域文化感受角色的悲欢离合。不读书的人只能在自己的小世界里打转读书的人却能在无数个灵魂中穿梭。1. 背景与问题SQL 是只读的也可能把生产库拖慢在企业问数场景中把 Agent 限制成只读 SQL 只是第一步。真正上线后最先暴露的问题往往不是 DELETE而是资源争用。例如用户问最近一年每个用户、每个商品、每天的购买趋势是什么模型可能生成SELECTuser_id,sku_id,DATE(created_at)ASdt,SUM(amount)AStotal_amountFROMordersWHEREcreated_atCURRENT_DATE-INTERVAL1 yearGROUPBYuser_id,sku_id,DATE(created_at)ORDERBYtotal_amountDESC;这条 SQL 没有写操作也可能完全符合权限规则但如果orders有数亿甚至数十亿行它依旧可能产生大范围扫描、Hash 聚合、排序落盘、临时文件膨胀、CPU 与 I/O 抢占最终拖慢正常在线请求。在线问数与离线分析最大的区别之一是在线链路有明确的交互预算。用户通常不会接受“SQL 最终能跑完但要等 40 秒”。所以 KFS MCP Server 不应该只是一个 SQL 转发器而应该承担资源治理职责自然语言问题 → Agent 生成 SQL → KFS 请求预算检查 → 并发闸门 → 短队列 → 获取数据库连接 → 数据库原生超时 → 结果行数与资源限制 → 取消 / 回滚 → 释放连接 → 指标与审计如果不做这一层一次流量尖峰很容易形成故障链大量 Agent 请求进入 → 每个请求都拿数据库连接 → 慢 SQL 占满连接池 → 新请求等待 → 上游 HTTP 超时 → Agent 自动重试 → 数据库压力继续升高因此本篇的核心问题不是“怎么让一条 SQL 在 3 秒后报错”而是如何让不可承受的 SQL 尽早被限制让正常查询在峰值期间仍有稳定资源。2. 环境与数据先定义服务预算再讨论参数本文以 PostgreSQL 18 为例示例业务库包含orders(order_id,user_id,sku_id,channel_id,amount,status,created_at)假设演示规模orders5 亿行 日增数据约 300 万行 平时在线问数并发38 活动高峰并发3050 数据库业务只读副本 KFS MCP Server2 个实例这些数字只是复现口径不是通用最佳值。2.1 为什么不能直接照抄一个 30 秒超时很多系统的第一版会设置statement_timeout 30s这个配置比无限执行好但对于在线问数仍可能太宽松。假设数据库同时允许 20 条 Agent SQL那么 20 条 30 秒级查询足以把资源持续占住很久。更合理的方式是从服务目标反推预算。例如用户感知 P953 秒左右 端到端 Request Budget5 秒 单 SQL 执行预算3 秒 锁等待预算300 ms 队列等待预算250 ms 事务最长生命周期4 秒 最大结果行数200这里最重要的设计是超时不是一个参数而是一组由外向内逐层收紧的时间预算。3. 复现过程为什么只设 statement_timeout 仍然会出问题3.1 第一版完全没有超时最简单的数据库调用cur.execute(sql)rowscur.fetchall()如果 SQL 运行 90 秒连接就可能被占用 90 秒。更糟的是上游客户端可能在第 5 秒已经放弃等待但数据库查询仍然继续执行。这会形成一种非常浪费的状态用户已经失败 数据库仍然在执行3.2 第二版只设置 statement_timeoutPostgreSQL 18 官方文档说明statement_timeout会终止执行时间超过阈值的语句默认值 0 表示关闭限制。针对 Agent 专用会话可以使用SETLOCALstatement_timeout3000ms;这能解决“SQL 无限运行”但仍然不能覆盖所有资源问题。锁等待不是执行计算查询可能不是慢而是在等待锁。因此还需要SETLOCALlock_timeout300ms;PostgreSQL 将lock_timeout定义为获取表、索引、行等数据库对象锁时的等待上限它只计算锁等待不等同于整个 SQL 执行时间。SQL 完成不代表事务结束代码异常时可能出现BEGIN → SELECT 完成 → 客户端异常 → 事务保持打开PostgreSQL 18 还提供transaction_timeout idle_in_transaction_session_timeout前者限制事务总生命周期后者专门处理“事务打开但会话长时间没有继续发送命令”的场景。长期 idle transaction 可能阻碍旧版本清理、增加表膨胀风险也可能长期持有某些锁。推荐关系通常是lock_timeout statement_timeout transaction_timeout request budget例如300ms 3000ms 4000ms 5000ms3.3 第三版把连接池大小当成 SQL 并发限制假设连接池最大连接数是50这不代表数据库适合同时运行 50 条复杂聚合 SQL。连接池解决的是连接建立与复用问题而 Agent 查询并发应该有独立上限。例如数据库连接池30 Agent SQL 最大执行并发12这样才能给元数据查询、健康检查和其他后台操作保留容量。4. 方案实施KFS MCP Server 的资源治理闭环4.1 第一层端到端 Request Budget假设REQUEST_BUDGET 5000ms它应该覆盖Agent 推理后的工具调用 → MCP 网络传输 → 排队 → SQL 执行 → 结果序列化 → 返回如果请求到达 KFS MCP Server 时已经消耗 4.8 秒就没有必要再启动一条理论上最多可运行 3 秒的 SQL。可以将调用开始时间或 deadline 下传remaining_msdeadline_ms-now_msifremaining_ms500:return{ok:False,code:REQUEST_BUDGET_EXHAUSTED}这样可以避免上游已经超时、数据库仍继续运行的资源浪费。4.2 第二层在获取数据库连接之前做并发限制最小实现global_semasyncio.Semaphore(12)awaitasyncio.wait_for(global_sem.acquire(),timeout0.25)如果 250ms 内拿不到槽位{ok:false,code:RESOURCE_BUSY,retryable:true}这里有一个非常重要的顺序先获取 Query Slot 再获取 Database Connection而不是先拿数据库连接 再等待执行槽位后一种方式会让排队请求提前占满连接池连接池本身变成昂贵的等待队列。4.3 第三层并发限制要分全局、租户、用户生产环境建议至少有concurrency:global:12per_tenant:6per_user:2这样同时解决三个问题global保护数据库总容量per_tenant避免一个业务部门吃掉全部资源per_user防止单个用户多窗口并发占满系统。安全控制与资源控制的共同特点是不能只判断 SQL 本身还要判断“谁在什么上下文里执行它”。4.4 第四层使用短队列禁止无限等待并发满后不能无限排队。例如queue_wait 250ms250ms 内没有槽位就返回RESOURCE_BUSY对于在线系统快速而明确的失败通常比“排队 8 秒之后再超时”更好。Agent 可以根据结构化错误向用户解释当前在线查询容量已满请稍后重试。但应限制自动重试次数否则可能形成 retry storm。4.5 第五层数据库原生四类超时工具获取连接后设置SETTRANSACTIONREADONLY;SETLOCALlock_timeout300ms;SETLOCALstatement_timeout3000ms;SETLOCALtransaction_timeout4000ms;SETLOCALidle_in_transaction_session_timeout2000ms;PostgreSQL 18 官方文档对这几种超时做了明确区分statement_timeout限制单条 SQL 执行时长lock_timeout限制每次获取锁的等待时间transaction_timeout限制事务持续时间idle_in_transaction_session_timeout清理长时间停在事务中的空闲会话。对在线问数来说这种分层比单一 30 秒超时更有价值。4.6 第六层结果行数、内存和临时磁盘同样要限制一条 SQL 可能在 1 秒内返回 100 万行。如果代码直接rowscur.fetchall()压力会从数据库转移到KFS 内存 JSON 序列化 网络传输 LLM Token因此建议MAX_ROWS200rowscur.fetchmany(MAX_ROWS)同时关注 PostgreSQL 的work_mem temp_file_limit官方资源文档特别提醒work_mem并不是“一条 SQL 最多使用这么多内存”。复杂查询可能存在多个排序、Hash 等操作每个操作都可能获得相应的内存额度并发会话和并行 worker 还会进一步放大总内存使用。因此 Agent 会话不应随意配置超大的work_mem。例如可以从保守值开始SETLOCALwork_mem8MB;SETLOCALtemp_file_limit256MB;其中temp_file_limit可以限制一个 PostgreSQL 进程用于排序、Hash 等临时文件的磁盘空间超过限制时取消事务。4.7 完整 query_readonly 工具调用Agent 调用{name:query_readonly,arguments:{sql:SELECT channel_id, SUM(amount) FROM orders WHERE created_at CURRENT_DATE - INTERVAL 90 day GROUP BY channel_id,request_started_ms:1786160000000}}KFS MCP Server 的执行顺序应该是1. 检查 request budget 2. 校验 SQL 只读与对象权限 3. 获取 global / tenant / user concurrency token 4. 超过 queue_wait 则 RESOURCE_BUSY 5. 获取数据库连接 6. 开启只读事务 7. 设置数据库 timeout/resource 参数 8. 执行 SQL 9. 最多 fetch 指定行数 10. rollback / reset 11. 释放数据库连接 12. 释放 concurrency token 13. 记录耗时、行数、错误码和 SQL 指纹如果 SQL 超时{ok:false,code:QUERY_TIMEOUT,retryable:false}为什么QUERY_TIMEOUT通常不建议原样重试因为它已经证明当前 SQL 无法在在线预算内完成。更合理的动作是缩小时间范围 减少分组维度 增加过滤条件 使用预聚合表 切换离线任务而不是消耗同样的资源再试一次。4.8 MCP 2026-07-28 的无状态核心与扩容MCP 2026-07-28 规范把核心协议转向无状态请求每个请求都能够携带足够信息落到普通负载均衡后的任意实例工具方法与工具名还可以通过请求头进行路由和授权。这非常适合 KFS MCP Server 水平扩展Load Balancer ↓ KFS-1 KFS-2 KFS-3网关可以基于工具名、客户端身份做限流 授权 路由 指标聚合但要注意协议无状态不代表并发额度天然是集群全局共享的。如果每个实例都配置Semaphore(12)三个实例理论上可能产生 36 个并发数据库查询。所以必须明确12 是 per-instance 还是 per-database / per-cluster如果要做集群级限流可以放在 API Gateway、Redis 令牌桶、服务网格或数据库代理层。5. 结果对比不要把“所有请求都跑完”当成性能目标资源治理评测建议至少看五类指标。5.1 端到端 P95应该从工具请求进入开始计算而不只是 PostgreSQLEXPLAIN ANALYZE的执行时间。因为排队 2 秒 SQL 1 秒用户感知仍是 3 秒。5.2 峰值数据库并发关注数据库真正同时执行多少条 Agent SQL。如果接入 KFS 之后入口并发 50 数据库 active query ≈ 12说明并发保护确实生效。5.3 Queue Wait排队时间应单独记录queue_wait_ms否则 P95 高了之后无法判断是 SQL 慢还是容量满。5.4 Resource Rejection Rate至少区分RESOURCE_BUSY REQUEST_BUDGET_EXHAUSTED QUERY_TIMEOUT RESOURCE_LIMIT DB_CONNECTION_ERROR不要全部叫QUERY_FAILED错误分类越清楚Agent 后续动作才越合理。5.5 Blast Radius一个用户或租户的异常请求能影响多少其他用户。这是资源隔离质量的核心指标之一。本文演示压测结果可以按下面方式展示指标无治理KFS 资源治理P95 请求延迟9.8s2.6s峰值 DB 并发6412队列等待无上限250msSQL 超时无3s最大结果行数无限制200失败分类connection timeoutRESOURCE_BUSY / QUERY_TIMEOUT这些数字是演示基准正式投稿应替换为真实压测或线上观测结果。建议做三组测试。稳态测试并发 5 持续 10 分钟目标是验证治理层不会明显影响正常请求。峰值测试瞬时并发 50观察DB active query connection pool usage RESOURCE_BUSY ratio P95是否仍在预期范围。慢 SQL 混合测试构造80%100500ms 15%12s 5%故意 10s重点验证少量异常 SQL 是否会拖慢大部分正常查询。优秀的资源治理不是把 100% SQL 都执行完成。而是让可承受的查询稳定完成让超出容量与预算的请求尽快、可解释地失败。6. 风险与复盘超时是止损不是 SQL 优化6.1 三秒超时不会把三十秒 SQL 变成三秒 SQL如果一条查询本来要 30 秒statement_timeout 3s只是让它第 3 秒失败。真正的优化还需要检查是否缺索引是否选错事实表是否需要分区裁剪是否使用预聚合表是否应该限制时间范围是否应该把问题转成离线分析。资源治理解决的是“坏 SQL 不无限伤害系统”不是自动解决性能。6.2 超时后必须回滚并正确释放连接PostgreSQL 语句发生错误后事务可能进入失败状态。所以不能catch timeoutreturnerror然后直接把连接放回池里。必须确认cancel rollback reset release否则下一个请求可能拿到异常状态的连接。6.3 客户端超时不等于数据库 SQL 被取消HTTP 客户端 5 秒超时只表示客户端不再等了并不自动证明数据库查询已经停止。是否真正取消取决于驱动、连接生命周期、取消协议以及服务端 timeout。因此数据库原生statement_timeout仍然非常重要。6.4 无限自动重试会制造更大压力错误要分类处理RESOURCE_BUSY → 可以有限重试但必须退避 QUERY_TIMEOUT → 不应原样重试应该改写查询 UNAUTHORIZED → 禁止重试 INVALID_SQL → 可以修正 SQL 后重试如果 Agent 对所有错误统一retry immediately资源高峰会被放大。6.5 单实例 Semaphore 不等于数据库总并发当 KFS 从 1 个实例扩成 3 个实例时12 × 3 36可能瞬间突破原本数据库容量规划。所以所有并发配置都应该明确范围per_user per_tenant per_instance per_cluster per_database最终真正需要保护的是数据库。6.6 并行查询会放大资源消耗PostgreSQL 18 官方资源文档明确提醒并行查询 worker 也是独立执行进程会消耗额外 CPU、内存和 I/Owork_mem等资源在并行场景下也可能被多个 worker 分别使用。因此一个连接并不等价于一个 CPU 执行单元对于在线问数专用角色可以基于真实压测决定是否进一步限制并行查询能力。6.7 本文参数不能直接照抄到生产本文示例global concurrency 12 statement_timeout 3s queue_wait 250ms max_rows 200只是用于说明治理逻辑。真实参数应该综合数据库 CPU 磁盘 I/O 缓存命中率 慢 SQL 比例 平均扫描量 只读副本容量 连接池大小 在线 SLA 峰值用户数通过压测逐步确定。最终复盘AI Agent SQL 资源治理可以归纳成四层。第一层限制“有多少请求能进入数据库”global / tenant / user concurrency第二层限制“请求能等待多久”queue_wait request_budget第三层限制“SQL 能执行多久、消耗多少”statement_timeout lock_timeout transaction_timeout work_mem temp_file_limit max_rows第四层限制“异常会影响多少其他用户”tenant isolation read replica circuit breaker classified errorsKFS MCP Server 的价值不是简单地把大模型与数据库连接起来而是在它们之间建立一个可限流、可超时、可拒绝、可审计、可度量的资源边界。如果继续向生产推进建议按顺序实施给 Agent 建立独立只读数据库账号和连接池明确在线问数 P95 与端到端 request budget设置statement_timeout与lock_timeout增加事务生命周期保护在获取数据库连接之前设置并发闸门使用短队列而不是无限排队限制 max rows、work_mem、临时文件区分 RESOURCE_BUSY 与 QUERY_TIMEOUT建立稳态、峰值、慢 SQL 混合三类压测根据真实容量持续校准参数。做在线问数时真正危险的场景不一定是一条 SQL 报错。更危险的是每一条 SQL 都合法每一条 SQL 都还在跑最后整个数据库一起慢下来。因此企业 AI 数据查询除了 SQL 安全还必须有 SQL资源治理。附录 A最小并发保护代码GLOBAL_LIMIT12QUEUE_WAIT_SEC0.25global_semasyncio.Semaphore(GLOBAL_LIMIT)asyncdefacquire_slot():try:awaitasyncio.wait_for(global_sem.acquire(),timeoutQUEUE_WAIT_SEC)exceptasyncio.TimeoutError:raiseResourceBusy(RESOURCE_BUSY)必须注意顺序先 acquire_slot() 再 acquire database connection附录 B数据库执行预算withpsycopg.connect(DSN,autocommitFalse)asconn:withconn.cursor()ascur:cur.execute(SET TRANSACTION READ ONLY)cur.execute(SET LOCAL lock_timeout 300ms)cur.execute(SET LOCAL statement_timeout 3000ms)cur.execute(SET LOCAL transaction_timeout 4000ms)cur.execute(SET LOCAL work_mem 8MB)cur.execute(SET LOCAL temp_file_limit 256MB)cur.execute(sql)rowscur.fetchmany(200)conn.rollback()附录 C推荐错误码{RESOURCE_BUSY:{retryable:true,meaning:当前并发容量已满},REQUEST_BUDGET_EXHAUSTED:{retryable:false,meaning:当前请求剩余时间不足},QUERY_TIMEOUT:{retryable:false,meaning:SQL 超过在线执行预算应缩小查询},RESOURCE_LIMIT:{retryable:false,meaning:查询超过内存、临时文件或结果集限制}}转载自https://blog.csdn.net/u014727709/article/details/163644272欢迎 点赞✍评论⭐收藏欢迎指正