如何在生产中自动滚动更新

在作战行动中推出新版本始终是一件令人紧张的事情。 特别是如果该过程涉及许多手动操作。 人为因素是一件可怕的事情。 “自动执行此过程会很好” –这个想法与整个IT世界一样古老。 对此有一个术语-连续部署。 是的,麻烦是,没有独特的方法来配置此连续部署。 这个过程很大程度上与项目及其环境的技术堆栈相关。

在本文中,我想分享在设置自动系统更新而不中断其在特定技术环境下的操作的实践经验,即:ASP.NET MVC +代码优先模式下的Azure SQL +实体框架Web应用程序,将该应用程序作为App Service部署在Azure中以及组装和部署是通过Azure DevOps(以前称为Visual Studio Team Services)完成的。



乍一看,一切都很简单,Azure App Service具有部署插槽的概念-在此处下载新版本并将其打开。 但是,如果项目基于非关系型DBMS(在其中没有严格的数据模式),这将很简单。 在这种情况下,可以-只是新版本可以吸引点击量和瞧瞧。 但是,使用关系型DBMS,一切都会变得更加复杂。

阻碍我们为技术堆栈实施持续部署的主要因素如下:

  • 应用程序的旧版本无法与新的数据库结构一起使用
  • 更新数据库结构可能会花费大量时间,并且并非总是可以通过自动迁移机制使用应用程序本身来进行更新。

我会解释。 假设您在并行插槽或备份数据中心中部署了新版本,并开始了迁移应用程序。 假设我们进行了三项迁移,并且恐怖地进行了两次迁移,而第三次下降了。 目前,工作的服务器什么都不会发生,实体框架不会检查每个请求的版本,但是您可能无法快速解决问题。 这时,应用程序上的负载可能会增加,并且平台将为您启动该应用程序的其他实例,由于数据库结构已更改,因此它自然不会启动。 很大一部分用户将开始收到错误。 因此,自动应用迁移的风险很大。



关于第二点,您的迁移可能包含某种类型的命令,其执行时间超过30秒,并且标准过程将超时。 好吧,除了这些要点之外,我个人不喜欢在自动迁移过程中不得不将部分基础架构升级到新版本这一事实。 而且,如果对于在Azure中具有插槽的模式而言,这种情况并不那么可怕,那么对于具有备份数据中心的模式,则您的基础结构中有一部分具有已知无法运行的应用程序。 都是危险的,它会在最不适当的时刻射击。

怎么办


让我们从最困难的地方开始-从数据库开始。 因此,最好以某种方式自动更新数据库结构,以便旧版本的应用程序继续工作。 此外,考虑到存在这样的更新,即可以在相当长的时间内执行单独的命令的事实,将是一个很好的选择,这意味着我们需要不使用内置机制而是通过执行单独的SQL脚本来更新数据库。 问:如何准备呢? 您可以将此过程手册。 如果您在团队中具有单独的发布经理角色,则可以强制其在Visual Studio中执行命令:

update-database -script 

她将生成一个脚本,此人将把该脚本放在特定的项目文件夹中。 但是您必须承认,这仍然很不方便,首先是人为因素,其次,如果发行版之间存在多个迁移,则会带来不必要的困难。 或者由于某种原因,在目标系统上跳过了一个发行版。 我们必须组成某种复杂的花园,并跟踪哪些迁移已经存在以及哪些迁移需要启动。 这很困难,最重要的是,这是与迁移机制相同的自行车。

将脚本生成和执行过程构建到发布计算过程中是正确的。 为了生成迁移脚本,您可以使用Entity Framework附带的migration.exe实用程序。 我提请您注意以下事实:您需要Entity Framework版本6.2或更高版本,因为脚本生成选项仅在2017年4月出现在此实用程序中。 该实用程序调用如下所示:

 migrate.exe Context.dll /connectionString="Data Source=localhost;Initial Catalog=myDB;User Id=sa;Password=myPassword;" /connectionProviderName="System.Data.SqlClient" /sc /startUpDirectory="c:\projects\MyProject\bin\Release" /verbose 

程序集的名称将指示您的上下文所处的位置,目标数据库的连接字符串,提供程序,以及非常重要的是,开始目录,其中包含具有上下文的程序集和实体框架程序集。 不要尝试使用工作目录的名称,更简单些。 我们偶然发现了一个事实,即migrate.exe无法读取目录,该目录的名称中包含空格和非字母字符。

在这里有必要做一个重要的题外话。 事实是,执行上述命令后,将生成单个SQL脚本,其中包含需要应用于目标数据库的所有迁移的所有命令。 对于Microsoft SQL Server,这不是很好。 事实是服务器在没有GO分隔符的情况下将命令作为单个程序包执行,并且某些操作无法在单个程序包中一起执行。

例如,在某些情况下,将字段添加到表中并立即使用新字段在该表上创建索引是行不通的。 但这还不够,某些命令在运行脚本时需要某些环境设置。 通过SQL Server Management Studio连接到SQL Server时,默认情况下会启用这些设置,但是当通过控制台实用程序SQLCMD执行脚本时,必须手动设置它们。 要考虑到所有这些,您将必须修改使用文件生成迁移脚本的过程。 为此,请在日期上下文旁边创建一个附加类,该类可以完成您需要的所有操作:

  public class MigrationScriptBuilder : SqlServerMigrationSqlGenerator { public override IEnumerable<MigrationStatement> Generate(IEnumerable<MigrationOperation> migrationOperations, string providerManifestToken) { var statements = base.Generate(migrationOperations, providerManifestToken); var result = new List<MigrationStatement>(); result.Add(new MigrationStatement { Sql = "SET QUOTED_IDENTIFIER ON;" }); foreach (var item in statements) { item.BatchTerminator = "GO"; result.Add(item); } return result; } } 

