我们如何管理跨环境的跨数据库依赖关系?

kry*_*tah 5 sql-server best-practices development continuous-integration

我推迟问这个问题有一段时间了,因为在没有文字墙的情况下很难概括我们的情况和挑战,但情况越来越糟,所以我会尽力而为。我正在寻求一些帮助,以改进我们开发和管理应用程序数据库和开发人员环境的方式,特别是在跨环境的数据库依赖项方面。

关于我们

我们是一家拥有大量遗留代码的中型公司。为了了解我们当前的应用程序数据库是什么样子,这里有一些大致的数字:50GB、450 个表、200 个视图和 400 个存储过程。此外,我们的生产服务器运行大约 15 个数据库,其中大部分需要或被我们的应用程序数据库需要。

澄清一下:当我说“需要”时,我指的是不会编译/将编译但不会在没有依赖项的情况下运行的数据库对象。这些对象的示例是链接服务器和复制订阅等服务器对象,或存储过程和视图等数据库对象。

在过去的一年中,我们对开发和部署数据库的方式进行了重大改进。迄今为止的改进包括引入专用开发人员环境、(几乎)所有数据库代码的版本控制、从 Git(基于触发器)自动部署以及向 SQL Server 集群的过渡。

问题

我们正在努力解决的问题是如何处理从我们的应用程序数据库到其他数据库的依赖关系,而我似乎找不到合适的资源。这些依赖关系分为两个不同的挑战:

1. 同一台服务器上的数据库

目前来说,我们的应用数据库依赖于同一台服务器上的 5 个数据库。这些是具有单独存储库、部署管道、库和 Web 项目的数据库。在引导开发人员环境时,我们必须注意以特定顺序创建这些环境,以便成功应用 DDL 和 DML 脚本,以免我们面临依赖错误。仅此过程就引起了很多头痛。事实上,它引起了如此多的头痛,以至于我们的一些开发人员干脆放弃了本地开发人员环境,并在共享数据库中进行所有开发。

2. 远程服务器上的数据库只能用于生产

在我们的生产环境中,我们从少数远程 SQL Server 实例导入数据。其中一些数据是使用存储过程导入的,这些存储过程使用链接服务器对象引用远程服务器。为了运行存储过程,链接服务器对象需要存在。为了使链接服务器对象“成功”存在,它引用的远程服务器必须是可访问的。远程服务器只能从我们的生产服务器访问(这是正确的),但这会导致我们的存储过程在部署期间无法正确编译。

在“持续交付”一书中,作者 Dave Farley 强调,在真正的持续集成中,组装和运行项目所需的每一个资源都应该驻留在其存储库中。此外,他还指定每个环境都应该相同(凭据和连接字符串等配置除外)。我们的应用程序不满足这些原则,我什至不确定这样做是否可行。

我们的工具

  • 数据库服务器:Microsoft SQL Server 2017
  • 构建服务器:Visual Team Services
  • 构建工具:Redgate DLM 自动化套件

感觉就像我在这里错过了一些核心架构原则。我们可以做些什么来缓解这些问题?也欢迎对相关文献提出建议。

Sol*_*zky 1

2. 远程服务器上的数据库只能在生产环境中访问

...每个环境都应该相同(除了凭据和连接字符串等配置之外)...

对于链接服务器来说,这其实并不是那么困难。您只需要正确地对它们进行分类:它们本身是系统的组件,并且在不同的环境中应该是相同的,但是它们的定义是在环境之间通常不同的配置。这意味着,跨环境需要保持一致的是链接服务器的存在及其名称。这样,对链接服务器的任何和所有引用在不同环境中都可以是相同的。

对于非生产环境,您只需将实例设置为链接服务器的虚拟目标,以便在编译代码时对其进行的验证将会成功。如果需要,可以使用 SQL Server Express 来实现这一点。您不需要此实例中的任何数据,只需要代码中显式引用的架构。您可以在环境之间共享这些虚拟目标之一,但您可能应该拥有其中两个:一个用于当前结构,另一个用于在那些远程资源中进行的更改,您需要在它们上线之前对其进行测试(假设您会收到即将发生的更改的通知)。

如果不是很明显,这些虚拟目标都是共享资源,无论开发人员是针对其工作站上的私有本地实例还是针对共享实例进行开发。

虚拟目标的架构不会与您的应用程序/数据库代码混合。它要么被视为配置,要么被视为构建过程/CI 过程的组件。

1.同一服务器上的数据库

(乱序引用)在引导开发人员环境时,我们必须注意以特定顺序创建这些环境,以便成功应用 DDL 和 DML 脚本,以免我们面临依赖错误……这导致了一些令人头疼的问题我们的开发人员干脆放弃了本地开发环境,并在共享数据库中进行所有开发。

我理解开发私有数据库以防止外部干扰背后的理想主义,但在实践中我不确定它有多么必要。我工作的环境很容易就有 20 个数据库用于 7 - 10 个产品、超过 1000 个表、超过 3000 个存储过程等。我们很容易就有 7 个产品团队。一些数据库专用于(即隔离)特定产品,一些数据库在多个产品之间共享。我省略了其他复杂性,但重点是这是一个比您所描述的更复杂的环境。我们在所有环境中共享数据库:开发、QA、登台等。在我在那里工作的 6 年里,我能想到可能有几次出现跨团队干扰的情况。当有的时候,我们只是互相讨论情况并想出一个不妨碍彼此的方法。有时我们会重新安排处理特定故事/任务的顺序,以便更快地完成依赖关系,或者如果不可能,我们可能会删除某些内容/对即将完成但尚未完成的某些内容进行部分更改是时候到正确的时候了。我不记得曾经有过团队根本无法进行的情况(如果我忘记了,那么这种情况仍然很少见)。如果依赖性对于故事/任务来说是一个很大的因素,那么我们通常会在计划冲刺之前讨论它,以便我们知道我们的故事是否可以继续进行,或者是否必须将其转移到下一个冲刺,而另一个冲刺团队做出了改变。

