十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从Flyway到Atlas:声明式数据库迁移与GitOps实战

从Flyway到Atlas:声明式数据库迁移与GitOps实战 搞数据库迁移搞了六七年我一直是 Flyway 和 Liquibase 的忠实用户但真正让我觉得迁移这玩意儿还能这么做的是后来切换到 Atlas 之后的事。这项目一开始不温不火直到把 schema 立成代码、把数据库漂移当成 bug 来治理我才意识到传统迁移工具那套增量脚本堆叠的思路在微服务和多环境并行开发面前已经到了极限。这篇就聊聊我用 Atlas 改造团队数据库发布流程的完整经历从为什么弃用 Flyway、核心声明式模型怎么理解到 GitOps 集成和线上事故级踩坑尽量把能直接抄作业的部分都摊开讲。1. 为什么我从 Flyway 转到 Atlas迁移工具的两种哲学在讲 Atlas 怎么用之前先得说清楚它解决的是哪个层面的痛点。绝大多数团队对数据库迁移的理解还停留在 Flyway 那一套定义目录下的 V1__init.sql、V2__add_column.sql按版本号一个个顺序执行。这套方案在单体应用、单数据库、发布节奏按周算的时候够用但一旦进入微服务拆分、多环境长期并存、数据库表结构频繁变动的阶段弱点会非常明显。Flyway 的核心模型是迁移脚本列表它只保证一件事每个脚本按照版本号严格顺序执行一次并在 schema_migrations 表里记录执行状态。问题是这套模型默认的是人肉的版本历史是可信的。每次你修改表结构必须手动生成一个新的 V 文件两个开发者同时从 V10 开始改库一个改了 users 表一个改了 orders 表merge 的时候冲突几乎是必然的而且 Git 层面的合并根本无法检测 SQL 层面的语义冲突。Liquibase 比 Flyway 强一点引入了 changelog 和 changeset 的结构化描述但它本质上还是在维护变更的累积列表并没有跳出增量脚本的思路。Atlas 换了个思路不管理脚本直接管理 schema 的最终状态。你把数据库期望的结构写成一个文件HCL 或 SQLAtlas 根据当前数据库实际结构和你期望的目标状态做 diff动态生成一份迁移计划然后你可以选择让 Atlas 直接执行也可以把它保存成传统 SQL 迁移文件交给既有流程。这个模型在实际使用中带来的最大变化是你不再需要思考这次改动应该写第几个版本号只需要想清楚我的库最终长什么样。我团队里有不少年轻人以前写 Flyway 迁移脚本最常犯的错就是在 V15 里改了 V8 加过的字段名结果上线直接炸。而 Atlas 因为每次都是基于当前真实状态去算差异根本不存在这种重复修改同一根柱子的问题因为目标状态里只会有最终形态。如果你现在还在用 Flyway并且项目表结构变化频率很低、环境数量少说实话没必要强行迁移过来——工具切换有一定学习成本。但凡你遇到下面这些信号就该考虑 Atlas同一套库结构要部署到 dev、staging、prod 以及客户私有化环境且部分环境因为历史原因 schema 不一致多人并行开发每周都有大量 alter table需要把数据库变更纳入 GitOps 或 CI/CD 流程希望合并即验证经常被 schema drift数据库实际结构和代码里对不上坑到2. Atlas 的核心模型Desired State 与 Diff 引擎2.1 HCL把数据库结构变成可评审的代码Atlas 的方式是把你期望的数据库结构用 HCLHashiCorp Configuration Language描述出来schemaload 之后和真实数据库比对。这里最核心的概念是 Desired State期望状态。它以代码形式存在可评审、可 diff、可版本控制。数据库本身是 Current State当前状态Atlas 的 diff 引擎负责计算两者差异并生成迁移。拿一个实际例子来感受下。假设你有一个用户表期望结构是这样的schema public { } table users { schema schema.public column id { null false type bigint } column email { null false type varchar(255) } column name { null true type varchar(100) } primary_key { columns [column.id] } index users_email_key { unique true columns [column.email] } }这段声明既直白又有歧义空间比如 bigint 到底是 int8 还是 bigserialAtlas 在 diff 时会把类型归一化到具体数据库方言这需要后面细讲。你不需要把每个细节背下来因为可以用atlas schema inspect自动从现有库里反向生成 HCLatlas schema inspect \ -u postgres://user:passlocalhost:5432/mydb?sslmodedisable \ --format {{ sql . }} schema.sql不过我更建议把 HCL 作为唯一事实源而不是导出的 SQL 快照。HCL 的优势在于它可以声明式地表达依赖和约束SQL DDL 则是线性的、不可逆的。真实开发中代码评审的人看 HCL 比看一串 ALTER TABLE 高效太多因为你能直接看到表结构的全貌。2.2 Diff 引擎的类型归一化陷阱Atlas 的 diff 引擎比较强但正因为强你才需要理解它是怎么对比两边的。它把数据库里的元信息information_schema 里的东西抽取成内部模型再和你的 HCL 模型做三方比较。执行atlas schema diff时它会分析两个 model 之间的差异生成 SQL 计划你可以用--edit手动调整也可以用--save保存为迁移文件。这里我踩过最深的坑是类型归一的边界。拿 PostgreSQL 举例你写在 HCL 里的类型实际数据库里常见的内部类型Atlas diff 表现varchar(255)character varying(255)无差异正常bigintbigint无差异integerint4无差异Atlas 会归一texttext无差异timestamptimestamp without time zone可能会警告取决于精度decimal(10,2)numeric(10,2)无差异jsonbjsonb无差异serialinteger default sequence有差异这是个大坑serial 这个类型尤其恶心。PostgreSQL 里其实没有单独的 serial 类型它是integer加上一个nextval()默认值和一个 sequence 的语法糖。你在 HCL 里很自然地写type serial但数据库实际存储的是integer。下次你跑atlas schema diff的时候引擎会认为你想把它改回 integer生成一个多余甚至危险的变更计划。我的做法是规避这类有歧义的类型HCL 里一律使用底层真实类型再手动补充 auto_increment 或序列定义。一开始可能麻烦点但避免了后续所有环境下的诡异漂移。2.3 依赖关系与变更顺序另一个值得理解的是变更计划里的顺序问题。你在 HCL 里新增了一张表 order_items它外键引用了 orders 表同时你把 orders 表一个字段类型从 varchar 改成 text。Atlas 不是按照你写 HCL 的顺序来做而是根据依赖图拓扑排序先加外键引用的目标字段/表再建引用方最后执行不破坏约束的数据变更。这个能力是手工维护增量脚本最难保证的部分。我实际在项目里体会最深的是触发一个明显多余的 alter 几乎都是顺序问题。比如你同时改了列类型并在这个列上建索引Atlas 会生成两步先改类型再建索引。这在逻辑上是正确的但也意味着你在执行 window 上放大了锁时间。要是对大表做这类操作最好拆成多个 migration 并在低峰执行。3. 从零接入 Atlas完整实战流程3.1 安装和初始化安装很简单mac 上 brew 一行Linux 下载二进制就行这里不赘述。关键在于初始化方式我推荐从已有数据库反向生成开始尤其是那些已经跑了许久的老项目。你一上来就手动写 HCL要么漏了某个 default 值要么漏了约束后面 diff 的时候会冒出一堆垃圾变更。正确的初始姿势# 1. 从现有数据库生成初始 schema atlas schema inspect \ -u mysql://root:passlocalhost:3306/appdb \ --format {{ . }} atlas.hcl # 2. 备份这份初始 schema 作为 baseline # 3. 提交到 Gittag 为 baseline然后用atlas schema apply把同一份 HCL 应用到空白数据库验证能不能从零复现atlas schema apply \ -u mysql://root:passlocalhost:3306/appdb_copy \ --to file://atlas.hcl这一步有个实战心得如果从零复制出来的库和原始库 diff 有差异不要慌绝大多数是 default 值、字符集、排序规则这类元数据差异。建议在反向生成的 HCL 里就把这些差异显式表达出来确保 baseline 完全干净否则以后每次 diff 都会带着噪音。3.2 核心命令速查Atlas 命令行工具我一直用的是这几个日常开发和 CI 里足够命令作用使用频率atlas schema inspect连接数据库把现有 schema 输出为 HCL/SQL高atlas schema diff比较两个 schema 源生成 SQL 差异高atlas schema apply把 HCL/SQL 直接应用到目标库高atlas migrate diff基于 HCL 和本地 migration 目录生成新迁移文件高atlas migrate apply执行本地 migration 目录里的迁移高atlas migrate lint静态分析迁移文件是否有风险操作中atlas schema fmt格式化 HCL 文件低atlas schema validate校验 HCL 语法和语义中migrate diff和schema apply的区别必须搞清楚schema apply 是直接基于期望状态改库migrate 是先把变更固化成文件再改库。前者适合本地开发和一次性修正后者适合生产环境走发布流程。我建议的生产流程一律走migrate diff 代码评审 migrate apply避免开发直接拿 schema apply 连生产库那相当于把一把没有保险栓的枪递给了所有人。3.3 实战对一个已有项目做第一次迁移我这里用 PostgreSQL 示例说明完整的第一次接入步骤# 假设你已经有一个 atlas.hcl 描述了目标状态 # 第一步生成第一个版本化迁移文件 atlas migrate diff add_users_table \ --dir file://migrations \ --to file://atlas.hcl \ --dev-url docker://postgres/15/dev?search_pathpublic # 第二步检查生成的 SQL cat migrations/*_add_users_table.sql这个命令里最值得展开的是--dev-url。Atlas 不会拿你的本地数据库直接做 diff 演算而是启动一个临时的 dev database 来模拟执行。你可以用本地 Docker 实例也可以指定远程一个专用 dev 库。我强烈建议每个开发者本地都跑一个 dev 容器并且和业务库 Postgres 大版本保持一致。版本不一致会导致 Atlas 生成和你生产库实际方言不匹配的 SQL。生成的迁移文件是原生 SQL并不是某个只有 Atlas 能懂的黑盒格式CREATE TABLE users ( id bigint GENERATED BY DEFAULT AS IDENTITY, email varchar(255) NOT NULL, name varchar(100) NULL, PRIMARY KEY (id) ); CREATE UNIQUE INDEX users_email_key ON users (email);这个特性特别重要意味着你可以在任意环境、用任意工具去 review 迁移内容甚至可以把它直接交给 DBA 人工审核后再执行。3.4 migrate apply 的执行语义atlas migrate apply和 Flyway 一样会在数据库里建一张表记录已执行的版本。默认叫atlas_schema_revisions里面有版本号、执行描述、hash、执行时间。但和 Flyway 有一个关键区别Atlas 记录的是迁移文件的 checksum 和整个 schema 的期望状态摘要不只是这个脚本跑没跑过。这意味着如果你在已经应用了 V2 的库上回头把 V1 文件内容改了再跑atlas migrate apply它会直接报告 hash 不一致并拒绝执行。这个机制我喜欢因为它从工具层面堵住了改历史迁移这种团队协作里的大忌。执行迁移时Atlas 还有一个优势它可以把一个迁移目录里所有 pending 的迁移合成一组事务来执行取决于数据库是否支持 DDL 事务。PostgreSQL 支持事务性 DDL所以多个迁移可以放在一个事务里中途失败自动回滚。MySQL 不支持事务性 DDL所以它只能逐个执行并记录执行到哪一步。这是数据库本身的限制不是工具问题但你设计迁移文件时得有意识MySQL 环境下一个迁移文件里最好只放一个逻辑变更失败后好定位、好重放。4. 把 Atlas 接进 CI/CDGitOps 工作流里的关键拼图4.1 合并请求里自动生成迁移预览团队切换到 Atlas 最大的收益其实不在本地命令而在 CI 流水线。以前代码评审只评审应用代码数据库变更只能靠相关人肉看 SQL 文件质量全靠自觉。把 Atlas 接进 CI 后每一次 MR 都能自动检测期望 schema 是否和本地迁移文件一致、迁移是否包含破坏性操作。我用 GitHub Actions 做了一个最小可用的 workflow核心步骤是这样name: atlas-ci on: pull_request: paths: - atlas.hcl - migrations/** jobs: atlas-ci: runs-on: ubuntu-latest services: postgres: image: postgres:15 env: POSTGRES_USER: atlas POSTGRES_PASSWORD: atlas POSTGRES_DB: dev ports: - 5432:5432 options: - --health-cmd pg_isready -U atlas --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkoutv4 - uses: ariga/atlas-action/migrate/lintv1 with: dir: file://migrations dev-url: postgres://atlas:atlaslocalhost:5432/dev?sslmodedisable - run: | atlas migrate apply \ --dir file://migrations \ --url postgres://atlas:atlaslocalhost:5432/dev?sslmodedisable这个 workflow 做了两件事第一对迁移目录执行migrate lint静态扫描是否包含锁表、DROP COLUMN 这类危险操作第二把迁移目录应用到临时 postgres 容器里验证迁移文件能干净地从零跑通。这两件事都不碰真实数据库完全在 CI 沙箱里完成。4.2 环境晋升同一批迁移文件从 dev 走到 prod有了 CI 验证之后正式发布流程我建议这样设计所有环境的 schema 变更都用同一批通过 CI 的迁移文件且只通过atlas migrate apply执行。dev、staging、prod 环境不直接连 atlas.hcl 做 schema apply因为 apply 的行为取决于数据库当前状态而迁移文件的行为是确定性的。这里有个团队协作的关键细节每次改动 schema开发者只需要更新 atlas.hcl然后跑一条命令生成迁移文件不需要人肉维护版本号。生成的迁移文件名自带时间戳前缀而且 Atlas 会根据 Git 历史判断当前目录下是否存在冲突的迁移链。如果你和同事各自基于同一个版本生成了两个迁移Atlas 不会像 Git 那样自动 merge而是会提示目录状态冲突让你明确决定保留一个。我实际工作中遇到过两次开发者直接改生产库结构、没走迁移文件的状况。事后怎么修复的重新atlas schema inspect生产库对比 atlas.hcl 发现差异再反推出缺失的迁移。这个恢复效率比 Flyway 高太多因为 Flyway 只知道有哪些脚本没执行无法告诉你当前数据库和期望状态的 delta 是多少。4.3 schema drift 的常态化检测GitOps 流程建立起来之后还要解决一个很隐蔽的问题数据库状态可能因为各种原因偏离迁移文件描述的状态。原因可能是有人手动执行了 SQL、某个后台服务自动建了表、甚至云数据库自动加了一些系统表。这种 drift 如果不及时发现会导致下一次迁移在某些环境上正常、在另一些环境上失败。我们团队的做法是写了一个每周跑一次的巡检任务对每个环境的数据库执行atlas schema diff \ --from file://migrations \ --to postgres://user:passprod-host:5432/appdb \ --dev-url docker://postgres/15/dev如果输出为空说明库和迁移链完全一致如果有输出说明漂移了需要立刻人工介入。这个巡检救了我们至少两次。一次是因为 DBA 手动给大表加了索引另一次是有个定时任务在业务库里创建了分析用临时表巡检 diff 把这些都抓得清清楚楚连背景和干预人都不用花时间去猜。5. 迁移文件的 lint 规则与破坏性变更拦截5.1 lint 的检测维度atlas migrate lint是我见过同类工具里少有的针对数据库变更的静态分析器。它基于 dev database 对迁移文件做预演结合 git base 上已有的迁移文件分析这些变更可能产生的影响。这里把默认启用的几个核心检测规则说明一下理解了它们才知道哪些迁移会在 CI 上被拦下来规则名称检测目标实际业务场景destructiveDROP TABLE / DROP COLUMN删列、删表这种不可逆操作concurrent_index建索引时是否可能长时间锁表对大表添加唯一索引not_null新增非空列时是否给出默认值ALTER TABLE ADD COLUMN xxx NOT NULL没 defaultdata_dependent列类型变更可能导致数据丢失varchar(50) 改成 varchar(20)modify_primary_key修改主键列行为变更主键类型unique新增唯一约束是否可能因重复数据失败给已有重复数据的列加唯一索引最常触发的是destructive和not_null。团队里有新人写迁移很自然地就写了ALTER TABLE users DROP COLUMN temp_token;在 CI 上直接被 lint 拦住提示这是破坏性变更需要确认使用--allow-destructive或者在 lint 配置里显式排除。我觉得这是套很好的刹车机制不会一刀切禁止所有 DDL但每次破坏性变更都需要出声解释而不是静默混在代码里合并。5.2 配置 lint 规则时要注意的边界lint 配置在atlas.hcl里可以设置error级别的禁止规则和warning级别的提示规则。我的习惯是lint { destructive { error true } concurrent_index { error true } not_null { error true } data_dependent { error true } }有几个第三方工具也会做类似的 schema 变更检查但 Atlas 的 lint 不是基于正则匹配字符串而是基于对 dev 库执行迁移后再做语义分析所以基本没有误报或漏报。它能识别出ALTER TABLE users MODIFY age INTEGER会不会截断数据、CREATE INDEX是不是在已有大量数据的表上操作这不是靠看文本能搞定的。不过要留个心眼lint 结果强依赖 dev-url 的数据库版本。如果你的 dev 是 PostgreSQL 15生产是 12某些规则比如索引并发支持在 dev 上可能检测不到问题。所以时刻记得 dev 和生产版本一致。5.3 给迁移文件做人工 review 的经验再强的自动检查也替代不了人眼我在团队里推行了一套 30 分钟的数据库变更 review checklist每次 MR 里的迁移文件必须逐条过新加列是否真的没有默认值在 MySQL 和 PG 里ADD COLUMN 加上 volatile 的 default 值比如random()会导致全表重写生产上锁表十几分钟甚至更久。能用应用层写入默认值就尽量别在 DDL 里加。索引的并发构建是否开启PostgreSQL 默认建索引是排他锁大表上要使用CREATE INDEX CONCURRENTLY。但并发建索引不能在事务里执行和 Atlas 默认的事务包裹策略冲突这种情况下我建议拆成独立迁移并显式关闭该迁移的事务包裹。数据迁移是否和表结构变更同一个迁移文件经验法则是DDL 和 DML 分开哪怕执行顺序有严格依赖。因为 MySQL 没有事务性 DDLDML 和 DDL 在同一个文件里一旦中间挂掉重放时极难恢复。分开后至少可以明确知道挂在哪一步。是否真的需要 drop 列而不是 rename应用代码里可能还在引用旧列直接 DROP 会导致线上代码报错。建议先发一个应用版本把所有旧列引用移除再在几个版本之后 drop。这个经验被验证过太多次了。6. 实战中的坑从 dev 到 prod 踩过的三个大问题6.1 dev-url 版本不一致导致生成的 SQL 在生产不可用这是第一次用 Atlas 时最容易掉进去的坑。有一次我在本地用 PostgreSQL 14 的 dev 容器生成迁移生产环境跑的是 PostgreSQL 12结果迁移文件里生成了 PG14 才支持的语法一个函数签名CI 全部通过但atlas migrate apply到生产直接报语法错误。排查链路是这样的先看atlas schema diff在 dev 上跑是干净的再排查生产库的方言版本最后发现 dev-url 指定的容器 tag 和生产环境不一致。从那以后我把项目的 dev-url 统一做成一个 make 目标拉取和线上完全一致的 PG 版本并且要求在 CI 的 services 块里也用同样 tag。6.2 MySQL 下非事务性 DDL 导致迁移状态错乱MySQL 早期版本里 DDL 是隐式提交的atlas migrate apply一次执行多个迁移文件时前面文件成功、后面文件失败Atlas 会记录失败的文件状态为 pending。理论上重跑就能继续但有些 DDL 是幂等性很差的比如你已经执行了一半的MODIFY COLUMN重跑会再报错。我的解决方案MySQL 项目里一个迁移文件只放一个逻辑变更同时把--tx-mode none用起来。宁可多生成几个文件也不要让单个文件里堆多个 DDL。现在 MySQL 8.0 支持原子性 DDL 了但这条经验仍然适用因为不是所有云 RDS 都会开启这个特性。6.3 schema drift 恢复时误判了当前状态有一次巡检发现生产库比迁移链多了一个索引我用atlas schema diff一看返回的就是一条CREATE INDEX。看起来很简单我以为是有人手动补的索引直接没管。但后来做一次大版本升级时把生产库导入 staging 做演练staging 里没有这个索引迁移链里也没有导致某个查询性能直接退化到原来的 20 倍。这个问题的根子在于我判断 drift 时只看了多出来的部分没有判断这部分是否应该纳入迁移链。后续我们约定只要是和期望 schema 不一致的差异哪怕是看起来无害的额外索引也必须走流程确认。确认后要么补进 atlas.hcl 里要么证明它是临时手动操作且要在下一次迁移里显式去掉。绝不能放着不管因为你不知道哪天它会变成一个隐形的依赖。7. 给正在选型或已经上手的团队的最终建议Atlas 不是银弹它有学习曲线尤其是在 HCL 语法熟悉度和团队流程改造上。但如果你想认真把数据库结构当代码来管而不是当一堆 SQL 脚本来堆我目前没有看到比它更顺手的开源方案。我自己从 Flyway 迁过来的过程里最大的体感变化是写迁移从回忆历史版本变成描述目标状态心智负担小了一半代码评审时数据库变更的讨论从这 SQL 语法对不对变成这个表结构设计合不合理讨论质量提升了一个档次数据库漂移从出了事故才被发现变成每周巡检自动报警被动挨打变成了主动治理最后分享一个我踩过几次坑之后沉淀下来的小技巧不管工具多智能都要在 Apply 之前手动看一眼生成的 diff 计划。Atlas 再懂数据库也不懂你的业务。它可能生成一个技术上正确但你业务上不能接受的变更计划比如给一张亿万级表加一个非并发索引。工具负责做对人负责做对的事。把这一步固化进发布流程再配合 CI 里的 lint 和自动验证整个数据库发布流程才算真正稳了。如果你团队正准备引入 API 管理、微服务治理、k8s 部署之类的体系我建议顺手把 Atlas 也纳入基础设施的一部分。它在配置即代码这条路上的完成度确实比大多数同类工具要高出不少。
返回列表