分享至

简介

修改数据库结构通常被称为“迁移”到新模式。虽然用于修改结构本身的操作通常相对简单,但细心和规划对于确保所管理的数据保持可访问、一致和语义正确至关重要。

在本指南中,我们将介绍团队可以用来更新数据库模式及相关代码库的一些策略,并讨论每种方案如何有效地解决潜在问题。我们将探讨一些通用的应用程序部署模式,以及一些专门为解决数据库特定场景而设计的选项。

在部署数据库环境中的更改时,可能会出现许多潜在问题。其中一些问题与客户端代码库和数据库结构之间的一致性有关,另一些则源于尝试更新现有数据的影响。

成功迁移现有数据

更改数据库的实际结构时,现有数据通常需要修改以符合新模式。在某些情况下,这相对简单。例如,如果您需要添加一个与现有数据无关的新表,则数据库中存储的任何内容都不需要修改。

在其他情况下,例如拆分或组合,您必须定义自定义数据转换,以指定如何修改现有数据来填充新上下文。对于处理大量数据的结构,此迁移过程可能需要大量时间。

使更改可逆

根据您的部署策略,使更改可逆也可能很困难。一旦数据结构被更新并由实时代码填充,回滚到以前的版本可能会导致数据丢失。

这使得在出现问题时“向前滚动”比“回滚”更具吸引力,因为即使行为本身被恢复,更新后的代码也能处理数据。

测试模式更改

数据库更改涉及的另一个挑战是测试。很难理解如何最好地测试模式更改,以捕获边缘情况并确保新数据格式的有效性。实际数据通常以意想不到的方式突破约束的边界,因此,全面了解所有潜在值并编写能考虑到这些值的测试非常重要。

模式更改还会影响数据库的其他部分,例如存储过程、触发器和其他组件,您会希望对其进行测试。这些都需要在每次模式更改时进行测试,以确保它们仍按预期工作。

性能和可用性影响

最后,模式更改可能很困难,因为它们会对性能和可用性产生巨大影响。这类问题在测试环境中很难模拟,因为测试环境中的数据集、请求负载和访问模式可能无法反映生产环境的值。

技术上可能有效的更改可能会带来不可接受的性能成本,这在预生产环境中可能难以推断。如果您的部署过程有可能影响可用性,那将是一个更大的问题。

策略

考虑到上述问题,您如何决定迁移到新模式的最佳方式?有多种方法需要考虑,具体取决于您的需求、优先级和应用程序环境。在某些情况下,结合使用多种策略可以帮助您应对更广泛的潜在问题。

计划内维护:在停机期间升级模式

实现模式迁移最古老、最不复杂的策略之一是,在迁移期间将数据库下线,并将停机时间视为可接受的成本。出于显而易见的原因,这种方法不适用于许多用例,因为正常运行时间和可用性通常是许多组织的首要目标。

尽管如此,离线执行模式更改和数据修改仍然是一种有效策略,在某些情况下很有用。如果这是一个选项,这种方法有许多优点:

  • 模式更改可以与客户端代码更改协调一致,一步到位地实现。
  • 可以检查和测试模式和存储数据的更改,而无需考虑它们对运行过程的性能影响。
  • 没有“过渡”期,其中条件代码路径或相同数据结构的不同变体导致复杂性暂时激增。
  • 实现此方法所需的设施和系统最少。
  • 相比其他一些系统,大型更改可能更容易整合。

然而,缺点不容忽视:

  • 可用性损失可能对 SLA、收入、声誉和其他重要指标产生巨大影响。
  • 在受限的“维护”窗口内工作时,部署期间的意外问题影响更大,因为它们直接影响到重新建立可用性所需的时间。
  • 下游服务也将中断,导致任何依赖软件的级联停机。
  • 计划性维护通常是“全体总动员”事件,这可能极具挑战性,尤其是在其作为您部署更改的主要方法时。

在停机期间部署数据库模式更改的过程在实践中相当简单。理想情况下,生成面向用户响应的应用程序或组件应在维护时段开始前充分提前警告任何计划的停机时间。这可以帮助用户和下游服务根据自身需求做出决策。

在维护窗口之前,应设计一份计划或清单,以定义必须执行的确切操作。部署本身应尽可能地脚本化和自动化,以减少人为错误并尽快执行所需操作。在服务下线之前,所有必要的资产和人员都应到位。

