Agent Substrate 的 PostgreSQL 模式演进:滚动更新下的迁移契约、Expand-and-Contract 与分区友好约束
2026/9/23 14:46:13 网站建设 项目流程
  • 人工智能
  • AI Agent
  • Agent 沙箱
  • 云原生
  • 容器运行时
  • 零信任

【免费下载链接】substrate

Agent Substrate: the core system

项目地址:https://gitcode.com/GitHub_Trending/substrate7/substrate
点击查看免费下载

本篇指南面向在 Agent Substrate(ateapi)中维护 PostgreSQL 存储层的开发者,系统讲解该项目在滚动更新场景下安全演进数据库模式(Schema)的完整工程约束:先说明"迁移必须跑在就绪之前、逐条提交、失败可续跑"的运行模型,再给出兼容性契约、Schema 变更规则、actors 表可分区性红线、Expand-and-Contract 三步法与迁移文件规范,最后以仓库内的真实迁移文件、校验脚本与测试用例逐一佐证。读完你将能独立编写符合项目规范的迁移文件,并在提交前完成完整的自查与验证。

为什么模式演进是滚动更新里最危险的一环

在 Agent Substrate 中,ateapi 是控制面 API 服务,它把 Agent 的元数据持久化到 PostgreSQL。其核心运行模型在文档 docs/dev/postgresql-schema-evolution.md 中写得很清楚:

  • ateapi在变为 ready(就绪)之前应用 PostgreSQL 迁移:连接建立后会先执行迁移,迁移成功后才对外提供服务。在 atepg.go 的Connect中,顺序是打开连接池 →PingcreateSchemanewPersistence(内部调用applyMigrations),随后主程序才通过 serverboot.Readiness 标记就绪。
  • 滚动更新期间,旧二进制继续服务:Kubernetes 滚动发布会让新旧两个版本的ateapi同时存活,新二进制改 Schema,旧二进制同时读写同一套表。
  • Goose 一次只提交一个迁移:迁移按文件逐个在事务中应用,任何一个迁移失败,它之前已完成的迁移前缀会保留在库中。

由此得出的铁律是:每一个迁移前缀(migration prefix)都必须让旧二进制安全读写。这正是下文所有规则的设计原点。

从源码看,迁移引擎选用 pressly/goose/v3:schema.go通过goose.NewProvider(goose.DialectPostgres, ...)创建 Provider,迁移文件用//go:embed migrations/*.sql打进二进制,账本表固定命名为schema_migrationsgoose.WithTableName("schema_migrations"))。

兼容性契约:六条不可逾越的底线

文档为每次 Schema 变更定义了六条兼容性契约,全部围绕"前一个版本的二进制在任意迁移前缀下都能工作"展开:

  1. 每个迁移前缀都要兼容旧二进制的读写——即任意时刻停住发布,旧进程都不能坏。
  2. 把每个迁移文件边界视为一个持久化的数据库状态——迁移是逐条落地的,不是一批原子操作。
  3. 不得依赖后续迁移来"修复"前面前缀的兼容性——前缀本身必须自洽。
  4. 让新二进制从上一个发布版本的状态出发执行迁移——升级路径是线性的 N-1 → N。
  5. 新二进制在全部待处理迁移完成之前不得就绪——这就把"迁移失败"转化为"服务不 ready",而不是带病上线。
  6. SELECTINSERT中显式列出列名——显式列清单让语句在新列加入后依然语义稳定。

第 6 条在存储层代码中有直接印证:例如 actor.go 的写入是INSERT INTO actors (atespace, name, uid, version, proto) ...,明确枚举列名而非INSERT INTO actors VALUES ...,这样旧二进制写入时即便新二进制已加了列也不会因为列数错位而失败。

Schema 变更规则:偏好在"加法"上做文章

优先做增量(additive)变更

在"新二进制使用新结构之前,先把结构加上"是基本顺序:

  • 新增表、列、索引,要先于依赖它的新代码上线。
  • 新增列必须可空(nullable)或带兼容的数据库默认值,否则旧二进制的新增INSERT会因为漏掉该列而失败。这条规则在 actor.go 这类显式列清单的INSERT语句下体现得尤为直接:新列若NOT NULL且无默认值,旧二进制立刻被卡死。
  • 存储层采用了"原生列 + 完整 protobuf 字节"的混合模型(见 atepg.go 包注释:每张表既有 SQL 需要操作的原生列,也有BYTEA存放完整消息)。读取时用proto.UnmarshalOptions{DiscardUnknown: true}丢弃当前二进制没有描述的字段,并回填默认值(unmarshalStored),这保证了"新副本写入的新字段,旧副本读到也不会崩"——这是数据兼容的又一道防线。

严禁做的事

  • 不得删除或重命名旧二进制还在用的表/列,不得以不兼容方式改变其类型或语义。
  • 不得在旧二进制可能写入违反约束的值时收紧约束不得删除旧二进制依赖的默认值
  • 两种结构并存、两个二进制都能写时,必须让两者保持一致;在新二进制要求新结构之前,先完成存量行的回填(backfill)。
  • 启动阶段不得执行大规模数据回填——这类工作必须另立独立的迁移流程,并先提出方案评审,而不是塞进启动迁移里。

这些规则的精神可以概括为一句话:Schema 是共享资源,变更必须对"仍在运行的所有版本"负责,而不是只对新版本负责。

保持 actors 表可分区:一条"未来选项"红线

项目为未来预留了把actors表按atespace或按name分区、以及其他带atespace列的表按atespace分区的可能性。为此,任何 Schema 变更或查询都不得引入以下三类结构:

  1. 唯一索引/唯一约束遗漏分区键:在带atespace的表上建唯一索引/约束必须包含atespace;在actors上必须包含name。例如在 000001_initial.sql 中,actors的主键就是(atespace, name),天然满足该要求。
  2. 外键引用遗漏分区键:引用这些表的列集合必须包含atespace;引用actors必须用(atespace, name)。初始迁移中actor_egress_policiesFOREIGN KEY (atespace, actor_name) REFERENCES actors(atespace, name)正是这个形态。
  3. 查询不带分区键过滤:对这些表的查询必须过滤atespace;对actors的查询还必须过滤name。按定义结果横跨所有分区的语句(如全局列表)必须登记在TestActorsTablePartitionable中;其他会读到多个分区的语句需要在该测试中获得社区同意的豁免。

这条红线在测试侧由 partition_test.go 的TestActorsTablePartitionable强制:它把一份全新迁移后的 Schema 实际按atespace(对actors再按name)改造成哈希分区(9 个分区,见partitions常量),然后在这套分区布局上跑完整的 store 契约套件(storecontract.RunContractTests),并用 pgx 的QueryTracer对每条语句做EXPLAIN,一旦发现某个语句的计划读了多个分区就报错。测试还显式验证了两类反例:CREATE UNIQUE INDEX actors_uid_key ON actors (uid)这种遗漏分区键的唯一索引会被 PostgreSQL 拒绝;WHERE uid = $1WHERE atespace = ANY($1)这类不带/多值分区键的谓词会触发扇出告警。

Expand and Contract:替换或删除结构的标准三步法

当需要替换或删除某个结构时,采用"先扩后缩"的三步发布序列:

  1. Release N:新增新结构,同时保留旧结构(新二进制开始写新结构,旧二进制继续用旧结构)。
  2. Release N+1:停止使用旧结构(所有读写都切到新结构,旧结构变成死数据)。
  3. Release N+2:删除旧结构。

该序列保证:发布过程中任意时刻,前一个版本的二进制都能兼容当前 Schema;即便二进制临时回滚(binary rollback),由于二进制回滚不会回滚 Schema,前一个版本仍能正常运行——这正是为什么不能依赖"回滚迁移"来兜底,而必须用 expand-and-contract 让每个前缀自洽。

迁移文件规范:目录、命名与内容红线

迁移文件统一存放在cmd/ateapi/internal/store/atepg/migrations。文档规定的硬性规则如下:

  • 使用下一个顺序编号的NNNNNN_name.sql文件名(6 位数字前缀 + 小写下划线命名)。
  • 恰好包含一条-- +goose Up注解。
  • 只使用 SQL 迁移。
  • 不添加 down 迁移(Goose 的-- +goose Down被明确禁止)。
  • 不使用NO TRANSACTION(每个迁移必须在事务中执行)或ENVSUB
  • 不添加 SQL 事务控制语句(BEGIN/START TRANSACTION/COMMIT/ROLLBACK),事务交给 Goose 控制。
  • 不把IF NOT EXISTS用作 Schema 变更的保护伞(失败必须显式暴露,而不是悄悄跳过)。
  • 每个启动迁移保持简短。

真实范例:000001_initial.sql

仓库当前唯一一份迁移文件 000001_initial.sql 就是完全合规的样板:文件以-- +goose Up开头,随后依次创建atespacesactors(主键(atespace, name))、actor_egress_policiesactor_templatestagsworkersworker_assignments、按created_at范围分区的worker_outbox(含UNLOGGED的 DEFAULT 分区与worker_outbox_trim高水位表)、leases等表。注意其中注释说明了worker_outboxcreated_at必须用clock_timestamp()而非now()(后者在事务开始时冻结,会让慢事务写入已过期的分区)——这类在迁移文件里保留设计动机的注释,正是后续维护者判断"能不能动这行 DDL"的关键上下文。

