 提升到 (数据库, 发布))
Bytebase 发布级任务设计将 GitOps 迁移任务粒度从 (数据库, 文件) 提升到 (数据库, 发布)【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase导读本文深入解析 Bytebase 开源仓库中《Release-Level Tasks for GitOps》设计方案见 docs/plans/2026-01-02-release-level-tasks-design.md核心目标是将 GitOps 变更任务的粒度从database, sheet/file提升为database, release使每个数据库只对应一个迁移任务从而简化 rollout 管理并大幅降低任务数量。读完本文你将掌握任务模型在 proto 层如何重构oneof source、任务创建逻辑如何从嵌套双重循环简化为单循环、执行器如何在失败后基于 revision 表自然断点续跑以及 API 转换层与前端展示层需要做哪些配套调整。背景为什么需要发布级任务现状与痛点Bytebase 的 GitOps 发布release通常包含多个迁移文件例如V0001.sql、V0002.sql、V0003.sql。在旧模型中任务以(database, sheet)为最小粒度若有 N 个数据库、M 个迁移文件系统会创建N×M个任务每个任务只负责一个文件跑一个数据库对于跨多个数据库的大型发布任务量呈乘法级膨胀。以 10 个数据库 × 20 个文件为例将产生200 个任务。这带来两个直接问题任务爆炸rollout 列表被大量粒度极细的任务淹没管理和排障成本高发布进度难追踪难以一眼看出哪些数据库已经完整完成了整个发布因为完成状态分散在大量 (database, file) 任务上。设计目标新方案将粒度收敛为(database, release)GitOps 发布只创建N 个任务每个数据库一个每个任务执行该发布中该数据库尚未应用的全部文件版本去重与断点续跑依托 revision 表——该表已经记录了每个版本是否成功失败重试时天然跳过已成功版本。这样 10 个数据库 × 20 个文件的大型发布任务数从 200 个降为 10 个任务量削减 95%该比例为设计方案中的示例估算见 设计方案 Metrics 章节。一、Proto 层重构用oneof source表达互斥来源改造前的字段设计改造前Task消息中与来源相关的字段如下详见 设计方案message Task { string sheet_sha256 4; // 单个 sheet 的哈希 string schema_version 10; // 版本号仅发布任务使用 TaskReleaseSource task_release_source 13; // 发布来源标记 // ... other fields }改造后的字段设计message Task { oneof source { string sheet_sha256 10; // 常规非发布任务单个 sheet string release 13; // 发布任务projects/{project}/releases/{release} } // ... other fields // 已移除: schema_version原字段 10被 sheet_sha256 取代 // 已移除: task_release_source原字段 13被 release 取代 }关键设计决策oneof source强制互斥一个任务要么是 sheet 来源、要么是 release 来源杜绝两种来源同时存在导致的歧义复用字段编号sheet_sha256复用 10 号字段release复用 13 号字段——这是向后兼容的关键避免了字段号变更带来的数据迁移删除schema_version该字段仅服务于旧的发布任务语义新版中每个文件的版本信息改由 release 的Payload.Files[].Version提供。仓库中的落地实现当前仓库的 proto/store/store/task.proto 已按该设计落地第 9 行将旧字段区间声明为reserved 4 to 9, 12;防止未来误用被废弃的字段号第 28–36 行正是上述oneof source结构// Source of the tasks SQL content - either a single sheet or an entire release. oneof source { // The SHA256 hash of a single sheet content (hex-encoded). // Used for non-release tasks. string sheet_sha256 10; // The release resource name: projects/{project}/releases/{release}. // Used for GitOps release-based tasks that execute multiple files. string release 13; }同时在Task.Type枚举的注释中明确了执行策略的归属DATABASE_MIGRATE的执行策略由 release 类型VERSIONED/DECLARATIVE或非发布任务的 sheet 内容决定。二、任务创建从 N×M 双重循环到 N 单循环旧逻辑设计文档中Before示意// Creates N×M tasks for _, database : range databases { for _, file : range release.Payload.Files { // Filter by applied versions if alreadyApplied(file.Version) { continue } // Create task(database, file) task : store.TaskMessage{ Payload: storepb.Task{ SheetSha256: file.SheetSha256, SchemaVersion: file.Version, TaskReleaseSource: storepb.TaskReleaseSource{ File: formatReleaseFile(file.Id), }, }, } taskCreates append(taskCreates, task) } }新逻辑设计文档中After示意// Creates N tasks for _, database : range databases { // Create task(database, release) task : store.TaskMessage{ InstanceID: database.InstanceID, DatabaseName: database.DatabaseName, Environment: database.EffectiveEnvironmentID, Type: storepb.Task_DATABASE_MIGRATE, Payload: storepb.Task{ SpecId: spec.Id, Release: c.Release, // 存 release 名而非单个文件 // 移除 SheetSha256、SchemaVersion、TaskReleaseSource }, } taskCreates append(taskCreates, task) }新逻辑带来的收益逻辑更简单去掉了内层文件循环不再需要已应用版本过滤该判断推迟到执行期任务数稳定无论发布含多少文件每个数据库只产生 1 个任务版本去重后置创建期不再判断alreadyApplied统一在执行期基于 revision 表决定跳过哪些文件。仓库中的实际实现设计文档中的函数名getTaskCreatesFromChangeDatabaseConfigWithRelease()在当前代码中体现为 backend/api/v1/rollout_service_task.go 的getTaskCreatesFromChangeDatabaseConfig()。实际实现与设计方案完全一致外层仅遍历databases在循环内根据c.Release是否为空来决定 payload 来源// Set source: either release or sheet if c.Release ! { payload.Source storepb.Task_Release{ Release: c.Release, } } else { payload.Source storepb.Task_SheetSha256{ SheetSha256: c.SheetSha256, } } taskCreate : store.TaskMessage{ InstanceID: database.InstanceID, DatabaseName: database.DatabaseName, Environment: env, Type: taskType, // 统一为 storepb.Task_DATABASE_MIGRATE Payload: payload, } tasks append(tasks, taskCreate)值得注意的实现细节getTaskCreatesFromChangeDatabaseConfig()在创建任务前若c.SheetSha256 ! 会先获取 sheet 内容做ghost directive 校验解析ghost.IsGhostEnabled并校验用户标志且该获取在循环外只做一次注释明确说明c.SheetSha256是循环不变量。这属于任务创建期的前置校验与发布级任务执行期决定的策略互补。三、任务执行单 sheet 执行与发布级执行的分流执行器入口分流设计文档建议在执行器RunOnce中先判断task.Payload.GetRelease()是否非空再决定走发布级执行还是回退到单 sheet 执行。当前仓库 backend/runner/taskrun/database_migrate_executor.go 正是如此// Execute migration based on task type if releaseName : task.Payload.GetRelease(); releaseName ! { // Parse release name to get project ID and release ID projectID, releaseID, err : common.GetProjectReleaseID(releaseName) ... // Fetch the release release, err : exec.store.GetRelease(ctx, store.FindReleaseMessage{...}) ... // Switch based on release type switch release.Payload.Type { case storepb.SchemaChangeType_VERSIONED: return exec.runVersionedRelease(...) case storepb.SchemaChangeType_DECLARATIVE: return exec.runDeclarativeRelease(...) default: return nil, errors.Errorf(unsupported release type %q, release.Payload.Type) } } // Fetch sheet for non-release tasks sheet, err : exec.store.GetSheetFull(ctx, task.Payload.GetSheetSha256()) ... if ghost.IsGhostEnabled(sheet.Statement) { return exec.runGhostMigration(...) } return exec.runStandardMigration(...)实际实现比设计文档更进一步不仅区分了release 任务 vs sheet 任务还按 release 的SchemaChangeType细分出VERSIONED版本化与DECLARATIVE声明式两条执行路径并在执行入口前调用ensureBaselineChangelog()保证数据库存在基线 changelog。版本化发布执行按 revision 表跳过已应用版本设计文档给出了runReleaseTask()的伪代码核心四步是取 release → 列出该数据库现有 revisions → 构建已应用版本集合 → 顺序执行未应用文件、遇错即停。仓库中的runVersionedRelease()database_migrate_executor.go完整实现了这一逻辑// Get existing revisions for this database revisions, err : exec.store.ListRevisions(ctx, store.FindRevisionMessage{ InstanceID: task.InstanceID, DatabaseName: task.DatabaseName, }) // Build map of applied versions appliedVersions : make(map[string]bool) for _, revision : range revisions { if revision.Payload.Type storepb.SchemaChangeType_VERSIONED { appliedVersions[revision.Version] true } }随后创建一条pending changelog覆盖整个 release并只取一次数据库驱动exec.dbFactory.GetAdminDatabaseDriver见第 522 行供所有文件复用接着顺序遍历release.Payload.Filesfor _, file : range release.Payload.Files { // Skip if already applied if appliedVersions[file.Version] { slog.InfoContext(ctx, skipping already applied version, ...) continue } sheet, err : exec.store.GetSheetFull(ctx, file.SheetSha256) ... // Log release file execution exec.store.CreateTaskRunLogS(ctx, ..., storepb.TaskRunLog{ Type: storepb.TaskRunLog_RELEASE_FILE_EXECUTE, ReleaseFileExecute: storepb.TaskRunLog_ReleaseFileExecute{ Version: file.Version, FilePath: file.Path, }, }) // Execute the SQL. if ghost.IsGhostEnabled(sheet.Statement) { err executeGhostMigration(...) } else { _, err driver.Execute(driverCtx, sheet.Statement, opts) } if err ! nil { migrationErr errors.Wrapf(err, failed to execute release file %s (version %s), file.Path, file.Version) break // 遇第一个失败文件即停 } // Create revision for this file r : store.RevisionMessage{ InstanceID: database.InstanceID, DatabaseName: database.DatabaseName, Version: file.Version, Payload: storepb.RevisionPayload{ Release: task.Payload.GetRelease(), File: file.Path, SheetSha256: file.SheetSha256, TaskRun: taskRunName, Type: storepb.SchemaChangeType_VERSIONED, Project: task.ProjectID, }, } _, err exec.store.CreateRevision(ctx, r) ... }几个关键实现要点逐文件记录 revision每个文件执行成功后立即CreateRevision这正是失败重试天然跳过已成功版本的机制来源顺序执行、遇错即停migrationErr一旦非空立即break避免在失败文件之后继续执行可能产生依赖问题的后续文件整体 changelog 状态收敛所有文件处理完后统一SyncDatabaseSchemaToHistory并更新 changelog——migrationErr nil时置为 Done否则置为 Failedghost 兼容即使发布文件含 gh-ost 指令也能走executeGhostMigration路径发布级任务并未牺牲在线变更能力。声明式发布执行与版本化不同runDeclarativeRelease()database_migrate_executor.go要求声明式 release 恰好只有一个文件多文件直接报错且执行后不创建 revision——声明式数据库的版本信息由数据库 schema 自身承载注释明确指出 declarative releases 通过 database schema 本身做版本追踪。这也解释了 proto 注释中执行策略由 release 类型VERSIONED/DECLARATIVE或 sheet 内容决定的含义。四、Store 层TaskPatch 与 payload 序列化设计文档要求更新 backend/store/task.go 中的TaskPatch移除/废弃SchemaVersion新增Release *stringpayload 更新逻辑从写schemaVersion改为写release键// 移除: // if v : patch.SchemaVersion; v ! nil { // payloadParts.Join( || , jsonb_build_object(schemaVersion, ?::TEXT), *v) // } // 新增: if v : patch.Release; v ! nil { payloadParts.Join( || , jsonb_build_object(release, ?::TEXT), *v) }设计文档特别强调无需实际数据迁移。原因有三proto 复用了既有字段号10/13二进制编码层面不产生结构破坏task payload 以 JSONB 存储旧任务保留旧结构、新任务采用新结构互不干扰执行逻辑按字段存在性判断GetRelease() ! 新旧结构在同一套代码中都能正确处理。五、API 转换层sheet 与 release 二选一设计文档要求更新 backend/api/v1/rollout_service_converter.go 的任务转换逻辑使 API 响应中DatabaseUpdate要么带sheet、要么带release。当前仓库实现rollout_service_converter.go采用了与 store proto 对称的oneof结构Task_DatabaseUpdate_Source// Build DatabaseUpdate payload databaseUpdate : v1pb.Task_DatabaseUpdate{} if releaseName : task.Payload.GetRelease(); releaseName ! { databaseUpdate.Source v1pb.Task_DatabaseUpdate_Release{ Release: releaseName, } } else if sheetSha256 : task.Payload.GetSheetSha256(); sheetSha256 ! { databaseUpdate.Source v1pb.Task_DatabaseUpdate_Sheet{ Sheet: common.FormatSheet(project.ResourceID, sheetSha256), } }此外执行日志同样增加了发布文件级条目TaskRunLogEntry_ReleaseFileExecute第 506–508 行与执行器的RELEASE_FILE_EXECUTE日志类型一一对应前端可以据此渲染哪个发布文件在哪个数据库上执行的过程信息。六、前端配套调整设计文档列出的前端改动点以当时 Vue 组件路径为例frontend/src/utils/v1/issue/rollout.ts——extractSchemaVersionFromTask()需适配新结构frontend/src/components/RolloutV1/components/TaskView.vue、TaskTable.vuefrontend/src/components/IssueV1/components/TaskListSection/TaskCard.vue。展示层面的目标行为对发布任务显示release 名称而非 schema version——例如显示Release: v1.2.3替代Version: 00001任务详情视图展示该 release 的文件列表可考虑展示应用进度如3/5 files applied配合执行器逐文件产生的RELEASE_FILE_EXECUTE日志前端可以精确还原每个文件的执行状态。需要说明当前仓库 frontend 已迁移为 React/TS 结构原设计文档中的 Vue 组件路径属于当时版本具体文件可能已重构但展示 release 名、展示文件进度的产品语义在现架构中依然由 release 字段与发布文件执行日志支撑。七、实施步骤与测试计划实施步骤设计文档给出的实施顺序按依赖关系排列Proto 变更更新task.proto并重新生成代码后端任务创建修改getTaskCreatesFromChangeDatabaseConfigWithRelease()现为getTaskCreatesFromChangeDatabaseConfig()后端任务执行在执行器中新增发布级执行路径现为runVersionedRelease/runDeclarativeRelease后端 Store更新TaskPatch与 payload 序列化后端转换器更新 API 响应转换前端更新任务展示组件测试端到端验证发布任务。测试计划设计文档规划的测试覆盖三层单元测试任务创建产生 N 个任务而非 N×M任务执行跳过已应用版本失败后重试的续跑行为正确。集成测试创建含 5 个文件、3 个数据库的 release验证只创建 3 个任务而非 15 个在第 3 个文件上人为制造失败验证重试时跳过文件 1–2验证 revision 记录正确。向后兼容既有 sheet 型任务仍能正常执行旧任务在 UI 上仍能正确展示。从仓库现状看执行器的appliedVersions构建与跳过已应用版本逻辑database_migrate_executor.go已经实现并被版本化发布路径复用集成测试所验证的失败重试跳过成功文件行为正是该机制的直接体现。八、迁移策略与收益总结迁移策略无需数据迁移JSONB 的灵活性天然兼容新旧 payload 结构新发布即时受益新创建的 release 自动采用 (database, release) 任务结构旧任务零改造既有 sheet 任务继续走单 sheet 执行路径渐进式生效新旧结构在同一个 rollout 中可并存发布与发布之间互不影响。指标对比指标改造前改造后任务粒度(database, file)(database, release)10 库 × 20 文件的发布200 个任务10 个任务版本去重时机任务创建期过滤执行期基于 revision 表跳过失败重试需逐任务判断同一任务内天然跳过成功文件该指标为设计文档中的示例估算95% reduction in task count for large releases适用于多数据库、多文件的 GitOps 大型发布场景。结语发布级任务是 Bytebase GitOps 任务模型的一次关键收敛proto 层用oneof source表达 sheet/release 的互斥来源创建层从 N×M 双重循环降为 N 单循环执行层基于 revision 表实现跳过已应用 失败即停 重试续跑转换层与执行日志层同步提供 release 与逐文件进度信息。整个设计借助 proto 字段复用与 JSONB 的灵活性做到了零数据迁移的渐进式上线——这正是数据库变更治理工具在模型演进时最值得借鉴的工程手法。想深入阅读完整设计推演可参考 docs/plans/2026-01-02-release-level-tasks-design.md并在 proto/store/store/task.proto、backend/api/v1/rollout_service_task.go、backend/runner/taskrun/database_migrate_executor.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),仅供参考