在维护窗口期间,应用程序应更新其响应,以指示正在进行计划维护以及服务恢复可用的预计时间。应将经过测试的更改程序应用于生产环境,之后,应检查并测试新代码和数据模式。

部署完成后,应用程序可以重新启动并开始使用新代码和模式处理请求。

蓝/绿部署

蓝/绿部署是另一种常用于应用程序环境中部署新代码的策略。它在一定程度上可以用于数据库模式更改,但确实存在一些显著的缺点。

蓝/绿部署是一种方法,它涉及到为您的数据库客户端设置两套相同的基础设施,这两套基础设施合计代表运行生产流量所需资源的两倍。

一套基础设施服务当前的生产流量。另一套基础设施用于设置下一个版本。当一切准备就绪时,负载均衡器或其他流量导向器将客户端请求从第一套切换到第二套,以引入新的更改。如果出现问题,流量可以切换回原来的基础设施。如果一切顺利,原来现在未使用的基础设施将成为下一个部署的预备目标。

蓝/绿部署很有吸引力,因为它们允许您将更改部署到生产就绪的基础设施上,而不会影响当前的生产环境。通过将部署过程与更改的“发布”解耦,开发人员可以在实际运行的基础设施上测试其更改,而无需停机。通过切换机制发布新代码和更改可以让您轻松回滚更改。

虽然蓝/绿部署在许多场景中都很有帮助,但将其应用于模式更改可能具有挑战性。当仅更改应用程序代码(不涉及数据相关更改)时,回滚有问题的代码就像将流量导回原始基础设施集一样简单。然而,当数据模式发生更改时,可能会出现不兼容。回滚到以前的基础设施可能会导致数据丢失,因为模式结构被删除等。

在使用蓝/绿部署进行模式更改时,避免这些问题的一种方法是使用扩展和收缩模式(稍后讨论)来构建您的部署。

功能标志

功能标志是软件开发中的一种设计模式,允许开发人员在运行时根据应用程序外部设置的值来修改应用程序的控制流。当执行到某个代码路径时,应用程序会检查一个已知的外部位置的当前值。该值告诉应用程序是否执行某个代码路径或在多个路径中选择哪个路径。

功能标志本身并非一种部署策略,而是一种可以使其他策略更容易实现的技巧,它允许您将新功能的部署与该功能的激活解耦。应用程序可以继续像最初那样运行,直到它在检查标志时看到不同的值。然后,它可以立即切换以使用新功能。

在引入模式更改方面,功能标志特别有价值,因为您的应用程序可以设计为与模式的多次迭代进行交互。可以设置功能标志来指示当前部署的模式版本,从而选择设计用于与该模式交互的代码。

使用功能标志的缺点通常很小,但应予以考虑。如果尚无合适的键/值存储,可能需要额外的基础设施来存储您的标志值。此外,功能标志可能会在其使用期间增加代码的复杂性。一旦功能标志过时,清理和简化代码路径有助于将额外的条件逻辑保持在最低限度。

金丝雀发布

另一种可以与其他方法结合使用的策略是金丝雀发布。金丝雀发布是一种部署策略,它简单地意味着在部署到其余基础设施之前,首先在单个或少数客户端上引入更改。

这使您能够及早发现之前测试中未发现的问题。运行新代码的客户端子集可作为衡量代码在其余系统上运行情况的指标,并允许您在对更改的稳定性与功能性建立信心后,逐步将更多系统切换到新代码。

在数据库模式更改方面,金丝雀发布通过降低引入更改的风险,使您能够验证模式更改及其相关客户端代码是否适用于生产环境。它不是影响所有客户端,而是使用一小部分客户端来评估更改。这为您提供了一个机会,如果更改产生不可预见的后果,可以尽早回滚,并可以使用一部分实际生产流量来查看性能。金丝雀发布有助于最大限度地减少代码更改的影响,从而有助于您的模式更改更顺利地进行。

扩展和收缩模式

也许引入模式更改的最佳方法是扩展和收缩模式。扩展和收缩模式允许您在保留原始模式的同时引入模式更改,将旧数据迁移到新结构,并通过一系列计划阶段逐步将生产流量转移到新结构。它可以与我们之前讨论的许多策略和技术结合使用,以在出现问题时提供多层安全保障来引入更改。