版本边界:v1 之前可改,v1 之后只增不改

  • 首个稳定 v1 发布之前:允许修改或合并(squash)迁移文件;迁移历史一旦变化,需要重建开发数据库(DROP SCHEMA ... CASCADE后重跑即可,测试代码正是这样做的)。
  • 首个稳定 v1 发布之后:已发布的迁移文件不得修改或删除,出错只能通过新增迁移来纠正。
  • 不要手工编辑schema_migrations账本表

校验脚本:把规则变成 CI 门槛

hack/verify/postgresql-migrations.sh 将上述规则脚本化,逐一检查每个迁移文件:

  • 文件名必须匹配^([0-9]{6})_[a-z0-9_]+\.sql$,且版本号必须严格等于期望的下一个顺序号(expected从 1 递增);
  • 必须包含-- +goose Up,不得包含-- +goose Down-- +goose no transactionIF NOT EXISTS-- +goose ENVSUB,不得出现行首的BEGIN/START TRANSACTION/COMMIT/ROLLBACK
  • 可选参数released-ref用于指定已发布 tag:脚本会检查自该 ref 以来迁移目录是否存在修改(-diff-filter=MD的改动或删除),存在则报错 "Do not change or delete released PostgreSQL migrations."。不传参数时,脚本会自动从git tag --merged HEAD中挑出最新的vMAJOR.MINOR.PATCH语义化版本 tag 作为 released-ref。

提交前自查清单与验证命令

文档给出了提交前必须完成的五步:

  1. 找出旧二进制会读写所变更对象的全部操作;
  2. 逐一核对每个新迁移前缀下这些操作是否仍成立;
  3. 某个前缀会破坏操作时,改用 expand-and-contract;
  4. 为 Schema 行为新增或更新测试;
  5. 运行迁移校验脚本和 PostgreSQL 存储测试:
hack/verify/postgresql-migrations.sh go test ./cmd/ateapi/internal/store/atepg

源码佐证:迁移如何安全落地

最后看迁移执行层 schema.go 与配套测试 migrations_test.go,理解"文档规则"背后的实现保障:

  • 版本门槛applyMigrations先读server_version_num,要求 PostgreSQL 13+(xid8pg_current_xact_id等特性依赖),版本不足时给出明确报错而非晦涩的 DDL 失败。
  • 遗留库防护rejectUnversionedSubstrateSchema在 Goose 创建账本表之前检查"存在 Substrate 表但没有schema_migrations账本"的库,直接拒绝,防止把迁移前的旧 schema 误标为已托管。对应测试TestMigrationSchemaStates/tables without metadata验证了该场景不会创建账本。
  • 并发安全:多个副本同时启动时,通过基于hashtextextended计算锁 ID 的 PostgreSQL advisory session lock(lock.NewPostgresSessionLocker,超时 1 秒 x 300 次)串行化迁移。TestMigrationsConcurrentStartup同时起两个Connect,断言每个迁移恰好应用一次;TestMigrationsWaitForInProgressMigration则验证迁移会阻塞等待他人持有的锁。
  • 失败可续跑migrateToLatest把 goose 的PartialError展开,记录已应用前缀后返回错误;TestMigrationFailureLeavesCompletedPrefixAndResumes构造"成功→成功→失败"三个迁移,断言版本 2 保留、失败迁移的事务整体回滚、修正版本 3 后可从版本 2 续跑成功——这正是文档"失败的迁移运行会保留已完成前缀"的落地证明。
  • 就绪语义:迁移在主服务标记 ready 之前完成(main.go 的就绪管理与 atepg.go 的Connect顺序),迁移失败即启动失败,不会带病对外服务。

综上,Agent Substrate 的 PostgreSQL 模式演进不是一套口号,而是"文档契约 + 脚本校验 + 测试强制"的三重闭环:文档定义为什么,postgresql-migrations.sh 检查怎么写,migrations_test.go 与 partition_test.go 验证跑起来是否安全。任何新增迁移都应在这套闭环内完成设计与提交。

  • 人工智能
  • AI Agent
  • Agent 沙箱
  • 云原生
  • 容器运行时
  • 零信任

【免费下载链接】substrate

Agent Substrate: the core system

项目地址:https://gitcode.com/GitHub_Trending/substrate7/substrate
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询