为了使实体框架可以使用它,请将其注册在Configuration类中,该类通常位于Migrations文件夹中:

  public Configuration() { SetSqlGenerator("System.Data.SqlClient", new MigrationScriptBuilder()); …. } 

之后,生成的迁移脚本将在每个语句之间包含GO,并且在文件的开头将包含SET QUOTED_IDENTIFIER ON;

Hooray,准备工作完成,它仍然是配置过程本身。 通常,作为Azure DevOps(VSTS / TFS)发布过程的一部分,这已经非常简单。 我们需要创建一个PowerShell脚本,如下所示:

 param ( [string] [Parameter(Mandatory=$true)] $dbserver, [string] [Parameter(Mandatory=$true)] $dbname, [string] [Parameter(Mandatory=$true)] $dbserverlogin, [string] [Parameter(Mandatory=$true)] $dbserverpassword, [string] [Parameter(Mandatory=$true)] $rootPath, [string] [Parameter(Mandatory=$true)] $buildAliasName, [string] [Parameter(Mandatory=$true)] $contextFilesLocation, ) Write-Host "Generating migration script..." $fullpath="$rootPath\$buildAliasName\$contextFilesLocation" Write-Host $fullpath & "$fullpath\migrate.exe" Context.dll /connectionProviderName="System.Data.SqlClient" /connectionString="Server=tcp:$dbserver.database.windows.net,1433;Initial Catalog=$dbname;Persist Security Info=False;User ID=$dbserverlogin;Password=$dbserverpassword;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" /startUpDirectory=$fullpath /verbose /scriptFile=1.SQL Write-Host "Running migration script..." & "SQLCMD" -S "$dbserver.database.windows.net" -U $dbserverlogin@$dbserver -P $dbserverpassword -d $dbname -i 1.SQL Write-Host "====Finished with migration script====" 

并将PowerShell脚本执行单元添加到发布计算过程中。 该块及其设置可能如下所示:



PowerShell安装程序如下所示:



重要的是不要忘记从<YourProject> /packages/EntityFramework.6.2.0/tools/文件夹中将migration.exe文件添加到项目中,并将“始终复制”属性设置为该属性,以便在构建项目时将此实用程序复制到输出目录,并可以在其中访问它。 Azure DevOps版本。

细微差别 。 如果您的项目在部署到Azure App Service时还使用WebJob,则将Migrate.exe添加到项目中是不安全的。 我们面临这样一个事实,在发布WebJob的文件夹中,Azure平台会愚蠢地启动遇到的第一个exe文件。 而且,如果您的WebJob按字母顺序花费了稍后的migrate.exe(并且确实如此),那么它将尝试运行migration.exe而不是您的项目!

因此,我们了解了如何在发布过程中通过生成脚本来更新数据库版本,简单的事情是:关闭迁移版本检查,以便在脚本执行过程中出现任何故障时,我们代码的旧版本继续有效。 我认为不必说您的迁移应该是非破坏性的。 即 对数据库结构的更改不应干扰前一版本的性能,但要好于前两个版本。 要禁用验证,只需将以下块添加到Web.config中:

  <entityFramework> <contexts> <context type="<full namespace for your DataContext class>, MyAssembly" disableDatabaseInitialization="true"/> </contexts> </entityFramework> 

其中full namespace for your DataContext class的完整名称空间是DbContext后代的完整名称空间,而MyAssembly是上下文所在程序集的名称。

最后,我们非常希望在将用户切换到新版本之前确保应用程序正在预热。 为此,请在web.config中添加一个特殊块,其中包含应用程序在初始化过程中自动退出的链接:

  <system.webServer> <applicationInitialization doAppInitAfterRestart="true"> <add initializationPage="/" hostName="" /> </applicationInitialization> </system.webServer> 

您可以简单地通过添加/>添加几个链接。有人认为,在Azure中,切换插槽时,平台等待应用程序初始化,然后才将流量切换到新版本。

但是.NET Core上的项目呢?


一切都简单得多,同时又有所不同。 可以使用常规工具生成迁移脚本,但它不是基于完成的程序集,而是基于项目文件。 因此,脚本应作为项目组装过程的一部分形成,并应作为组装工件包含在内。 在这种情况下,脚本将从开始就包含所有迁移的所有命令。 由于脚本是幂等的,因此没有问题。 它可以重复地应用于目标库,而不会产生任何后果。 这还有另一个有用的结果:我们不需要修改脚本生成过程就可以将命令分成多个包,为此已经完成了所有工作。

好吧,具体地说,该过程的步骤如下所示。 在构建过程中,添加任务:



我们将其配置为生成带有迁移的文件:



不要忘记将脚本添加到PowerShell项目中,该脚本将执行迁移(如上所述)和迁移文件本身。 结果,在构建项目之后,工件可能看起来像这样(除了带有程序集的实际归档文件之外,还有其他带有迁移的PS脚本和SQL脚本):



它仅保留在适当的Release步骤中,以与上述相同的方式配置此PowerShell脚本的执行。

关于作者


Pavel Kutakov是云技术方面的专家,是各个业务领域的软件系统开发人员和架构师-从在美国到巴布亚新几内亚的全球范围内运营的银行IP,再到为国家彩票运营商提供的云解决方案。

Source: https://habr.com/ru/post/zh-CN436670/


All Articles