BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

Obevo简介:玩转数据库SDLC

| 作者 Shant Stepanian 关注 0 他的粉丝 ,译者 罗远航 关注 1 他的粉丝 发布于 2017年12月12日. 估计阅读时间: 25 分钟 | GMTC大前端的下一站,PWA、Web框架、Node等最新最热的大前端话题邀你一起共同探讨。

亲爱的读者:我们最近添加了一些个人消息定制功能,您只需选择感兴趣的技术主题,即可获取重要资讯的邮件和网页通知

本文要点

  • 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)并且通过标准的构建/发布机制进行部署。

由于我们数据库中的用例的多样性,这一过程变得十分复杂:

  1. 现在:全新的模式(schemas)对表进行部署,进行内存数据库测试,并从一开始就加入合适的SDLC
  2. 过去:超过10年的数据库系统从未有过受控的部署过程
  3. 复杂程度:具有成百上千的不同类型的对象,例如,数据库表、视图(view)、存储过程(stored procedures)、函数(function)、静态数据上传(static data uploads)以及数据迁移
  4. 费时:数据库表具有上百万条数据,部署需要花费数小时

不论使用用例是怎样的,SDLC过程本身就具有高度的复杂性,因为分布于不同地理位置的大量开发人员在不断地对数据库进行更改。

现有的开源工具可以处理简单一些的用例,但是它们无法适应我们现有系统的规模和复杂性。当然,我们也不能任由现有系统没有合适的SDLC,这些系统都是需要积极开发与发布的系统。

因此,我们开发了Obevo(开源于Apache 2.0协议),它是能够处理所有用例的一个工具。Obevo于其它工具的主要区别在于,它通过文件维护每一个数据库对象(类似于如何在每个文件中存储类定义),与此同时它还能够处理增量部署。

在这篇文章中,我们将会讨论数据库部署的问题空间,之后会阐述基于对象的项目结构如何帮助我们优雅地为各种对象以及在不同环境下管理成百上千的数据库模式对象。

Obevo特性速查表:人人必读
对于新系统 对于现存系统以及复杂系统

易于维护、审查和部署

对数据库表的选择性部署更易于测试

支持大多数数据库表脚本的内存测试数据库的转换

易于逆向工程以及集成

支持有状态对象(数据库表)以及无状态对象(视图、存储过程、函数……)

易于支持成百上千的对象

数据库对象类型(有状态和无状态)

首先,让我们回顾一下不同的数据库对象类型的部署语义,这会影响到工具的设计。

本篇文章术语速查:
1.使用代码或者SQL语句来修改数据库的行为被称为部署迁移 
2. 被部署的代码单元被称为scriptlet 
3.
一个文件可能包含多个scriptlet,也就是说,scriptlet不等同于scriptlet文件。一个文件包含一个scriptlet还是多个scriptlet也是这篇文章将要讨论的一个话题。

有状态(stateful)对象(例如,数据库表)

有状态对象需要对其定义进行增量修改,而不是完全的定义替换。下面是向MyTable添加两列的例子。

理想状态下,我们可以通过指定一个定义包含所有四列的表的SQL语句来完成对数据库的添加。然而可惜的是,SQL并没有为此提供一个可行的解决方案:

  1. 删除或者重建表意味着你会损失数据
  2. 将数据存储到临时的表是一项昂贵而又复杂的操作

相反,关系型数据库管理系统(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)部署至空数据库中

开发人员可能希望将其部署到一个空的沙盒数据库中:

  1. 验证你的SQL scriptlet的实际部署情况
  2. 运行包含数据库的系统集成测试(例如,测试添加一个新列或一个新的存储过程)
  3. 在单元测试中测试数据库访问代码,同时测试内存数据库

有许多方法是可行的:

  1. 从头开始恢复所有的迁移scriptlet,前提是你的包中已经保存了之前的所有scriptlet
  2. 通过重新部署scriptlet,这样它们就可以部署到一个空的数据库中,同时还允许后续的增量生产部署。

C)保证可维护性和可读性

在我们的数据库进行改革之前,我们看到了一些团队,他们会维护一个包含对象s定义的数据库对象的文件,即使这些文件没有被用于部署。

