实现生产级的Migrate操作

作者 风爻,编辑 吕昭波

前言

在涉及到关系型数据库开发时、我们每次在线上操作数据库都需要找一个叫DBA的角色,时光荏苒,就这样过了一年又一年。一群不堪压迫,集颜值、智商、动手能力于一身的程序员们发现,明明我们手里有操纵DB的能力,为什么要把命运交给别人主宰呢?

这群程序员做了一个伟大的决定!

让数据库的数据结构也能够通过版本追踪,这种操作,就被称为“Database Migrate”。本文将以Flask-Migrate作为基础蓝本,讲解如何实现生产级质量的Migrate。

其实Migrate实现并不复杂,只要我们:

· 支持追踪每一次的数据库结构变更【构建commit】

· 新开发者参与到项目中可以自动的应用变更到最新版本【生效commit】

· 回滚更新【revert】

一个基础完备且健壮的Migrate就算实现了。常见的Migrate实现都是直接使用当前数据库来进行构建的。Flask-Migrate也不例外。

在Flask-Migrate中,构建commit是通过文件的方式生成的。以一个简单的项目为例:

而生效的commit,存储在DB的alembic_version中:

在本例中,当前的migrate有两个,但是只生效了一个。当我们再次执行Migrate操作时,系统会对比当前目录和数据库中的记录,根据依赖关系顺序生效还未生效的commit。

近十年来,随着MVC、敏捷开发、DevOps等等设计、开发模式从出现、探讨到成熟。Migrate已经是一种不争的维护关系型数据库表结构的手段之一。

然而很不幸的是,虽然Flask有比较稳定的flask-migrate来支持这一功能。但是官方文档给出的做法确差强人意。

我们一直认为Flask生态最大的优势是极其简单、学习迅速,但是用好Flask,做到生产级可用,真的不简单。Flask不像Django用约束换取自由,Flask本身可以认为是完全自由的。

不禁想起了社区的金句:Pirates use Flask, the navy uses Django.

但是因为Flask自身的Micro风格,对等的带来一个巨大的优势,可以将Flask代码作为一个项目的内嵌项目来实现,特别是对于一些老旧项目的重构改造等,Flask有着难以比拟的整合性。从Migrate角度讲,Flask-Migrate拥有更广泛的使用场景。

生产级的Migrate

我们认为一个生产级的Migrate的指标有:

基础指标

· 正确识别表结构变动并支持Migrate

· 支持回滚操作(可以是有条件限制的)

· 支持多个模块的Migrate

进阶指标

· 自动触发Migrate操作

· 新项目可以只复用表结构而无需复用Migrations

很不幸的是,flask-migrate除了基础指标的1和2之外,均不能达成……所以,是时候从入门到放弃了吗?

当然不!要告诉你的是:这世上不存在开源且完全符合自身需求的程序。如果有,在编程这条路上,你离被淘汰不远了。

解决方案

Step1.实现跨模块

分析

· Flask框架风格追求Micro。所以每个App风格都是尽可能少的引用,于是每个App中的Model只会包含数据库中的一部分而不是全部。

· 其实和Migrate的思路产生了冲突。因为Migrate的目标是掌控整个数据库。如果我们有办法让Migrate识别出所有的Model,那问题不是就解决了?

核心代码

完整版代码传送门:https://github.com/wangwenpei/fantasy/blob/master/fantasy/cli.py#L71

Step2.自动触发Migrate

分析

· 如果你对Migrate的概念比较熟悉。你就会意识到,Migrate操作始终是在当前版本代码载入生效之前执行的。

· 常见的思路是,我们通常会把这种操作构建到持续部署(continuous deployment)系统中。而基于Flask自由的灵魂,我们还可以构造出更加灵活简单的方式,只要我们保证在代码生效前执行就可以了。

核心代码

完整版代码传送门:https://github.com/wangwenpei/fantasy/blob/master/fantasy/init.py#L63

Step3.多项目无冲突复用表结构

使用场景

· 随着公司和项目的成长,举例:人事的变动,已有的项目A负责人开始接手新项目B。老项目A和项目B之间是完全独立的。而有一部分数据库表结构,希望只共用结构,而不共用数据。比如:消息发送记录表(table1)。

· 一个粗暴的办法,我们当然可以直接施展复制粘贴大法,从此分道扬镳。在风爻看来,这种策略实在低级,牺牲了太多特性。

好的策略

我们认为一个好的策略是这样的:

· 一方负责维护,其他方仅引用使用

这样一旦有商议通过的更新,所有人都会同步更新,项目组之间无需独立维护。

· 新项目只依赖需要的数据结构,创建全新的migrations文件

项目A的依赖关系没有任何变动,项目B的依赖关系为全新,互不影响。

完整版Demo传送门:https://github.com/wangwenpei/shining-flask/tree/master/aivptr

Migrate风格DB设计原则

以下为风爻建议的一般通用性原则。

· 保持简单

你应该保持当前DB表结构的简单,不要做过度的设计,尽量不要预留字段。

特别是不太确定是否要添加的字段。

· 保持小巧

在生成Migrate时候,应该尽可能的发挥DB的原子操作性。

不要多个表的Migrate混在一起操作。

在一些极端的情况下,执行Migrate时DB连接会出现中断,而利用好DB原子特性的Migrate,可以将风险和损失降到最低。

· 保持单一的核心

关系型数据库作为一种经典数据库,作为核心数据库,数据存储应该单一化,突出关系特性。

做关系型数据库擅长的事情,比如订单、会员资料等是适用于关系型数据库的。

一个简单(粗略但快速的)的判断标准是,你使用到的数据是否需要事务特性。

· 拥抱多元的扩展

现代数据库相比10年前已经出现了极大的演进,仅开源DB据统计就有100种之多。

随着需求的不断更新,关系型数据库虽然大部分特定需求都可以满足,但相比专用数据库实现复杂,性能和效率也差强人意。

对于一些专用数据,风爻建议使用外围DB更合适,如MongoDB, LevelDB, Cassandra等。

随着云计算的成熟,引入多种DB成本是极低的。

作者简介

王文沛(风爻) 开源爱好者。专注于Python社区,曾为数个明星社区贡献过代码,如Mongoengine、Django Plugins级项目等。对Javascript也有少量涉猎,曾为Pug(原名Jade)社区等贡献过代码。也为国内一些云厂商SDK捐助过代码或撰写过第三方独立扩展。对于开源生态:主张融合与协作,尽量避免重复造轮子。目前就职于UCloud,担任资深SRE专家职务。