本文将深入探讨如何解决数据库兼容性以及布署过程相关的一些问题。如果你没有做一些准备工作就去尝试本文介绍的布署方法,我将无法预知你的应用会发生什么。接下来,我们将透过应用程序生命周期的每一步骤介绍零宕机方案。我们的应用程序最终要达到的效果是在一个不支持向后兼容的数据库上实现向后兼容变更。
下面所用到的代码示例,你都可以在GitHub上找到。
零宕机布署
传说中的零宕机布署是个什么鬼?你可以把它理解为这样一种应用程序布署方式:你可以成功把应用程序的新版本更新到生产环境,并且用户没有宕机的感觉。从用户和公司的角度来看,这是最好的布署方式,因为不仅发布了新的功能,而且没有带来bug。
那么如何才能做到?下面是众多方法中的一种:
-
为你的服务布署版本1
-
将数据库迁移到新版本
-
为你的服务布署版本2,让版本1和版本2并行
-
如果版本运行很完美,把版本1停掉
-
你做到了!
太简单了,对不对?但是很不幸,事情并不是这么简单,我们接下来就探讨这个问题。让我们来看一下另外一种流行的布署方法:蓝绿布署。
你听说过蓝绿布署吗?在云环境下很容易实现。在此前的一篇文章中,我们曾经做过深度介绍。快速总结一下,做蓝绿布署要点如下:
-
维护两套生产环境(蓝和绿)
-
通过URL映射,只让蓝色系统对外提供服务;
-
任何测试和变更都在绿色系统中进行;
-
通过URL映射,把所有访问都切换到绿色系统。
通过蓝绿布署,发布新功能变得很轻松,不再担心把bug引入到生产系统。因为你可以通过路由映射很方便地回滚到前一个版本,就像“拨动开关”一样简单。
读完以上信息,你可能会问:蓝绿布署和零宕机有什么关系?
他们有很多共同之处,因为要维护两个相同的环境,必须付出双倍的支撑。所以像Martin Fowler这样的团队倾向更多变通的方式:蓝绿布署的一个变体是使用同一个数据库,只在网站和域这一层做蓝绿切换。
采用这种技术会有数据库方面的挑战,特别是当软件的新版本需要修改数据库模式的时候。
至此,引出了我们今天的主要话题:数据库。让我们再回顾一下这句话:
将数据迁移到新版本。
现在,你可以问自己一个问题,如果数据库变更不向后兼容会发生什么?难道我的版本1还会崩溃不成?事实上,它确实会。
虽然零宕机/蓝绿布署很强大,但是出于安全考虑,公司仍然顷向于以下方式:
-
为新版本应用程序准备安装包
-
关闭运行中的应用
-
运行数据库迁移脚本布署运行新版本应用程序
本文将深入介绍如何让你的数据库和代码从零停机布署中受益,并且很好地运行。
数据库问题
如果你的应用是无状态的,不需要使用数据库来存储任何数据,那你现在就可以做到零宕机布署。但不幸的是,大部分软件都需要把数据存到某个地方。这就是为什么在做任何数据库模式变更时,你都要再三考虑。在我们探讨数据库模式变更细节前,让我们先关注模式版本。
模式版本
在这篇文章中我们使用Flyway做为模式版本控制工具。我们还写了一个Spring Boot应用程序,可以为Flyway提供原生支持,而且可以为模式迁移的执行提供应用程序上下文设置。使用Flyway时,你可以把迁移脚本存储在项目文件夹(默认位置是classpath下的db/migration)。这里有一个迁移文件的示例:
在这个例子里我们可以看到四个迁移脚本,如果以前没有执行过,在应用启动时将会顺序依次执行。让我打开一个文件(V1__init.sql)看一下。
代码已经做了很好的自我解释:你可以使用SQL定义你的数据库变更。如需要更多了解Spring Boot和Flyway,请参考Spring Boot文档。
使用模式版本控制工具来管理Spring Boot,有两个好处。
-
从代码变更中分离数据变更
-
数据迁移和应用程序布署同步,你的布署过程变得简单
在本节内容中我们将关注数据库变更的两种方式。
-
非向后兼容
-
向后兼容
如果没有做好准备,不要用第一方式做零宕机布署,我们会给出警告。第二种方式,我们会提供一些零宕机向后兼容的布署建议。
我们的项目是一个简单的Spring Boot Flyway应用,在这个应用中,一个Person在数据库里有first_name和last_name两个字段。我们想把last_name重命名为surname。
假定
在深入细节之前,我们需要为应用做一些假定。主要是让过程变得简单。
提示:忠告,简化的流程可以为你节省很多钱(公司人越多,可以节省的钱越多)!
-
我们不需要做数据库回滚
不要简化布署过程(一些类似删除回滚,一些数据库回滚几乎不可能)。我们选择只做应用回滚。采用这种方式,即便你有不同的数据库(如SQL和NoSQL),但你的布署过程是一样的。
-
回滚一个版本(不是太多)通常是我们都能做到的
我们只回滚到所需要的版本。如果当前版本存在bug,并且不容易修复,我们希望可以回到上一个稳定版本。我们假定上一个稳定版本就是上一个版本。为多个布署维护代码和数据库兼容是困难且昂贵的。
提示:为了提高可读性,我们在文章中将只展示代码的新增内容。
第一步:初始化环境
应用版本: 1.0.0
数据库版本: v1
说明
这是我们要使用的应用程序初始状态
数据库变更
数据库包括last_name列
代码变更
应用程序将Person数据存储到last_name列:
-
使用非向后兼容方式重命名数据列
假设要修改列名,我们来看一下下面的例子:
警告:下面的例子是故意这样做的,它会崩溃。我们用它来展示数据库兼容问题。
应用版本: 2.0.0.BAD
数据库版本: v2bad
说明
当前变更不允许我们同时运行两个实例(新旧两个版本)。所以零宕机布署是难以达到的(根据我们的假定,这实际上是不可能的)。
A/B测试
在当前环境中我们布署了一个版本为1.0.0的应用程序和一个版本为v1的数据库。我们要布署另外一个实例使用版本为2.0.0的应用程序,并且将数据库升级到v2bad。
步骤:
-
布署一个新实例,应用程序版本为2.0.0.BAD,将数据库升级到v2bad
-
在v2bad 中数据库中已经没有last_name数据列,它已经重命名为surname
-
数据库和应用都已经成功升级,一个实例使用版本1.0.0,一个使用版本2.0.0.BAD.他们都使用版本为v2bad的数据库
-
版本为1.0.0的实例在插入last_name的时候会出现异常,因为last_name列已经不存在了
-
版本为2.0.0.BAD的实例可以正常运行
如果我们对数据库和应用程序做非向后兼容布署,我们无法使用A/B测试。
回滚应用
假设尝试A/B布署之后,我们需要将应用回滚到1.0.0.并且我们不希望回滚数据库。
步骤:
-
我们关闭版本为2.0.0.BAD的应用
-
database仍然使用v2bad
-
因为1.0.0版本的应用,不知道数据列surname的存在,它会抛出异常
-
灾难性故障,无法回滚
如果我们对数据库和应用做非向后兼容变更,我们无法回滚到前一个版本。
异常日志如下:
数据库变更
迁移脚本将数据列last_name重命名为surname
代码变更
我们已经将数据列lastName重命名为surname。
-
使用向后兼容方式重命名数据列
执行非向后兼容变更是我们经常遇到的情况。我们已经证实,如果不做些额外工作,无法通过简单的数据库迁移做到零宕机布署。本节内容将通过三次应用程序布署和数据库迁移达到我们期望的效果,并且同时做到向后兼容。
提示:作为提醒,假定我们使用V1版本的数据库,它包含列first_name和last_name。我们想把last_name变更为surname.而且我们还有一个1.0.0版本的应用还没有使用surname.
第二步: 添加surname
应用版本: 2.0.0
数据库版本: v2
说明
通过添加新的数据列和内容复制,我们已经通过向后兼容的方式对数据库做了变更。我们回滚一个JAR,同时有一个老版本的JAR在运行,它不会在运行时挂掉。
回滚一个新版本
步骤:
-
创建一个新数据列,命名为surname,并把你的数据库迁移过来。现在你的数据库版本是v2
-
将数据从last_name复制到surname。注意,如果你有大量的数据,你最好考虑批量迁移的方式!
-
写代码,同时使用新旧两个数据列。现在你的应用版本是2.0.0
-
如果surname列的值不为空,从它读取值,如果surname不存在则从last_name读取。你可以从代码中移除getLastName(),因为如果你把应用从3.0.0回滚到2.0.0,它将会产生空值。
如果你正在使用Spring Boot Flyway,通过这两步可以启动2.0.0版本的应用。如果你是手动运行数据库版本控制工具,你需要在一个独立的进程中操作(先手动升级数据库,再布署新应用)。
重要:记住,新创建的列不能使用NOT NULL,如果你做回滚,老版本应用程序不知道新的列,不会在插入数据时为它赋值。所以,如果你添加了这个约束,并且你的数据库版本是V2,那么新的数据列的值不能为空,否则违法约束。
重要:你需要移除getLastname()方法,因为在3.0.0版本中的代码中不知道数据列last_name。这意味着默认会把它的值设置为空。你也可以保留该方法,但要添加判空检查,但是最好的方案是getSurname()在逻辑上保障没有空值。
A/B测试
目前的情况是我们在生产环境布署了一个1.0.0版本的应用,数据库版本为v1。我们想再布署一个实例,使用2.0.0版本的应用,将数据库升级到v2.
步骤:
-
布署新实例,应用使用2.0.0,数据库升级到v2
-
此时有一些请求会被分发到使用1.0.0版本的实例
-
升级成功,版本为1.0.0的实例和版本为2.0.0的实例同时运行,都使用版本为v2的数据库
-
1.0.0不使用数据列surname,2.0.0使用。他们彼此没有交互,不会抛出异常。
-
2.0.0将数据保存到新旧两个数据列,所以是向后兼容的
重要:如果你要基于新/旧列进行一些统计查询,请记住,你有两个值(可能已经迁移)。例如,你想统计姓氏以A开头的用户数,除非你已经做好了数据迁移(从旧列到新列),否则你在新列上做统计会遇到数据不一致。
应用回滚
目前情况是应用程序版本为2.0.0,数据库版本为v2
步骤:
-
将应用程序回滚到版本1.0.0。
-
1.0.0不使用数据列surname,所以回滚成功。
数据变更
数据库包含一个数据列last_name
使用脚本添加surname列。
警告:记住,不要为新加列设置非空约束。因为JAR回滚时旧版本不知道新加列,会自动设置空值。如果有非空约束,旧版本应用会崩溃。
代码变更
我们将数据同时保存到last_name和surname.我们从last_name读取最新数据。在升级过程中一些请求可能会被分发到还没有更新的实例。
第三步:从代码中移除last name
应用程序版本:3.0.0
数据库版本: v3
说明
通过添加一个新列,并把内容复制过来,我们为数据库配置了向后兼容模式。如果我们回滚到JAR, 同时旧的JAR版本也在运行,系统不会奔溃。
回滚应用
目前的情况,应用是3.0.0版本,数据库是v3版本。应用3.0.0不再把数据存到last name列,这意味着,最新的数据是存储在surname列。
步骤:
-
回滚应用到2.0.0版本
-
版本2同时使用last name和surname。
-
如果surname不为null,版本2.0就使用surname。如果为null,就使用last name。
数据库变更
结构上,数据库没有变化。执行下面的代码,来实现数据迁移:
代码变更
我们用last name和surname两列来存储数据。同时,从last name读取数据,因为这一列数据是最新的。在部署阶段,有些需求可能会被没有更新的实例来执行。
第四步:从数据库移除last name
应用程序版本:4.0.0
数据库版本:v4
说明
因为应用3.0.0不使用last name,在从数据库移除这一列后,如果我们回滚到版本3.0.0,不会发生崩溃情况。
脚本执行的日志:
数据库变更
跟v3相比,我们移除了last name并且加上了约束。
代码变更:
无变化
通过一些向后兼容的方式,我们成功布署了重命名这样的非向后兼容变更。在此总结一下:
-
部署版本为1.0.0的应用程序和版本为v1的数据库(列名是last name)
-
部署版本为2.0.0的应用程序,数据存储在last name和surname两列,读取的时候只从last name取值。数据库是v2版本,包含last name和surname两列。surname列是last name的复制。(注意:这一列不能有非空约束)
-
部署版本为3.0.0应用程序,数据只存储在surname列,读取时从surname列取值。对于数据库,最后从last name迁移到surname。对于last name列,not null限制要取消。数据库现在是v3版本。
-
部署版本为4.0.0的应用程序-代码没有任何变化。部署版本为v4数据库,完成最后从last name 到surname的迁移,并删掉last name。并可以添加上需要的约束。
通过遵守这种方法,你可以做到回滚版本,而不会破坏数据库和应用程序的兼容。
代码
本文所使用的代码,都可以在Gitbub上下载到。更多的介绍,可以在下面看到。
项目:
克隆代码仓库后,你会看到如下文件结构:
脚本
你可以运行脚本来执行两种场景:数据库向后兼容和非向后兼容。
检查向后兼容的例子,请运行:
检查非向后兼容的例子,请运行:
所有的例子都克隆于Spring Boot Sample Flyway项目。
通过例子可以启动H2 console ,所以你可以浏览数据库的状态(默认的jdbc url 是jdbc:h2:mem:testdb)
译者:张万程
本文来自云栖社区合作伙伴"DBAplus",原文发布时间:2016-07-01