综上所述,要使其在私有开发沙箱的开发工作站上运行需要一些工作,并且可能不值得投入时间来使其运行。特别是因为您需要处理的不仅仅是引导。如果这只是初始设置的问题,那么我仍然建议恢复共享开发实例的最新备份(因为无论如何您仍然会有其中之一,对吧,在代码签入时?就像应该有一个共享应用程序服务器,它从所有团队获取部署,以确保在迁移到 QA 之前一切都正确编译)。我没有看到运行 SQL 脚本有任何好处,因为这仍然使私有实例没有数据。您希望每个开发人员花费多少时间向表添加行以便他们可以测试他们的更改?这不仅浪费时间,而且很容易出错,因为我们都知道它们不会通过应用程序(以及应用程序逻辑),而只是直接插入或使用 SSMS 编辑表( s) 添加行。他们只会在理想的条件下添加最少的行数。因此,您最终会得到更多的代码实例,这些代码在功能上无法针对真实数据工作,而开发人员会说:“好吧,它可以在我的盒子上工作”。

但这不仅仅是结构初始数量(希望还有数据)的问题。您仍然需要推迟其他人提交的更改。现在这甚至更有可能失败,因为另一个团队可能已经完成了他们的项目,该项目要么在被推送到开发工作站时中断,要么由于环境发生变化,开发工作站的更改无法合并到共享数据库中。在共享实例中,这些相互依赖关系在开发周期中很早就被发现并考虑在内。

综上所述,如果您的开发人员当前没有半频繁地互相阻止,那么我建议坚持使用当前的共享实例设置。

...这些是具有单独存储库、部署管道的数据库,...

单独的存储库、库等不是问题。但是单独的部署肯定会让事情变得复杂,其程度取决于依赖项所在的方向。依赖项是否在项目之间双向存在,或者总是从一个项目/数据库到另一个项目/数据库?

我对全自动数据库部署不太有信心。签入代码并让流程盲目地获取所有内容并对其执行某些操作对于应用程序代码来说效果很好,因为应用程序代码就是这样:代码。您获取提交/签入的文件,编译它们,然后您就得到了成品(通常)。如果库之间存在依赖关系,则可以通过按正确的顺序构建项目来处理它们。

但就其本质而言,数据库不仅仅是无状态代码:它们还包含状态。因此,我不知道如何绕过自定义部署脚本(在单一数据库和/或没有太多数据之外)。我知道有些人能够使用自动迁移脚本,但我从未在可以使用的环境中工作过。因此,尽管我强烈主张将所有代码保留在源代码控制中,但我永远不会考虑从中生成部署脚本。

由于我们的复杂结构(以及我们部署到的服务器/数据库的数量 - 相同的模式部署到 4 到 20 多个实例,具体取决于产品,并且每个实例有多个数据库 - 我们有一个自定义部署系统。它是多线程的,而且非常好,实际上(我相信主要使用 nAnt 和 CruiseControl)。我们的流程是在构建系统处理的存储库(与 Trunk 分开)中拥有一个单独的发布脚本文件夹(每个版本) ,由签入触发。我们为每个项目都有文件夹,并且在每个项目文件夹中,我们都有用于架构(表、视图和触发器)以及数据、函数、存储过程等的文件夹。当我们更新存储过程和函数时(即代码)我们将对象脚本签入我们的团队分支和相应的发布文件夹中。所有发布脚本都需要是幂等的(即可重新运行)。构建过程每次都以相同的顺序处理文件夹,并运行“代码”文件夹之前的架构和数据文件夹。对于架构和数据更改,我们将对象脚本(基本 CREATE 语句)签入团队分支,并将自定义迁移脚本签入发布脚本文件夹。这使我们能够正确处理事务、依赖项等。此外,每个发布脚本文件夹中的所有脚本都以 3 位订单号为前缀:001 - date - this change.sql002 - date - shortDescription.sql等。这样每次运行时都会以相同的方式处理发布,并且在 3 周的冲刺过程中运行了数百次。如果我们在一个共享资源的数据库中工作,并且需要进行很多更改,我们只需通过使用我们需要的.sql第一个和最后一个订单号签入空脚本来“保留”我们需要的插槽数量。每个人总是在提交任何内容之前进行更新,因此我们总是知道下一个可用的订单号是什么。最坏的情况是,如果两个人在大致相同的时间签入相同的订单号,我们会重命名一个脚本来调整订单号(但同样,这种情况非常罕见,即使我们有 10 个人不断地签入东西)。

一件棘手的事情是管理链接服务器。我们有一个数据库,所有链接服务器都存在(规则是它们只能位于该一个数据库中)。而且,为了让它变得更好,我们使用了复制,并且必须确保在数据更改之前在订阅者处进行架构更改。为了完成这项工作,我们为这个数据库设置了两个发布脚本文件夹:其中一个是要处理的第一个发布脚本文件夹,另一个是要处理的最后一个发布脚本文件夹。架构更改进入“第一个”文件夹,以便对其依赖的其他数据库进行更改,并且数据更改进入“最后”文件夹,因为此时发布者和订阅者已更新其架构。

希望这能为您提供一些处理依赖项和链接服务器等复杂性的方向和想法。

祝你好运。