本文要点
- Obevo 是由高盛集团开发的一款企业级数据库部署工具,它发布于 2017 年,是一款采用 Apache 2.0 License 协议的开源项目。
- Obevo 利用数据库脚本来管理每一个对象,这种脚本类似于应用程序代码,为开发人员提供了许多优势。
- Obevo 可以帮助现有数据库的新应用程序和系统在受控于 SDLC(系统生命开发周期)的同时,变更它们的数据库管理。
- 开发团队可以通过 Obevo 的集成工具和应用实例来快速上手 Obevo。
- Obevo 包含许多额外的特性,包括回滚支持、内存数据库(in-memory database)测试以及分阶段部署。
近年来,高盛采用了标准的软件开发生命周期(SDLC)来进行应用程序的构建和部署。这包括对于新的和现有系统的数据库模式(database schema)定义的版本控制,这比管理应用程序代码要困难得多。在这篇文章中,我们将谈到 Obevo 是如何帮助我们管理众多的企业级应用数据库的 SDLC(系统开发生命周期)的,Obevo 是高盛集团于近期开源的 DB 部署工具。
部署企业级数据库的问题空间
由于许多原因,将数据库定义加载到标准的 SDLC 过程是具有挑战性的,尤其是由于数据库具有状态这一特性以及数据库需要执行增量迁移。因此,许多应用程序从来都没有经历过自动化或平滑的数据库部署过程。我们的目标是,在相同的 SDLC 下对数据库模式进行管理:将所有的数据库模式定义提交大一个版本控制系统(VCS)并且通过标准的构建 / 发布机制进行部署。
由于我们数据库中的用例的多样性,这一过程变得十分复杂:
- 现在:全新的模式(schemas)对表进行部署,进行内存数据库测试,并从一开始就加入合适的 SDLC
- 过去:超过 10 年的数据库系统从未有过受控的部署过程
- 复杂程度:具有成百上千的不同类型的对象,例如,数据库表、视图(view)、存储过程(stored procedures)、函数(function)、静态数据上传(static data uploads)以及数据迁移
- 费时:数据库表具有上百万条数据,部署需要花费数小时
不论使用用例是怎样的,SDLC 过程本身就具有高度的复杂性,因为分布于不同地理位置的大量开发人员在不断地对数据库进行更改。
现有的开源工具可以处理简单一些的用例,但是它们无法适应我们现有系统的规模和复杂性。当然,我们也不能任由现有系统没有合适的 SDLC,这些系统都是需要积极开发与发布的系统。
因此,我们开发了 Obevo (开源于 Apache 2.0 协议),它是能够处理所有用例的一个工具。Obevo 于其它工具的主要区别在于,它通过文件维护每一个数据库对象(类似于如何在每个文件中存储类定义),与此同时它还能够处理增量部署。
在这篇文章中,我们将会讨论数据库部署的问题空间,之后会阐述基于对象的项目结构如何帮助我们优雅地为各种对象以及在不同环境下管理成百上千的数据库模式对象。
Obevo 特性速查表:人人必读 对于新系统 对于现存系统以及复杂系统 易于维护、审查和部署
对数据库表的选择性部署更易于测试
支持大多数数据库表脚本的内存测试数据库的转换
易于逆向工程以及集成
支持有状态对象(数据库表)以及无状态对象(视图、存储过程、函数……)
易于支持成百上千的对象
数据库对象类型(有状态和无状态)
首先,让我们回顾一下不同的数据库对象类型的部署语义,这会影响到工具的设计。
本篇文章术语速查: 1.使用代码或者 SQL 语句来修改数据库的行为被称为 _ 部署 _ 或 _ 迁移 _
2. 被部署的代码单元被称为 _scriptlet_
3.一个文件可能包含多个 scriptlet,也就是说,scriptlet 不等同于 scriptlet 文件。一个文件包含一个 scriptlet 还是多个 scriptlet 也是这篇文章将要讨论的一个话题。### 有状态(stateful)对象(例如,数据库表)
有状态对象需要对其定义进行增量修改,而不是完全的定义替换。下面是向 MyTable 添加两列的例子。
理想状态下,我们可以通过指定一个定义包含所有四列的表的 SQL 语句来完成对数据库的添加。然而可惜的是,SQL 并没有为此提供一个可行的解决方案:
- 删除或者重建表意味着你会损失数据
- 将数据存储到临时的表是一项昂贵而又复杂的操作
相反,关系型数据库管理系统(RDBMS)可以让用户通过 ALTER 语句来对现有的表进行修改。
一些列的升级可能需要进行数据迁移,例如将一列数据从一个表移动到另一个表。
因此,一个对象是应用了多个 scriptlet 之后的结果,最开始的 scriptlet 会创建一个表,后续的 scriptlet 可能会对这个表进行修改。
无状态对象(例如,视图、存储过程)
另一方面,一个无状态对象可以通过指定其完整的对象定义来进行创建和修改。
- “DROP + CREATE”语句或者“CREATE + REPLACE”语句是可用的
- “DROP + CREATE”语句是安全的,因为这些对象没有数据 / 状态
我们还把静态数据文件(即代码或者引用数据表reference data tables)视作无状态对象。尽管它涉及到了表中的数据,不论你是批量地执行“delete + insert”操作还是有选择性地执行“insert + update + delete”操作,数据已经在你的scriptlet 中定义完全了,并且部署到了表中。
数据库部署工具原则
Martin Fowler 的这篇文章很好地介绍了大多数基于源代码控制的数据库部署工具所遵循的关键原则,其中还提供了具体的相关重点内容。
1)所有的数据库构件的版本都是由应用程序代码所控制(而不是通过 UI 界面进行管理)
对于不太懂技术的人来说,基于UI 的管理可能能易于接受。但是,我们建议开发团队应该在源代码控制中管理他们的数据库scriptlet(因为这也应该作为应用程序的一部分),并以一种自动化的方式调用部署。
2)显式的编码增量迁移(coded incremental migration)是有状态对象的首选(而不是自动计算迁移 auto-calculated migration)
自动计算迁移在企业级环境中是有风险的,例如在当前的数据库表状态和代码的完整视图之间进行的迁移。
数据库部署工具的功能需求
我们根据一个数据库部署工具如何处理以下的这些需求来评估它。
A)对一个现有的数据库进行增量变更部署
这是数据库部署工具的主要功能;大多数生产部署都是这样执行的。这种方法也在一些非生产环境中使用,特别是在产品发布之前的 QA 环境中对部署进行测试。
B)将完整模式(schema)部署至空数据库中
开发人员可能希望将其部署到一个空的沙盒数据库中:
- 验证你的 SQL scriptlet 的实际部署情况
- 运行包含数据库的系统集成测试(例如,测试添加一个新列或一个新的存储过程)
- 在单元测试中测试数据库访问代码,同时测试内存数据库
有许多方法是可行的:
- 从头开始恢复所有的迁移 scriptlet,前提是你的包中已经保存了之前的所有 scriptlet
- 通过重新部署 scriptlet,这样它们就可以部署到一个空的数据库中,同时还允许后续的增量生产部署。
C)保证可维护性和可读性
在我们的数据库进行改革之前,我们看到了一些团队,他们会维护一个包含对象 s 定义的数据库对象的文件,即使这些文件没有被用于部署。
这似乎毫无意义,但我们从中获得了一些想法:
通用数据库部署工具设计
主要的部署算法
通过上面给出的原则,大多数基于源代码版本控制的数据库部署工具(包括 Obevo)都是这样的:
- 开发人员为他们的下一个版本编写 scriptlet,并在源代码控制中添加之前已部署的 scriptlet。
- scriptlet 会经过测试并且构建到包中
- 该包会部署至目标数据库
- 更简单的工具需要部署人员指定 scriptlet 进行部署
- 高级的工具会通过对部署表进行对比来决定需要部署哪些 scriptlet,如下图所示
- 这样做能够对任何环境使用相同的包和部署命令,而不必考虑之前部署的包的版本
有状态对象和无状态对象的部署语义
对象类型会对变更集(changeset)的计算语义产生影响。
- 无状态对象允许对 scriptlet 进行添加、删除和更新:对象定义 (假设它是有效的) 可以在数据库中替换现有的定义,而不会丢失数据
- 然而,有状态对象通常只允许对 scriptlet 进行添加:更新已经部署的 scriptlet,可能导致对象的结果与预期的不同。
为了演示有状态对象的使用用例,我们部署版本为 1 的包以获得正确的表
现在,我们假设有人修改了 M3,对表的列进行了重命名,然后我们重新部署。我们期望的结果是什么呢?
工具检测到了一处不匹配的地方:
- scriptlet M3 被修改了,并且它要往表中增加一列 C123456。
- 但是数据库中已经部署了一列 C。
- 源 scriptlet 不再包含列 C,但是没有办法从数据库中删除它。
因此,通用规则:有状态对象 scriptlet 在部署后不能被修改。
某些可选择的功能,让我们可以在需要的时候进行处理,比如:
- 回滚(rollback):提供一个在未部署时使用的显式回滚 scriptlet
- 重新制定基准(re-baselining):将已经部署的 scriptlet 重写为更简洁的 scriptlet,而不需要重新进行部署
不同的实现选择
考虑到它们的基本算法是相似的,部署工具在几个实现的方面略有不同。
1)如何通过文件对 scriptlet 进行管理
有以下选项,其中包括对 scriptlet 进行分组:
- 发布(release)
- 修改对象
- 不对所有的 scriptlet 进行分组,保持单独的迁移
2)如何对部署的 scriptlet 进行排序
有以下选项可供参考:
- 一个明确列出迁移顺序的单独文件
- 一个标明了能够决定迁移顺序的约定文件
- 通过依赖分析找出 scriptlet 的顺序
接下来,我们会详细介绍 Obevo 是如何解决这两个问题的。
Obevo 设计:基于对象的 Scriptlet 管理
我们主要的数据库部署问题是对模式中大量对象的管理:开发、维护与部署。我们还需要开发人员处理他们的数据库对象并且为他们的应用程序进行编码。
因此,我们希望提供一种用户体验,让你觉得使用我们的工具就像是拥有一帮开发人员为你服务一样,我们会根据对象的名称来管理你的 scriptlet。我们将在这一部分深入探讨这些细节。
(这种结构在排序上增加了一些挑战性,我们将在本文的下一节中讨论这些问题。)
项目结构基础
我们根据 scriptlet 所应用的对象来组织 scriptlet,如下所示:
文件结构根据对象是有状态的还是无状态的而不同。
- 无状态对象可以将定义本身存储在文件中,因为它们的完整定义可以部署到数据库中。
- 然而,有状态对象需要使用增量 scriptlet 进行部署。
- 因此,需要使用多个 scriptlet 来将对象引入当前状态,并将所有对象保存在同一个文件中
- 我们将文件分割为多个 scriptlet,然后使用“//// CHANGE”将其分隔开来
分析:无状态对象处理
基于对象的结构对于无状态对象非常方便,因为完整的无状态对象定义可以在一个文件中维护,并且可以很便捷地进行修改。
相比之下,从技术上讲,它能够以一种增量的状态方式来处理无状态对象的部署,例如,在多个版本中都存在的增量 scriptlet。但是,当对象在多个版本上发生变化时,这就导致了数据库脚本中的冗余。
分析:可读性
从维护的角度看,这个项目结构有许多优势:
从项目结构入手,数据库结构是非常易读的。
- 如果对对象进行更改或检查,可以清楚地看到要查找的文件。
- 可以在同一位置轻松地查看对象定义,而不是在其他地方进行查看 (例如在其他文件中或其数据库中)。
- 即使 scriptlet 可以在一个有状态对象的文件中不断积累,但是我们可以将多个 scriptlet 合并到一起而不执行任何部署的重新制定基准(re-baselining)特性来减小文件的大小。
相比较之下,如果像许多工具所支持的那样,使用一个示例项目结构,其中一个文件与迁移或发布相关联,这可能会导致一些问题:
-
降低对象可读性:如果一个对象在许多版本中被修改,它的结构将被分散到多个文件(对于有状态的对象而言),或者在许多文件中会轻易产生冗余(比如先前所演示的无状态对象)。
-
不可读对象和不可写对象的堆积:会由于之前的某个时间点的操作导致不可读,没有按规则修改有状态对象脚本会导致不可写。
-
虽然重新制定基准可以减少文件计数,但它必须基于一个完整的模式来完成而不是仅仅针对一个对象。但是,于基于对象的项目结构相比:
- 重新定制基准付出的代价要更大
- 重新定制基准的结果可能会使得生成的文件更庞大、更不易于阅读
从代码审查 / 发布审查的角度来看:基于对象的结构意味着特定版本中的所有更改都将分散到文件中。乍一看,对下一个版本部署的 scriptlet 进行审查似乎有些困难。但是,我们仍然可以通过比较 VCS 的历史和标记来对代码进行审查,就像我们对应用程序代码所做的那样。
分析:对开发人员的好处
开发人员会从 Obevo 项目结构获得许多好处。
当对象的 scriptlet 被放在一个文件中,我们可以轻松地在测试中部署单独的对象,这对于在内存数据库的进行数据访问 API 的单元测试很有用。
开发人员还可以利用ORM 工具从自己的应用程序类生成DDL,然后重新编译它们部署至他们的迁移scriptlet 中。由于篇幅有限,我们不会在这里进行深入研究,但是您可以在我们的文档中阅读到更多详细信息。
Obevo 设计:通过依赖分析进行排序
在上一节的内容中,我们谈到了选择基于对象的项目结构会带来许多好处,但是它使得排序问题变得更加复杂了。
对象是相互依赖的,当我们在一个模式中扩展到成百上千的对象时,手动指定一个顺序变得越来越困难。
接下来我们会讨论该如何克服这一挑战。
排序算法
针对有状态迁移的对象之间的依赖关系,并不是所有的 scriptlet 都是互相依赖的,因此,在对象以来声明的约束下,我们的排序还是有一些灵活性存在的。所以,我们设计了一个简单的图算法作为解决方案。
以下面的语句为例:
- 其中 3 个是创建表
- 1 个是创建一个 foreign key
- 1 个是创建一个视图:
注意以下内容:
- 我们创建表格的顺序无关紧要
- foreign key 的创建必须在 TABLE_A 和 TABLE_B 创建完成之后
- VIEW1 的创建必须在 TABLE_A 创建完成之后
这本身就能通过一个有向图进行表示,图的节点表示的是 scr,图的边表示的是顺序依赖。
现在,我们可以使用拓扑排序算法来得到一个满足这些排序约束的可行的排序结果,并通过它成功地部署我们的数据库。
拓扑排序可以得到许多可行的排序,但是我们调整了算法的使用,使得它每次都提供一个相同的排序,这样我们就可以在不同的环境中保持行为的一致。
现在我们要谈论下最后一个细节问题:我们该如何在scriptlet 中声明它们之间的依赖呢?
依赖声明和依赖发现
我们找到了一种最简单的在scriptlet 中声明依赖的方法。请查看下图scriptlet 里TABLE_B.fkA 中的 dependencies属性。
然而,这种方式在处理大型数据库时对开发人员十分不友好(试想开发人员要对成百上千个对象进行注释),因此,我们需要一种方法来自动检测依赖,同时还要允许开发人员根据具体需要进行重写。
我们使用两种策略来对依赖进行推断:
有状态迁移的对象内部依赖
我们允许有状态对象定义多个scriptlet。自然而然,其中隐含的信息是,文件中的迁移是按照它们所编写的顺序进行部署的,因此我们在图中推断出这种依赖关系。
通过文本搜索发现跨对象间依赖
为了检测跨对象的依赖关系,我们需要搜索scriptlet 的内容以查找相关的对象。
从技术上来说,理想的方案是对SQL 语句进行解析以查找这些对象。但是,这是很困难的,因为我们需要了解所有我们要支持的DBMS 的 SQL 语法。
相反,Obevo 采取了一种简单而通俗的方法:选择那些在你的项目中通过字符串搜索找到的对象名称,并且假设这些是依赖关系。
具体实现的注意事项:
- 可以通过简单地列出项目中的文件来找到可用的对象名称
- 有许多种方法都能从 scriptlet 中搜索到对象名称。我们目前的实现是通过空格将 scriptlet 分词成为一个个的 token,然后检查该 token 是否存在于对象名称的集合中
- 在算法实现中有更多的细微差别,但是上面提到的内容对于这篇文章来说就已经足够了
这是我们之前的例子的算法结果:
如果出现错误的匹配(比如说由于注释产生的错误匹配)或错误的遗漏,开发人员可以根据需要指定排除一些依赖或自己进行重写。
乍看起来,可能很难想象这种方法能够适用于实际的用例,但是我们已经成功地使用了这种技术来部署许多复杂的模式,其中包括表、存储过程、视图等数千个对象。
如果你想要看实际中的例子,请参考我们的 kata lesson ,其中展示了对于一个大型数据库模式的逆向工程示例。
在多个表之间进行数据迁移
我们将会简单的介绍一下下面的这个用例(将数据从一个旧的列移动到一个新列,然后删除旧的列),在最开始时,将基于对象的文件结构的概念应用到这个例子中似乎比较困难。
Obevo 可以处理这个问题——简言之,我们提供了一个“迁移”对象的概念来帮助解决这个问题:
- 让我们定义 scriptlet 更新来促进这些迁移
- 允许每个对象 scriptlet 只保留与它的定义相关的 scriptlet,因此保留了单独进行部署测试的能力
更多详细内容,请参看项目文档。
对现有表的逆向工程
我们已经向你证明,你可以使用 Obevo 对十分复杂的模式进行部署。但是,为了现有系统能够使用 Obevo,我们必须让开发人员能够很容易地对现有的数据库进行反向工程。
这并不是一个简单的问题,因为,不幸的是,没有任何一个 API 能够在不同的 DBMS(数据库管理系统)完美工作。
- Java + JDBC 的方案提供了 DatabaseMetaData API ,但是它在不同的 DBMS 中是具有不同实现的
- 一些第三方工具试图弥补这个缺口,但是这些工具并不能涵盖所有供应商会使用的所有详细的 SQL 语法,并且它们也不能及时涵盖 DBMS 发布的新特性
因此,我们选择的方案是将供应商提供的反向工程工具集成至 Obevo(如下表所示)。一些工具只是将完整的模式保存到一个文件,但是我们提供了一个实用工具,它利用简单的字符串解析和正则表达式技术,将这些文件转换为 Obevo 的基于对象的结构。我们发现这种技术对于现有系统来说更为可靠,特别是因为核心供应商的工具比 Java API 更加了解该如何解析他们自己的模式。
总结
尽管有许多开源工具都可以进行数据库部署,但是我们觉得更复杂的用例需要更强大的工具。
对于 Obevo,我们的目标是支持所有类型的系统,通过我们的测试功能和简单的基于对象的维护,或者通过让他们的老系统在 SDLC 控制下重新焕发光彩,来提高现代系统的生产力。
在这篇文章中,我们有许多特性和数据库部署活动都没有进行介绍(比如,回滚、分阶段部署、内存数据库测试、多模式管理)。请随时访问我们的 Github 页面、项目文档和 kata lessons ,以了解更多关于 Obevo 的信息,以及了解该如何将这个工具应用到你的系统。
我们希望多写一些关于我们的实践和 DB 提升经验的文章,读者们所以可以随意发表评论,并向我们提出任何问题。
关于作者
Shant Stepanian 是高盛集团(Goldman Sachs)平台业务部的高级工程师。他是 Obevo 的首席架构师并且领导了高盛的数据库 SDLC 改革。他研究了各种系统架构,从基于批处理的报告应用数据库系统,到利用分布式和分区内存数据网格的高吞吐量 OLTP 系统。
查看英文原文: Article: Introducing Obevo: Get Your Database SDLC Under Control
评论