这似乎毫无意义,但我们从中获得了一些想法:

  • 开发人员喜欢在数据库对象和结构化的代码中进行可视化
  • HibernateReladomo这种ORM工具会生成显示结构化的DDL,并且会额外将这些数据绑定到数据库实例上

通用数据库部署工具设计

主要的部署算法

通过上面给出的原则,大多数基于源代码版本控制的数据库部署工具(包括Obevo)都是这样的:

  1. 开发人员为他们的下一个版本编写scriptlet,并在源代码控制中添加之前已部署的scriptlet。
  2. scriptlet会经过测试并且构建到包中
  3. 该包会部署至目标数据库
    1. 更简单的工具需要部署人员指定scriptlet进行部署
    2. 高级的工具会通过对部署表进行对比来决定需要部署哪些scriptlet,如下图所示
      1. 这样做能够对任何环境使用相同的包和部署命令,而不必考虑之前部署的包的版本

有状态对象和无状态对象的部署语义

对象类型会对变更集(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到文件的对应关系 每个文件对应1到多个scriptlet 每个文件对应1个scriptlet
scriptlet命名转换 <objectName>.<changeName> <objectName>
示例

分析:无状态对象处理

基于对象的结构对于无状态对象非常方便,因为完整的无状态对象定义可以在一个文件中维护,并且可以很便捷地进行修改。

相比之下,从技术上讲,它能够以一种增量的状态方式来处理无状态对象的部署,例如,在多个版本中都存在的增量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可以处理这个问题——简言之,我们提供了一个“迁移”对象的概念来帮助解决这个问题:

  1. 让我们定义scriptlet更新来促进这些迁移
  2. 允许每个对象scriptlet只保留与它的定义相关的scriptlet,因此保留了单独进行部署测试的能力

更多详细内容,请参看项目文档

对现有表的逆向工程

我们已经向你证明,你可以使用Obevo对十分复杂的模式进行部署。但是,为了现有系统能够使用Obevo,我们必须让开发人员能够很容易地对现有的数据库进行反向工程。

这并不是一个简单的问题,因为,不幸的是,没有任何一个API能够在不同的DBMS(数据库管理系统)完美工作。

  • Java + JDBC的方案提供了DatabaseMetaData API,但是它在不同的DBMS中是具有不同实现的
  • 一些第三方工具试图弥补这个缺口,但是这些工具并不能涵盖所有供应商会使用的所有详细的SQL语法,并且它们也不能及时涵盖DBMS发布的新特性

因此,我们选择的方案是将供应商提供的反向工程工具集成至Obevo(如下表所示)。一些工具只是将完整的模式保存到一个文件,但是我们提供了一个实用工具,它利用简单的字符串解析和正则表达式技术,将这些文件转换为Obevo的基于对象的结构。我们发现这种技术对于现有系统来说更为可靠,特别是因为核心供应商的工具比Java API更加了解该如何解析他们自己的模式。

DBMS Obevo利用的工具
DB2 DB2LOOK
Postgres pg_dump
SQL Server Microsoft.SqlServer.Management.Smo.Scripter class in Powershell
Oracle DBMS_METADATA API via JDBC
Sybase ASE ddlgen

总结

尽管有许多开源工具都可以进行数据库部署,但是我们觉得更复杂的用例需要更强大的工具。

对于Obevo,我们的目标是支持所有类型的系统,通过我们的测试功能和简单的基于对象的维护,或者通过让他们的老系统在SDLC控制下重新焕发光彩,来提高现代系统的生产力。

在这篇文章中,我们有许多特性和数据库部署活动都没有进行介绍(比如,回滚分阶段部署内存数据库测试多模式管理)。请随时访问我们的Github页面项目文档kata lessons,以了解更多关于Obevo的信息,以及了解该如何将这个工具应用到你的系统。

我们希望多写一些关于我们的实践和DB提升经验的文章,读者们所以可以随意发表评论,并向我们提出任何问题。

关于作者

Shant Stepanian 是高盛集团(Goldman Sachs)平台业务部的高级工程师。他是Obevo的首席架构师并且领导了高盛的数据库SDLC改革。他研究了各种系统架构,从基于批处理的报告应用数据库系统,到利用分布式和分区内存数据网格的高吞吐量OLTP系统。

查看英文原文:Article: Introducing Obevo: Get Your Database SDLC Under Control

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT