ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI代理批量创建PostgreSQL数据库的风险与最佳实践

AI代理批量创建PostgreSQL数据库的风险与最佳实践

最近有个趋势值得拿出来聊一聊:AI 代理开始从“帮忙写代码”进化到“直接创建数据库”,而且不是建一两个,是批量建。3月19日这天,我在一个内部讨论群里看到某同事贴出来一条日志,团队里的 AI 代理接到需求后一口气创建了四十多个 PostgreSQL 数据库,接着就是磁盘告警、连接数告警、pg_database 目录一片混乱。当 AI 代理开始批量创建数据库,这不再是一个新鲜的演示功能,而是实打实会出现在你生产环境里的风险源。

这篇内容我会从场景、权限、连接管理、生命周期几个角度展开,聊聊我自己在测试环境和准生产环境里踩过的一些坑,以及一套相对可落地的做法。适合正在做数据库自动化、想给 AI 代理开放数据库操作权限的开发者和运维同学,尤其是那些还没被“批量建库”坑过的人。

1. 一句“帮我建一百个库”背后:AI代理批量建库的真实场景

1.1 需求并非空想:谁真的会让代理去建库

很多人第一反应是“谁会这么没事干”。但只要你接触过稍微复杂的研发流水线,就会知道批量建库的需求到处都是。

最常见的是分支环境。某团队把前端、后端、数据迁移脚本全部收进同一个流水线,每次提交一个 feature 分支,流水线就想跑一次完整的集成测试。测试环境是共享的,但数据库必须隔离。以前的做法是人肉创建一个db_test,跑完再清数据,现在 AI 代理天然适合把“创建测试库、跑迁移、跑用例、删库”这条链路串起来。

还有一类是数据分析场景。业务同学会用自然语言向数据平台提出需求,比如“帮我把这三个渠道的数据从 ODS 层迁到分析库”,AI 代理如果拥有数据库操作权限,就可能在底层执行 CREATE DATABASE 来建立一个新分析库。这件事听起来合理,但批量执行时,问题会从“能不能建”迅速转移到“建完之后怎么办”。

无论是哪种场景,核心逻辑都一样:以前建库是一个低频、需要审核的动作,现在被 AI 代理变成了高频、无人逐一把关的自动化步骤。这个变化才是真正值得警惕的。

1.2 批量建库的第一个拦路虎:连接数限制

很多人第一次写“循环创建数据库”脚本时,会习惯性地为每个数据库单独开一个连接。在 PostgreSQL 里,这个习惯很快就会撞墙。

默认max_connections通常是 100,其中还保留了一部分给超级用户使用(superuser_reserved_connections默认是 3)。如果你有 5 个并发流程,每个流程同时开 10 个连接去建库,很可能连还没建完就报sorry, too many clients already。

这类问题在单次建库时几乎不存在,但一旦让代理“批量操作”,就变成一个必须提前设计的约束。

我自己的做法是:所有批量操作复用同一个维持连接(或者一个很小的连接池),把“连接”当作一种稀缺资源来管理。建库这个动作本身是元数据操作,不需要跑到目标库里做任何事,所以一个连接完全够用。真正需要多个连接的是建完后立刻要执行的初始化 SQL,那部分应该拆分到单独的池子里去考虑。

1.3 比连接更早爆炸的:命名和目录纪律

连接数还能通过并发控制压住,最让人头疼的是命名。

AI 代理生成的数据库名五花八门,常见的有db_final_v2、project_test(3)、20250319_analysis、hello world。这些名字在创建时一般没问题,但会立刻给连接配置、工具链、监控告警带来灾难。

比如名字里的空格和小括号,在 JDBC/URI 链接串里必须跑去编码;大写字母在某些环境里会被自动转小写,导致配置对比一直对不上。更麻烦的是,如果没有统一的标签体系,等这波库跑完测试之后,根本分不清哪些能删、哪些是正在用的业务库。

所以我现在的硬性要求是:任何代理生成的库名,必须走一个命名模板。大致是环境前缀 + 业务代号 + 使用者标识 + 日期,例如ci_idp_shaw_0319_001。这套规则不是在代理层写一句“请你规范命名”就完事,而是要在代码或脚本里用正则强制校验,不符合就直接拒绝执行。代理自然语言生成的名字,绝不能直接拼接到 CREATE DATABASE 语句里。

2. CREATE DATABASE 不是简单循环:批量创建的正确姿势

2.1 没有 IF NOT EXISTS:幂等的三种写法

PostgreSQL 的CREATE DATABASE不像CREATE TABLE那样支持IF NOT EXISTS,所以“重复执行同一个建库任务”必须自己处理幂等。

最直接的方法是先查pg_database:

SELECT 1 FROM pg_database WHERE datname = 'ci_idp_shaw_0319_001';

如果结果为空,才执行建库。放到 shell 里可以这样写:

db_name="ci_idp_shaw_0319_001" exists=$(psql -Atqc "SELECT 1 FROM pg_database WHERE datname='$db_name'" postgres) if [ -z "$exists" ]; then psql -c "CREATE DATABASE $db_name OWNER = app_role TEMPLATE = template1 ENCODING = 'UTF8'" postgres fi

注意最后那个postgres,指的是连接到一个已存在的维护库,而不是目标库。因为刚创建的库还不能保证可连接,用维护库来执行 DDL 是最稳妥的。

第二种写法是用pg_create_database这个函数吗?其实 PostgreSQL 没有公开提供这个 C 函数给 SQL 调用,所以不建议用。最靠谱的就是上面的查询加创建,在代理代码里再包一层异常处理。

第三种写法更“代理化”:让 AI 代理不要直接拼 SQL,而是调用一个内部建库 API。API 内部实现完整幂等逻辑,代理只需要传参数。这样即使模型偶尔产生幻觉,也不至于把语法写错。

2.2 串行还是并行:不要只盯着并发数

建库过程中,PostgreSQL 会把模板库的数据文件复制到新库目录里,文件拷贝是 I/O 密集操作。如果你建的是空模板库,单个操作可能很快;如果模板库里预装了扩展、公共表结构,那每个新库都需要复制几百 MB 甚至几个 GB 的数据,这种场景下并发数就变得极其敏感。

我建议把“默认并发数”压到 3 到 5。不要看到 50 个库就天真地设 50 并发,那会让磁盘 I/O 直接冲高,甚至把正在跑业务的库拖垮。

一个比较合理的节奏是:

  • 首批先创建 5 个库,观察磁盘 IO 指标和 60 秒内的平均写入延迟;
  • 如果延迟稳定,再逐步把并发数扩大到 8 或 10;
  • 单次批量建库总数量超过 20 个时,建议分成多批,每批之间停几秒。

这个策略不是高科技,而是为了在“快速交付”和“不干扰现有业务”之间找平衡点。

2.3 模板库选错了,批量建库就是批量翻车

默认情况下,新建数据库会从template1拷贝。template1可以包含你认为所有新库都该有的配置,比如pgcrypto扩展、常用函数、默认权限。

但这里有一个坑:如果你在template1里放了自定义数据表,或者有一个长事务长时间没有结束,那么每次CREATE DATABASE都可能会等待或失败。批量建库时,这种偶发性等待会被放大,表现为某些库建到一半卡住,后面所有库都排队。

安全一点的做法是:

  • 建库里尽量显式指定TEMPLATE = template1或TEMPLATE = template0;
  • 如果业务库有强一致性模板需求,单独维护一个template_ci,而不是把所有自定义内容都堆到template1;
  • 不要在template1里放业务数据表,它只应该承担“默认配置”的角色;
  • 定期检查模板库里是否有长时间空闲事务。

我实际见过一个团队,因为模板库里放了一批模拟数据,代理每建一个库就要复制这些数据,十几分钟之后磁盘空间就爆掉了。模板库选错,批量场景下就是翻车现场。

3. 权限边界:让代理可以犯错但不会闯祸

3.1 最小权限:只给 CREATEDB,不给超级权限

如果你把postgres超级用户账号直接交给 AI 代理,那等于把数据库的生死权交给一个有时会胡说八道的模型。更合理的做法是创建一个专门的角色,只给“建库”这一项权限。

CREATE ROLE db_provisioner LOGIN PASSWORD 'xxxx' CREATEDB; GRANT CONNECT ON DATABASE postgres TO db_provisioner;

这个角色不能创建角色,不能修改 PG 系统表,不能任意 drop 数据库。有些环境里你想限制它只能把库建到某个表空间,可以提前创建表空间,再把权限收窄。但要注意,PostgreSQL 在表空间层面的权限控制没有 MySQL 那么细,更多还是要靠代理层的路由来控制逻辑。

代理建库时,所有通过CREATE DATABASE ... OWNER = app_role把拥有者指定给业务角色,不要让代理创建的库 owner 也是db_provisioner。否则后面的权限回收会变得很绕。

另外,建库之后的初始化 SQL 里,如果涉及CREATE EXTENSION、GRANT ALL ON SCHEMA public TO app_role这类操作,建议把这些逻辑写死在模板脚本里,代理只触发执行,不生成内容。

3.2 数量上限和磁盘配额,PostgreSQL 没内置自己补

PostgreSQL 本身没有“一个用户最多能建多少库”的限制。数据库数量完全受磁盘空间和pg_database表里的元数据影响。所以数量控制只能靠外部机制补齐。

最简单的是在 API 层记录:先查询当前总库数,再查询目标前缀下有多少库。超过阈值就返回失败。

比如:

SELECT count(*) FROM pg_database WHERE datname LIKE 'ci_%';

如果这个数字已经超过 30,代理的建库请求就会被拦截。这样做最大的好处是,即使某个 prompt 的意图是“给每个机器人都建一个测试库”,到达数据库层之前已经被限流了。

磁盘配额方面,如果跑在云上,可以靠底层磁盘容量监控;如果跑在自己机器上,可以考虑给 PostgreSQL 的数据目录挂在独立分区,并设置 quota。数据库管理的边界往往不是数据库软件本身,而是操作系统和文件系统的配合。

3.3 审计与追溯:至少能回答“谁建的、用来干嘛”

AI 代理批量建库后,你一定会遇到这个灵魂拷问:这个库是谁建的?什么时候建的?怎么证明它没用?为了能回答这些问题,我建议不要在代理端直接执行 SQL,而是封装一个统一入口。

这个入口可以是一个 只有几百行代码的内部服务。代理提交一个 JSON 请求,里面包含库名、用途、关联的变更单号、保留时长。服务校验通过后执行建库,并把审计记录写进一张普通表里。

这样出现异常时,你能立刻查到建库请求是从哪条会话发起的,省去翻日志的功夫。更关键的是,如果将来要规范化,也可以基于这套审计数据做容量规划。

4. 建库只是开始:连接编排与生命周期管理的坑

4.1 连接池面对动态库名时的无力感

很多应用侧的连接池配置是静态的,比如spring.datasource.url里写死了目标库名。AI 代理批量创建了几十个库之后,应用要连接这些新库,如果还需要人工改配置,那批量建库的意义就少了一半。

但我必须说一句实在话:连接池应对动态库名的能力,远没有大家想象的那么好。哪怕是你非常熟悉的PgBouncer,动态库也需要配置或动态注册。现实里最稳的方案是,让应用访问层接受“库名作为路由参数”,在代码里用连接工厂动态创建DataSource,同时控制单例数量和生命周期。否则每建一个库就新增一组连接池配置,最终会变成一团乱麻。

如果你的场景里“动态库”数量会超过几十个,我不建议一开始就追求全自动接入。可以先实现“建库自动通知、应用半年自动加载新库配置”,给运维留出缓冲时间。

4.2 下线比上线难:DROP DATABASE 之前的必修课

建立一个库只需要一条 DDL,删除一个库却很可能卡住。因为 PostgreSQL 不允许删除还有活动连接的数据库。

如果你直接用:

DROP DATABASE ci_idp_shaw_0319_001;

大概率会得到报错:database "xxx" is being accessed by other users。

正确流程应该是:

  1. 先禁止新连接进入:
ALTER DATABASE ci_idp_shaw_0319_001 WITH ALLOW_CONNECTIONS false;
  1. 终止已有连接:
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'ci_idp_shaw_0319_001';
  1. 再次确认无连接后,执行 DROP DATABASE。

自动化回收脚本里一定不要漏掉第二步。漏掉的话,删除任务会一直挂起,看起来就像代理“卡住了”。

还有一点非常关键:DROP DATABASE 是没有后悔药的。AI 代理可以频繁建库,但建议让它默认不拥有删除权限。真要删除,必须在 API 层走“先标记下线、观察一段时间、再物理删除”的流程。

4.3 如果场景允许,优先 schema 隔离而不是建库

批量建库这个需求看起来很“自动化”,但有时候是在错误抽象级别上解决问题。

如果多个测试团队需要独立环境,但业务之间并不需要独立的数据库实例级隔离,那么在同一个实例里用多个 schema 也足够了。也就是每个团队创建schema team_a、schema team_b,然后通过search_path来控制默认访问。

对比起来:

  • 建库要处理连接数、权限、备份、模板,成本高;
  • 建 schema 只需要CREATE SCHEMA和GRANT,轻量得多;
  • 同实例多 schema 可以用同一个连接池,天然避开了“动态库名”的配置难题。

当然,如果需求是彻底的资源隔离、独立备份、独立账号体系,那数据库级隔离是合理的。但如果是“跑个测试、跑完删掉”,schema 隔离至少应该被列入选项。让代理先选成本最低的方案,这是我一直坚持的原则。

5. 我实测的一组数据和踩坑记录

5.1 一个很简单的基准:50 个库,串行还是并发

我在一台用于 CI 测试的虚拟机上跑过一个非常简单的基准:2 核 4GB 内存的规格,模板库大小约 800MB(里面有一些公共扩展),连续创建 50 个同名前缀的库。

串行执行,大概每分钟能建 6 到 8 个库,50 个库跑完约 7 分钟。整体温和,磁盘 I/O 峰值不高。之后我把并发数提到 5,确实快了不少,总耗时缩短到不到 3 分钟,但系统负载明显上升,中间有几次磁盘利用率接近饱和。如果这台机器上还跑着业务查询,这个并发数是不可接受的。

所以我的最终结论不是“并发越高越好”,而是“根据实例规格和业务容忍度,选择一个能让系统负载保持在水位线以下的并发值”。建议 5 并发之前先观察 60 秒,稳了再继续。

5.2 踩坑实录:从命名到授权的五个反面案例

我这里总结几个实际遇到过的反面案例,全是真实发生过的,细节稍微改了一点。

第一个是命名带连字符。代理生成的库名叫ci-api-service,然后拼到 JDBC URL 里,解析时把ci-api当成了 host 的一部分,应用连了半小时都报连接失败,人看着控制台完全摸不着头脑。

第二个是忘记授权。代理建库之后,应用角色连接上去,结果 schema public 下没有任何权限,SELECT直接报 permission denied。原因是默认public schema的权限机制在不同版本里行为不完全一致,最好在建库脚本里显式加上:

GRANT ALL ON SCHEMA public TO app_role;

第三个是模板库里的长事务。团队为了给新库统一初始化数据,在 template1 里跑了一些测试插入,但事务一直没提交,结果后续所有 CREATE DATABASE 都被卡在等待模板库事务结束上。这不是代理的问题,但代理批量建库会把这种隐藏问题瞬间放大。

第四个是连接数打满。我曾见过一个代理尝试用多线程同时建库,每个线程一个连接,10 个线程没问题,30 个线程直接把 max_connections 干爆,导致生产库的正常业务连接也被挤掉。从那以后,凡是代理发起的批量操作,我都强制走统一入口,入口只允许复用 3 到 5 个连接。

第五个是删库脚本没有 kill 连接。批处理删库时,有几个库被应用侧定时任务仍占着连接,DROP DATABASE 一直等到超时,代理就反复重试,越积越多。按前面说的三步流程处理一次之后,再没出过这个问题。

5.3 现在的操作原则:让代理走 API,不让它碰 psql

踩过这些坑之后,我现在的操作原则其实很朴素:AI 代理可以负责“拆解需求、编造参数、解释结果”,但所有 DDL 的动作都只能通过一个受控的 API 发出,代理本身不直接持有数据库连接串。

这个 API 需要做什么?至少做到四件事:

  • 幂等校验:库已经存在就直接返回“已存在”,不重复创建;
  • 命名校验:不符合规则的请求直接拒绝;
  • 资源限额:检查当前库数量、磁盘水位、并发状态;
  • 审计日志:记录调用者、会话、用途、保留期限。

这样即使 AI 代理对某个词的理解出现了偏差,最终落到数据库上的操作也会被约束在一个可控范围内。

最后再分享一个细节:我一般会让代理在建库请求里强制带上“保留时长”。比如测试库默认保留 7 天,在这 7 天里无论它有没有被连接过,到期后都会进入下线流程。这个逻辑并不复杂,但它让“批量创建”变成了一个可自愈的闭环。数据库目录干净了,AI 代理犯错之后造成的破坏也就能控制在最小范围。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进