这么强?!Erda MySQL Migrator:持续集成的数据库版本控制

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
日志服务 SLS,月写入数据量 50GB 1个月
简介: 深度好文,一键收藏!

为什么要进行数据库版本控制?

现代软件工程逐渐向持续集成、持续交付演进,软件一次性交付了事的场景逐渐无法满足复杂多变的业务需求,“如何高效地进行软件版本控制”成为我们面临的挑战。同时,软件也不是仅仅部署到某一套环境中,而是需要部署到开发、测试、生产以及更多的客户环境中,“如何一套代码适应不同的环境”也成为我们要思考的问题。

1.png
一套软件的副本要部署在不同的环境(图源:Flyway)

代码版本管理工具(Git、SVN 等)和托管平台(Github、Erda DevOps Platform 等)让我们能有效地进行代码版本管理。越来越丰富的 CI/CD 工具让我们能定义可重复的构建和持续集成流程,发布和部署变得简单清晰。

基础设施即代码”的思想,让我们可以用代码定义基础设施,从而抹平了各个环境的差异。

可以说,在软件侧我们应对这些挑战已经得心应手。

但是绝大多数项目都至少包含两个重要部分:业务软件,以及业务软件所使用的数据库——许多项目数据库侧的版本控制仍面临乱局:

  • 很多项目的数据库版本控制仍依赖于“人肉维护”,需要开发者手动执行 SQL;
  • 环境一多,几乎没人搞得清某个环境上数据库是什么状态了;
  • database migrations 脚本没有统一管理,遗失错漏时有发生;
  • 不确定脚本的状态是否应用,也许在这个环境应用了但在另个环境却没有应用;
  • 脚本里有一行破坏性代码,执行了后将一个表字段删除了,数据无法恢复,只能“从删库到跑路”;
  • ……

为了应对这样的乱局,我们需要数据库版本控制工具

数据库版本控制,即 Database Migration,它能帮你:

  • 管理数据库的定义和迁移历程
  • 在任意时刻和环境从头创建数据库至指定的版本
  • 以确定性的、安全的方式执行迁移
  • 清楚任意环境数据库处于什么状态

从而让数据库与软件的版本管理同步起来,软件版本始终能对应正确的数据库版本,同时提高安全性、降低维护成本。

Erda 如何实践数据库版本控制

Erda 是基于多云架构的一站式企业数字化平台,为企业提供 DevOps、微服务治理、多云管理以及快数据管理等云厂商无绑定的 IT 服务。Erda 既可以私有化交付,也提供了 SaaS 化云平台 Erda Cloud,以及开源的社区版。

当你正在阅读这篇文章时,有无数来自不同组织的应用程序正在 Erda Cloud 或 Erda 私有化平台的流水线上完成以构建和部署为核心的 CI/CD 流程,无数的代码,以这种持续而自动化的方式转化成服务实例。

Erda 平台不但接管了这些组织的应用程序的集成、交付,Erda 项目自身的集成也是托管在 Erda DevOps 平台的。Erda 自身的持续集成和丰富的交付场景要求它能进行安全、高效、可持续的数据库版本控制,托管在 Erda 上的应用程序也要求 Erda 提供一套完整的数据库版本控制方案。

Erda 项目使用 Erda MySQL Migrator 作为数据库版本控制工具,它被广泛应用于 CI/CD 流程和命令行工具中。

基本原理

第一次使用 Erda MySQL Migrator 进行数据库版本控制时会在数据库中新建一个名为 schema_migration_history 的表,如下如所示:

2.png
schema_migration_history 表的基本结构(部分主要字段)

Erda MySQL Migrator 每次执行 database migration 时,会对比文件系统中的 migrations 脚本和 schema_migration_history 表中的执行记录,标记出增量的部分。在一系列审查后,Erda MySQL Migrator 将增量的部分应用到目标 database 中。成功应用的脚本被记录在案。

Erda MySQL Migrator 命令行工具

erda-cli 工具的构建与安装

erda-cli 是 erda 项目命令行工具,它集成了 Erda 平台安装、Erda 拓展管理以及开发脚手架。其中 erda-cli migrate 命令集成了数据库版本控制全部功能。

从 erda 仓库 拉取代码到本地,切换到 master 分支,执行以下命令可以编译erda-cli :

% make prepare-cli
% make cli

注意编译前应确保当前环境已安装 docker。编译成功后项目目录下生成了一个 bin/erda-cli 可执行文件。

使用 erda-cli migrate 进行数据库版本迁移

Erda MySQL Migrator 要求按 modules/scripts 两级目录组织数据库版本迁移脚本,以 erda 仓库为例:

.erda/migrations
├── apim
│   ├── 20210528-apim-base.sql
│   ├── 20210709-01-add-api-example.py
│   └── requirements.txt
... ...
├── cmdb
│   ├── 20210528-cmdb-base.sql
│   ├── 20210610-addIssueSubscriber.sql
│   ├── 20210702-updateMbox.sql
│   └── 20210708-add-manageconfig.sql
└── config.yml
    └── 20200528-tmc-base.sql

erda 项目将数据库迁移脚本放在 .erda/migrations 目录下,目录下一层级是按模块名(微服务名)命名的脚本目录,其各自下辖本模块所有脚本。与脚本目录同级的,还有一个 config.yml 的文件,它是 Erda MySQL Migration 规约配置文件,它描述了 migrations 脚本所需遵循的规约。

脚本目录下按文件名字符序排列着 migrations 脚本,目前支持 SQL 脚本和 Python 脚本。如果目录下存在 Python 脚本,则需要用 requirements.txt 来描述 Python 脚本的依赖。

进入 migrations 脚本所在目录 .erda/migrations,执行 erda-cli migrate :

% erda-cli migrate --mysql-host localhost \
    --mysql-username root \
    --mysql-password 123456789 \
    --sandbox-port 3307 \
    --database erda
INFO[0000] Erda Migrator is working                     
INFO[0000] DO ERDA MYSQL LINT....                       
INFO[0000] OK                           
INFO[0000] DO FILE NAMING LINT....                        
INFO[0000] OK                            
INFO[0000] DO ALTER PERMISSION LINT....                 
INFO[0000] OK                     
INFO[0000] DO INSTALLED CHANGES LINT....                
INFO[0000] OK                    
INFO[0000] COPY CURRENT DATABASE STRUCTURE TO SANDBOX.... 
INFO[0014] OK 
INFO[0014] DO MIGRATION IN SANDBOX....                  
INFO[0014] OK                                            
INFO[0014] DO MIGRATION....                             
INFO[0014]                 module=apim
... ...
INFO[0014]                 module=cmdb
INFO[0014] OK
INFO[0014] Erda MySQL Migrate Success !

执行 erda-cli migrate 命令

从执行日志可以看到,命令行执行一系列检查以及沙盒预演后,成功应用了本次 database migration。我们可以登录数据库查看到脚本的应用情况。

mysql> SELECT service_name, filename FROM schema_migration_history;
+---------------+-------------------------------------------+
| service_name  | filename                                  |
+---------------+-------------------------------------------+
| apim          | 20210528-apim-base.sql                    |
| apim          | 20210709-01-add-api-example.py            |
... ...        ... ...                                      ... ...
| cmdb          | 20210528-cmdb-base.sql                    |
| cmdb          | 20210610-addIssueSubscriber.sql           |
| cmdb          | 20210702-updateMbox.sql                   |
| cmdb          | 20210708-add-manageconfig.sql             |
+---------------+-------------------------------------------+

登录 MySQl Server 查看脚本应用情况

基于 Python 脚本的 data migration

