Bytebase Plan Check Run 运行时派生重构:从冗余存储到按需计算的配置推导
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
导读
本篇文章深入解析 Bytebase 开源仓库中的一份核心设计文档(docs/plans/2026-01-05-plan-check-run-runtime-derivation.md):如何将 Plan Check Run(计划检查运行)的配置从"随运行持久化一份完整副本"重构为"运行时从关联 Plan 实时推导"。读完本文,你将理解 Bytebase 数据库变更治理链路中计划检查的数据模型演进、config/payload两列的迁移清理方案,以及派生函数在调度器(Scheduler)中的实际调用流程,可直接对照仓库源码继续深入。
一、背景与动机:为什么不再存储一份自包含配置
Bytebase 的 Plan Check(计划检查)是变更审批前的自动校验环节,例如对 SQL 进行语句评审(Statement Advise)、生成摘要报告(Statement Summary Report),以及在开启 gh-ost 在线变更时执行 ghost 同步检查。
在重构之前,每次计划检查运行时,系统会通过getPlanCheckRunFromPlan()从 Plan 复制一份配置存入 Plan Check Run 记录中,形成"自包含"的持久化副本。该设计文档指出这种做法带来三个问题:
- 简化代码:需要移除
getPlanCheckRunFromPlan()中的配置复制逻辑; - 减少存储:避免存储与 Plan 重复的冗余数据;
- 防止过期:Plan Check Run 配置一旦与 Plan 脱钩,就可能与 Plan 的实际状态不同步(staleness),导致检查基于过时配置执行。
重构前的数据冗余
设计文档以表格清晰刻画了重构前plan_check_run表中重复存储的字段:
| 字段 | 在 Plan 中的位置 | 在 Plan Check Run 中的位置 |
|---|---|---|
sheet_sha256 | ChangeDatabaseConfig | CheckTarget |
enable_prior_backup | ChangeDatabaseConfig | CheckTarget |
enable_ghost | ChangeDatabaseConfig | CheckTarget |
ghost_flags | ChangeDatabaseConfig | CheckTarget |
targets | Spec 级别 | 按每个CheckTarget展开 |
其中config列以 JSONB 存储PlanCheckRunConfig(内含上述重复字段),而payload列则始终未被使用(reserved)。
二、核心洞察:一运行一 Plan 的唯一性约束
该重构成立的前提,是设计文档强调的一个关键约束:
每个 Plan 只有一个 Plan Check Run(
plan_id上存在唯一约束)。当计划检查被重新执行时,旧的运行会被直接替换。
由此可以推出三点结论:
- 无需保留历史配置:旧运行被替换,历史配置没有保留价值;
- 结果自带目标信息:检查结果中包含 target 信息,天然自描述;
- 配置可以从当前 Plan 状态推导:由于运行始终与最新 Plan 关联,运行时推导必然拿到最新状态。
这三点共同保证了"运行时派生"不会引入正确性问题——检查永远基于 Plan 的最新配置执行,反而从根本上消除了配置过期风险。
三、数据模型变更:裁剪plan_check_run表
移除的列
config列(JSONB,存储PlanCheckRunConfig)payload列(未使用,预留)
保留的列
| 列 | 说明 |
|---|---|
id | 主键 |
created_at/updated_at | 创建/更新时间 |
plan_id | 指向 Plan 的外键 |
status | 运行状态(RUNNING / DONE / FAILED / CANCELED) |
result | JSONB,存储PlanCheckRunResult |
Proto 层变更
设计文档要求对proto/store/store/plan_check_run.proto进行如下清理:
- 删除
PlanCheckRunConfigmessage; - 删除
CheckTargetmessage(仅用于 config); - 如果 API 层向客户端暴露了 config,则同步更新 API proto(
proto/v1/plan_service.proto)。
对照当前仓库的 proto/store/store/plan_check_run.proto,该文件已只保留PlanCheckRunResult(含Result、SqlSummaryReport、SqlReviewReport等)以及ChangedResources系列 message,PlanCheckRunConfig与CheckTarget均已不在其中——证明上述清理已落地。同时,PlanCheckRunResult.Result中保留了target(格式为instances/{instance}/databases/{database})、type、sheet_sha256字段,这与设计文档"结果自带目标信息、结果自描述"的结论一致。
Store 层变更
- 从
PlanCheckRunMessage中移除Config字段; - 更新 CRUD 操作,使其不再读写 config/payload 列。
四、运行时派生:纯函数式的配置推导
派生结构体
设计文档给出的核心方案是:getPlanCheckRunFromPlan()不再创建并持久化 config,而是返回一个内存中的结构体:
type DerivedCheckTarget struct { Target string // database resource name SheetSHA256 string // from plan spec EnablePriorBackup bool EnableGhost bool GhostFlags map[string]string Types []storepb.PlanCheckType }在仓库的实际实现中,该结构体以 backend/runner/plancheck/check_target.go 中的CheckTarget落地,字段与设计一致,并补充了注释说明:
Target:规范化的数据库资源名,形如instances/{instance}/databases/{database}或projects/{project}/instances/{instance}/databases/{database};SheetSha256:SQL sheet 的内容哈希;EnablePriorBackup:是否在迁移前开启备份;EnableGhost:是否启用 gh-ost 在线迁移;GhostFlags:gh-ost 的配置参数;Types:针对该目标需要执行的计划检查类型。
Executor 流程
设计文档定义了重构后的执行流程:
- 获取 Plan Check Run(包含
plan_id、status); - 通过
plan_id获取 Plan; - 调用派生函数得到 targets 与 config;
- 针对每个 target,使用派生出的 config 执行检查;
- 将结果写入
result字段。
仓库中的实际调度实现
对照 backend/runner/plancheck/scheduler.go,runPlanCheckRun()完整实现了上述流程:
- 通过
s.store.GetPlan()按projectID + planUID获取 Plan; - 校验
plan.Config.GetApprovalInputVersion()与运行声明的approvalInputVersion一致,否则将该运行标记为 CANCELED("stale plan check run")——这是并发场景下防止旧运行写入过期结果的兜底机制; - 获取 Project,若项目已归档(
project.Deleted)则取消运行; - 调用
GetDatabaseGroupForPlan()(见 backend/runner/plancheck/database_group.go)在需要时解析数据库组并展开匹配的数据库列表; - 调用
DeriveCheckTargets()在运行时推导 targets; - 遍历 targets,通过
s.executor.RunForTarget()(接口定义见 backend/runner/plancheck/executor.go)执行检查并聚合结果; - 依据执行结果调用
UpdatePlanCheckRunIfApprovalInputVersion()将运行标记为 DONE / FAILED / CANCELED,结果写入PlanCheckRunResult; - 运行结束后向
ApprovalCheckChan发送信号触发审批查找(approval finding),进而推动 rollout 创建。
值得注意的是,步骤 8 表明 Plan Check 是审批链路的前置闸门:只有当检查 DONE 之后,审批与发布流程才会继续推进。
五、派生函数的实现细节:组展开、CI 采样与 gh-ost 识别
设计文档特别强调:"派生逻辑本身保持不变(数据库组展开、CI 采样)"。仓库中的 backend/runner/plancheck/derive.go 展示了完整的派生实现,它同时印证并深化了设计:
1. 两个入口,两种采样策略
func DeriveCheckTargets(ctx context.Context, s *store.Store, project *store.ProjectMessage, plan *store.PlanMessage, databaseGroup *v1pb.DatabaseGroup) ([]*CheckTarget, error) { return deriveTargets(ctx, s, project, plan, databaseGroup, true) } func DeriveReviewTargets(ctx context.Context, s *store.Store, project *store.ProjectMessage, plan *store.PlanMessage, databaseGroup *v1pb.DatabaseGroup) ([]*CheckTarget, error) { return deriveTargets(ctx, s, project, plan, databaseGroup, false) }DeriveCheckTargets:应用 CI 采样。Plan Check 本质是 CI 校验,采样用于控制检查成本;DeriveReviewTargets:不采样。评审运行(review run)的 DONE 意味着每一个 (spec, target) 单元都被评估过,必须看到完整目标集。
2. 目标展开逻辑
deriveTargets()遍历plan.Config.Specs,按 spec 类型分支处理:
CreateDatabaseConfig:创建数据库不执行计划检查;ChangeDatabaseConfig:若Release != ""(发布场景)则跳过计划检查;否则:- 若目标列表中唯一目标恰为数据库组名,则用
databaseGroup.MatchedDatabases展开; - 否则直接使用
Targets列表; - 在
applySampling为 true 且project.Setting.GetCiSamplingSize() > 0且目标数超出采样上限时,截断到前samplingSize个。
- 若目标列表中唯一目标恰为数据库组名,则用
3. gh-ost 指令解析
派生函数会通过getSheetContent()依据SheetSha256读取 SQL sheet 内容,调用ghost.IsGhostEnabled()判断是否启用 gh-ost,若启用则调用ghost.ParseGhostDirective()解析 gh-ost 指令得到GhostFlags;最终根据是否启用 gh-ost 决定追加PLAN_CHECK_TYPE_GHOST_SYNC检查类型:
types := []storepb.PlanCheckType{ storepb.PlanCheckType_PLAN_CHECK_TYPE_STATEMENT_ADVISE, storepb.PlanCheckType_PLAN_CHECK_TYPE_STATEMENT_SUMMARY_REPORT, } if enableGhost { types = append(types, storepb.PlanCheckType_PLAN_CHECK_TYPE_GHOST_SYNC) }4. 数据库组解析
backend/runner/plancheck/database_group.go 中的GetDatabaseGroupForPlan()负责:识别 Plan 的 change-database spec 是否指向数据库组 → 查询数据库组 → 列出项目下全部数据库 → 通过utils.GetMatchedDatabasesInDatabaseGroup()计算匹配数据库并填充MatchedDatabases。注释特别提醒:allDatabases必须是完整未过滤的数据库列表,否则组展开会静默不完整。
六、迁移脚本:一次安全的在线 DDL
设计文档给出的迁移 SQL 为:
ALTER TABLE plan_check_run DROP COLUMN config; ALTER TABLE plan_check_run DROP COLUMN payload;该迁移已在仓库中落地为 backend/migrator/migration/3.14/0021##remove_plan_check_run_config_payload.sql,内容与设计完全一致。由于两个列中payload本就未使用、config可由 Plan 在运行时重新推导,删除操作不涉及数据回填或转换,属于低风险的列裁剪迁移。
七、保持不变的部分
设计文档明确列出了重构中"不变"的边界,确保改动不扩散:
PlanCheckRunResultproto 保持不变(按 target 存储实际检查结果);- Plan Check Run 的状态生命周期不变(RUNNING → DONE / FAILED / CANCELED);
- 派生逻辑本身不变(数据库组展开、CI 采样);
- "每个 Plan 一个 Plan Check Run"的唯一约束不变。
这些不变项意味着该重构是纯内部架构优化:对外部 API 消费者、状态机与检查语义均无影响,风险被严格控制在存储层与运行器的实现细节内。
八、涉及文件一览(按设计文档与仓库现状对照)
设计文档给出的改动清单如下,结合当前仓库可以看到各层的实际落点:
| 层 | 文件 | 变更 |
|---|---|---|
| Proto(store) | proto/store/store/plan_check_run.proto | 移除PlanCheckRunConfig、CheckTarget(已落地,当前仅保留PlanCheckRunResult) |
| Proto(API) | proto/v1/plan_service.proto | 若向客户端暴露 config 则移除 |
| 迁移 | backend/migrator/migration/3.14/0021##remove_plan_check_run_config_payload.sql | DROPconfig、payload列 |
| Store | backend/store/plan_check_run.go | 移除Config字段,更新 CRUD |
| API | backend/api/v1/plan_service.go | 简化创建逻辑,抽出派生函数 |
| Runner | backend/runner/plancheck/scheduler.go | 运行时获取 Plan 并推导 targets |
| Executors | backend/runner/plancheck/*.go | 接收派生出的配置(CheckTarget) |
九、测试佐证:派生语义的可验证性
重构后的派生逻辑由单元测试直接守护。backend/runner/plancheck/derive_test.go 中的TestDeriveReviewTargetsSkipsCISampling构造了一个含 3 个目标的 Plan(CiSamplingSize: 1),断言:
DeriveCheckTargets仅返回 1 个目标——"plan checks apply the CI sampling limit";DeriveReviewTargets返回全部 3 个目标——"review must evaluate every target regardless of CI sampling"。
该测试同时验证了设计文档"派生逻辑本身保持不变"与"CI 采样"两条核心承诺,是理解派生语义的最佳入口。
总结
plan-check-run-runtime-derivation是一次典型的"以数据模型简化换取运行时计算"的架构演进:借助"一 Plan 一运行"的唯一约束,Bytebase 将计划检查的配置从持久化副本改为纯函数派生,删除了config/payload两列,把getPlanCheckRunFromPlan()收敛为DeriveCheckTargets/DeriveReviewTargets两个派生入口,并在调度器中按"取运行 → 取 Plan → 派生目标 → 执行检查 → 写回结果"的流程运行。对于想要理解 Bytebase 计划检查、审批门禁与 gh-ost 集成链路的开发者,backend/runner/plancheck 目录下的derive.go、scheduler.go、check_target.go与配套测试是最直接的阅读起点。
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考