扩展和收缩模式可以通过以下步骤实现:

  1. 设计并部署所需的模式,使其与原始模式并存。
  2. 修改客户端代码,使其同时向两个模式写入更改。
  3. 将现有数据从原始模式迁移到新模式,并根据需要进行修改以符合新结构。
  4. 测试新模式,确保其功能正确且数据已正确传输。
  5. 修改客户端代码,使其开始从新模式读取数据。
  6. 修改客户端代码,使其停止向原始模式写入数据。
  7. 移除原始模式。

通过执行上述阶段,您的模式更改将分散在更长的时间内。然而,这允许您在生产环境中逐步更改应用程序代码,以适应模式的更改。

该策略的主要优势之一是将对新数据模式的读写操作解耦。这种方法意味着应用程序在新模式影响任何面向客户端的响应之前就已经很好地使用了新模式,并且客户端代码积极参与将新数据写入模式,而旧数据可以在后台进行回溯。

您可以在使用扩展和收缩模式进行模式更改指南中阅读有关此方法的更多详细信息。

使用扩展和收缩模式以及功能标志的示例

通常,部署模式更改的最佳方法是结合多种技术。为了举一个与模式更改相关的例子,假设您正尝试引入一个模式更改,该更改将修改 names 表,以将原始的 first_namelast_name 列合并为单个 full_name 列。

您已按照扩展和收缩模式的第一阶段所述,将新的数据库模式与现有模式一同部署。现在,您拥有两个结构大致相同的表:names,它是包含所有当前数据的原始结构;以及 new_names,它是代表所需结构的新建空表。

接下来,您希望修改应用程序代码,使其既写入新结构,也写入旧结构。为实现此目的,您在应用程序中引入了一个新逻辑,该逻辑检查组织 Redis 实例中 DATABASE_NAMES_TABLE_WRITE 的值。该值是一个列表,列出了修改 names 表时要写入的表。

您也知道,最终您会希望将读取操作从旧模式切换到新模式。为了解决这个问题,您还包含了一个检查 Redis 中 DATABASE_NAMES_TABLE_READ 变量值的“关卡”,以确定从哪个结构读取。

您在 Redis 实例中设置值以使用原始数据模式:

rpush DATABASE_NAMES_TABLE_WRITE names
set DATABASE_NAMES_TABLE_READ names

接下来,您将新代码部署到客户端,该代码包含 Redis 检查作为确定读写位置的“关卡”。

当您希望使客户端同时写入两种数据结构时(如扩展和收缩模式的第 2 步所示),您可以更新 Redis 中的 DATABASE_NAMES_TABLE_WRITE 列表以包含新表名:

rpush DATABASE_NAMES_TABLE_WRITE new_names

现在,DATABASE_NAMES_TABLE_WRITE 列表将包含两个值:

lrange DATABASE_NAMES_TABLE_WRITE 0 -1
1) names
2) new_names

如果您的功能标志代码使用这些值来确定写入位置,它现在将同时写入两个表。

现在您的应用程序正在向两个位置写入数据,您可以开始在后台迁移数据。由于这是一个相当直接的更改,您可以通过从 names 表读取 first_namelast_name 值,用空格连接它们,并将结果字符串写入 new_names 表的 full_name 列来填充新模式。

现在新表已填充完毕并包含所有当前数据。此时您可以执行任何进一步的测试,以确保其正常运行并可以替代原始表。

当您准备好将读取操作从旧结构过渡到新结构时,您可以用新的表名覆盖 DATABASE_NAMES_TABLE_READ 变量:

set DATABASE_NAMES_TABLE_READ new_names

客户端应用程序下次在执行读取操作前检查该值时,它将接收到新值并从新结构中读取数据。

一旦您确认一切正常,您可以更新 DATABASE_NAMES_TABLE_WRITE 列表以移除原始表的名称:

lrem DATABASE_NAMES_TABLE_WRITE 0 "names"
(integer) 1

客户端的下一次写入操作将触发查找并接收仅包含 new_names 的新值。

您现在可以安全地删除原始的 names 表,并移除要求客户端检查读写位置的功能标志脚手架。在此过程中,您可能希望将 new_names 表重新命名为 names,以完成模式更改。

结论

尽管有许多部署和迁移策略可用于实现模式更改,但最简单的方法通常存在一些显著的缺点。通过了解不同迁移策略的影响,并清楚您自己的组织需求和专业知识,您可以开发一个迁移过程,最大限度地减少停机时间并允许您安全地测试更改。

© . All rights reserved.