从上一节我们看到,脚本目录中混合着 SQL 脚本和 Python 脚本,migrator 对它们一致地执行。Erda MySQL Migrator 在设计之初就决定了单脚本化的 migration,即一个脚本表示一次 migration 过程。大部分 database migration 都可以很好地用 SQL 脚本表达,但仍有些包含复杂逻辑的 data migration 用 SQL 表达则会比较困难。对这类包含复杂业务逻辑的 data migration,Erda MySQL Migrator 支持开发者使用 Python 脚本。

erda-cli 提供了一个命令行 erda-cli migrate mkpy 来帮助开发者创建一个基础的 Python 脚本。执行:

% erda-cli migrate mkpy --module my_module --name myfeature.py --tables blog,author,info

命令生成如下脚本:

"""
Generated by Erda Migrator.
Please implement the function entry, and add it to the list entries.
"""

import django.db.models


class Blog(django.db.models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    class Meta:
        db_table = "blog"

class Author(django.db.models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    class Meta:
        db_table = "author"

class Info(django.db.models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField()
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField()
    number_of_pingbacks = models.IntegerField()
    rating = models.IntegerField()

    class Meta:
        db_table = "info"


def entry():
    """
    please implement this and add it to the list entries
    """
    pass


entries: [callable] = [
    entry,
]

该脚本可以分为四个部分:

  1. import 导入必要的包。脚本中采用继承了 django.db.models.Model 的类来定义库表,因此需要导入 django.db.model 库。开发者可以根据实际情况导入自己所需的包,但由于单脚本提交的原则,脚本中不应当导入本地其他文件。
  2. 模型定义。脚本中 class Blogclass Authorclass Entry 是命令行工具为开发者生成的模型类。开发者可以使用命令行参数 --tables 指定要生成哪些模型定义,以便在开发中引用它们。注意,生成这些模型定义类时并没有连接数据库,而是根据文件系统下过往的 migration 所表达的 Schema 生成。生成的模型定义只表示了表结构而不包含表关系,如“一对一”、“一对多”、“多对多”等。如果开发者要使用关联查询,应当编辑模型,自行完成模型关系的描述。Django ORM 的模型关系仅表示逻辑层面的关系,与数据库物理层的关系无关。
  3. entry 函数。命令行为开发者生成了一个名为 entry 的函数,但是没有任何函数体,开发者需要自行实现该函数体以进行 data migration。
  4. entries,一个以函数为元素的列表,是程序执行的入口。开发者要将实现 data migration 的业务函数放到这里,只有 entries 中列举的函数才会被执行。

从以上脚本结构可以看到,我们选用的 Django ORM 来描述模型和进行 CRUD 操作。

为什么采用 Django ORM 呢?

因为 Django 是 Python 语言里最流行的 web 框架之一,Django ORM 也是 Python 中最流行的 ORM 之一,其设计完善、易用、便于二次开发,且有详尽的文档、丰富的学习材料以及活跃的社区。无论是 Go 开发者还是 Java 开发者,都能在掌握一定的 Python 基础后快速上手该 ORM。我们通过两个简单的例子来了解下如何利用 Django ORM 来进行 CRUD 操作。

示例 1 创建一条新记录。

# 示例 1
# 创建一条记录
def create_a_blog():
    blog = Blog()
    blog.name = "this is my first blog"
    blog.tagline = "this is looong text"
    blog.save()

Django ORM 创建一条记录十分简单,引用模型类的实例,填写字段的值,调用 save()方法即可。

示例 2 删除所有标题中包含 "Lennon" 的 Blog 条目。

Django 提供了一种强大而直观的方式来“追踪”查询中的关系,在幕后自动处理 SQL JOIN 关系。它允许你跨模型使用关联字段名,字段名由双下划线分割,直到拿到想要的字段。

# 示例 2
# 删除所有标题中含有 'Lennon' 的 Blog 条目:
def delete_blogs_with_headline_lennon():
    Blog.objects.filter(info__headline__contains='Lennon').delete()

最后,别忘了将这两个函数放到 entries 列表中,不然它们不会被执行。

entries: [callable] = [
    create_a_blog,
    delete_blogs_with_headline_lennon,
]

可以看到,编写基于 Python 的 data migration 是十分方便的。erda-cli migrate mkpy 命令行为开发者生成了模型定义,引用模型类及其实例可以便捷地操作数据变更,开发只须关心编写函数中的业务逻辑。

想要进一步了解 Django ORM 的使用请查看文档:

Django - 执行查询

在 CI/CD 时进行数据库版本控制

每日凌晨,Erda 上的一条流水线静静启动,erda 仓库的主干分支代码都会被集成、构建、部署到集成测试环境。开发者一早打开电脑,登录集成测试环境的 Erda 平台验证昨日集成的新 feature 是否正确,发现昨天新合并的 migrations 也一并应用到了集成测试环境。这是怎么做到的呢 ?

3.png
Erda 每日自动化集成流水线(部分步骤)

原来这条流水线每日凌晨拉取 erda 仓库主干分支代码 -> 构建应用 -> 将构建产物制成部署制品 -> 在集成测试环境执行 Erda MySQL 数据迁移 -> 将制品部署到集成测试环境。流水线中的 Erda MySQL 数据迁移 节点是集成了 Erda MySQL Migrator 全部功能的 Action,是 Erda MySQL Migrator 在 Erda CI/CD 流水线中的应用

Erda MySQL Migrator 除了可以作为 Action 编排在流水线中,还可以脱离 Erda 平台作为命令行工具单独使用。

Erda MySQL Migrator 其他特性

规约检查

成熟的团队一般都会制定代码开发规约。Erda MySQL Migrator 支持开发者团队通过配置规约文件,来约定 SQL 脚本规范,如启用和禁用特定的 SQL 语句、约束表名与字段名格式、约束字段类型等。

比如要求 id 字段必须是 varchar(36) 或 char(36),可以添加如下配置:

- name: ColumnTypeLinter
  meta:
    columnName: id
    types:
      - type: varchar
        flen: 36
      - type: char
        flen: 36

比如要求表名必须以 "erda_" 开头,可以添加如下配置:

- name: TableNameLinter
  alias: "TableNameLinter: 以 erda_ 开头仅包含小写英文字母数字下划线"
  white:
    committedAt:
      - "<20220215"  ## 此处表示对提交时间早于2022年2月5日的文件不作此条规约要求
  meta:
    patterns:
      - "^erda_[a-z0-9_]{1,59}"

关于如何编写规约配置文件的更新信息见链接:
https://github.com/erda-project/erda-actions/tree/master/actions/erda-mysql-migration/1.0-57#%E8%A7%84%E7%BA%A6%E9%85%8D%E7%BD%AE

使用命令行工具进行规约检查

erda-cli migrate lint 命令可以检查指定目录下所有脚本的 SQL 语句是否符合规约。开发者在编写 migration 时用该命令来预先检查,避免提交不合规了不合规的脚本。

例如开发者在 SQL 脚本中编写了如下语句:

alter table dice_api_assets add column col_name varchar(255);

执行规约检查:

% erda-cli migrate lint

2021/07/19 17:39:43 Erda MySQL Lint the input file or directory: .
apim/20210715-01-feature.sql:
    dice_api_assets:
        - 'missing necessary column definition option: COMMENT'
        - 'missing necessary column definition option: NOT NULL'

apim/20210715-01-feature.sql [lints]
apim/20210715-01-feature.sql:1: missing necessary column definition option: COMMENT: 

apim/20210715-01-feature.sql:1: missing necessary column definition option: NOT NULL:

使用命令行工具进行本地规约检查

可以看到命令行返回了检查报告,指出了某个文件中存在不合规的语句,并指出了具体的文件、行号、错误原因等信息。上面示例中指出了这条语句有两条不合规处:一是新增列时,应当有列注释,此处缺失;二是新增的列应当是 NOT NULL 的,此处没有指定。

使用 CI 工具进行规约检查

开发者自行使用命令行工具自检是合规检查的第一道关卡。在提交的代码合并到 erda 仓库主干分支前,PR 触发的 CI 流程会利用命令行工具检查 migrations 合规性则是第二道关卡。当提交包含不合规的 SQL 的 PR 时,CI 就会失败:

4.png
Github CI:Erda MySQL Lint 失败提示

使用 Erda MySQL Migration Lint Action 进行规约检查

对于托管在 Erda DevOps 平台的项目,可以使用 Erda MySQL Migration Lint Action 进行规约检查。前文中的 Erda MySQL 数据迁移 Action 已经包含了规约检查功能,所以从功能上来说,Erda MySQL Migration Lint Action 可以看做 Erda MySQL 数据迁移 Action 的一部分。下图是使用 Erda MySQL Migration Lint Action 编排的流水线检查脚本合规性的示例。


5.jpeg
使用 Erda MySQL Migration Lint Action 编排流水线检查脚本合规性

示例中该 Action 失败,打开 Action 日志可以查看具体失败原因。

沙盒与 Dryrun

引入沙盒是为了在将 migrations 应用到目标数据库前进行一次模拟预演,期望将问题的发现提前,防止将问题 migration 应用到了目标数据库中。对 Erda 这样的有丰富的交付场景的项目而言,在 migrate 前先进行一道预演是十分有意义的。这是 Erda MySQL Migrator 根据自身实际设计的,是 Flyway 等工具所不具备的。

Erda MySQL Migrator 可以配置仅在沙盒中而不在真实的 MySQL Server 中执行执行 migration,从而达到 Dryrun 的目的。

文件篡改检查与修订机制

Erda MySQL Migrator 不允许篡改已应用过的文件。之所以这样设计是因为一旦修改了已应用过的脚本,那么代码与真实数据库状态就不一致了。如果要修改表结构,应当增量地提交新的 migrations。这是一种常见的做法,Flyway 等工具也会对已执行的文件进行检查。

但实际生产中,“绝不修改过往文件”这种理想状态很难达到,Erda MySQL Migrator 提供了一种修订机制。当用户想修改一个文件名为“some-feature.sql”过往文件时,他应该修改该文件,并提交一个名为“patch-some-feature.sql”的包含了修改内容的文件到 .patch 目录中。

日志收集

Erda MySQL Migrator 在 debug 模式下,会打印所有执行执行过程和 SQL 的标准输出。除此之外,它还可以将纯 SQL 输出到指定目录的日志文件中。

获取工具

erda-cli 下载地址

Mac

http://erda-release.oss-cn-hangzhou.aliyuncs.com/cli/mac/erda-cli

Linux

http://erda-release.oss-cn-hangzhou.aliyuncs.com/cli/linux/erda-cli

注意:以上 erda-cli 仅用于 amd64 平台,其他平台请按文中介绍的安装方式自行构建。

源码地址

Erda MySQL MIgrator Action 源码地址
https://github.com/erda-project/erda-actions/tree/master/actions/erda-mysql-migration/1.0-57

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3天前
|
NoSQL 关系型数据库 MySQL
微服务架构下的数据库选择:MySQL、PostgreSQL 还是 NoSQL?
在微服务架构中,数据库的选择至关重要。不同类型的数据库适用于不同的需求和场景。在本文章中,我们将深入探讨传统的关系型数据库(如 MySQL 和 PostgreSQL)与现代 NoSQL 数据库的优劣势,并分析在微服务架构下的最佳实践。
|
5天前
|
存储 SQL 关系型数据库
使用MySQL Workbench进行数据库备份
【9月更文挑战第13天】以下是使用MySQL Workbench进行数据库备份的步骤:启动软件后,通过“Database”菜单中的“管理连接”选项配置并选择要备份的数据库。随后,选择“数据导出”,确认导出的数据库及格式(推荐SQL格式),设置存储路径,点击“开始导出”。完成后,可在指定路径找到备份文件,建议定期备份并存储于安全位置。
65 11
|
6天前
|
存储 SQL 关系型数据库
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
MySQL如何进行分库分表、数据迁移?从相关概念、使用场景、拆分方式、分表字段选择、数据一致性校验等角度阐述MySQL数据库的分库分表方案。
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
|
4天前
|
SQL 监控 关系型数据库
MySQL数据库中如何检查一条SQL语句是否被回滚
检查MySQL中的SQL语句是否被回滚需要综合使用日志分析、事务状态监控和事务控制语句。理解和应用这些工具和命令,可以有效地管理和验证数据库事务的执行情况,确保数据的一致性和系统的稳定性。此外,熟悉事务的ACID属性和正确设置事务隔离级别对于预防数据问题和解决事务冲突同样重要。
17 2
|
7天前
|
存储 缓存 关系型数据库
MySQL 视图:数据库中的灵活利器
视图是数据库中的虚拟表,由一个或多个表的数据经筛选、聚合等操作生成。它不实际存储数据,而是动态从基础表中获取。视图可简化数据访问、增强安全性、提供数据独立性、实现可重用性并提高性能,是管理数据库数据的有效工具。
|
7天前
|
SQL 关系型数据库 MySQL
MySQL技术安装配置、数据库与表的设计、数据操作解析
MySQL,作为最流行的关系型数据库管理系统之一,在WEB应用领域中占据着举足轻重的地位。本文将从MySQL的基本概念、安装配置、数据库与表的设计、数据操作解析,并通过具体的代码示例展示如何在实际项目中应用MySQL。
33 0
|
29天前
|
SQL 关系型数据库 MySQL
【揭秘】MySQL binlog日志与GTID:如何让数据库备份恢复变得轻松简单?
【8月更文挑战第22天】MySQL的binlog日志记录数据变更,用于恢复、复制和点恢复;GTID为每笔事务分配唯一ID,简化复制和恢复流程。开启binlog和GTID后,可通过`mysqldump`进行逻辑备份,包含binlog位置信息,或用`xtrabackup`做物理备份。恢复时,使用`mysql`命令执行备份文件,或通过`innobackupex`恢复物理备份。GTID模式下的主从复制配置更简便。
124 2
|
24天前
|
弹性计算 关系型数据库 数据库
手把手带你从自建 MySQL 迁移到云数据库,一步就能脱胎换骨
阿里云瑶池数据库来开课啦!自建数据库迁移至云数据库 RDS原来只要一步操作就能搞定!点击阅读原文完成实验就可获得一本日历哦~
|
27天前
|
关系型数据库 MySQL 数据库
RDS MySQL灾备服务协同解决方案构建问题之数据库备份数据的云上云下迁移如何解决
RDS MySQL灾备服务协同解决方案构建问题之数据库备份数据的云上云下迁移如何解决
|
25天前
|
人工智能 小程序 关系型数据库
【MySQL】黑悟空都掌握的技能,数据库隔离级别全攻略
本文以热门游戏《黑神话:悟空》为契机,深入浅出地解析了数据库事务的四种隔离级别:读未提交、读已提交、可重复读和串行化。通过具体示例,展示了不同隔离级别下的事务行为差异及可能遇到的问题,如脏读、不可重复读和幻读等。此外,还介绍了在MySQL中设置隔离级别的方法,包括全局和会话级别的调整,并通过实操演示了各隔离级别下的具体效果。本文旨在帮助开发者更好地理解和运用事务隔离级别,以提升数据库应用的一致性和性能。
103 2
【MySQL】黑悟空都掌握的技能,数据库隔离级别全攻略