简介
修改数据库的结构通常被称为“迁移”到新的模式。虽然用于修改结构本身的操作通常相对简单,但谨慎和计划对于确保被管理的数据保持可访问、一致和语义正确非常重要。
在本指南中,我们将介绍团队可以用来更新其数据库模式和相关代码库的一些策略,并讨论每种方案在多大程度上解决了潜在问题。我们将研究一些通用的应用程序部署模式,以及一些专门为解决特定于数据库的场景而设计的方案。
与数据库相关的部署策略的挑战
在将更改部署到带有数据库的环境时,可能会出现许多潜在问题。其中一些问题与客户端代码库和数据库结构之间的对齐有关,而另一些问题则源于尝试更新现有数据的影响。
成功迁移现有数据
当更改数据库的实际结构时,通常需要修改现有数据以符合新的模式。在某些情况下,这相对简单。例如,如果您需要添加一个与现有数据无关的新表,则无需修改数据库中存储的任何内容。
在其他情况下,例如拆分或合并列,您必须定义自定义数据转换,以指定应如何更改现有数据以填充新上下文。对于处理大量数据的结构,此迁移过程可能需要大量时间。
要使用 Prisma 执行迁移,您可以使用 Prisma Migrate。使用 Prisma Migrate 进行开发 基于声明式 Prisma 模式 生成迁移文件,并将它们应用于您的数据库。
使更改可逆
根据您的部署策略,使更改可逆也可能很困难。一旦数据结构被更新并由实时代码填充,恢复到以前的版本可能会导致数据丢失。
这使得在出现问题时“向前滚动”比“向后回滚”更具吸引力,因为即使行为本身被恢复,数据也可以在更新后的代码中被考虑在内。
测试模式更改
数据库更改涉及的另一个挑战是测试。可能很难理解以何种最佳方式测试模式更改,以捕获边缘情况并确保新数据格式的有效性。真实世界的数据通常会以意想不到的方式冲击约束的边界,因此重要的是要充分掌握潜在值的整个范围,以便编写考虑这些值的测试。
模式更改也可能影响数据库的其他部分,您需要对其进行测试,例如存储过程、触发器和其他组件。这些都需要在每次模式更改时进行测试,以确保它们仍然按预期工作。
性能和可用性影响
最后,模式更改可能很困难,因为它们可能对性能和可用性产生巨大影响。这类问题可能很难在测试环境中模拟,因为在测试环境中,数据集、请求负载和访问模式可能无法反映生产值。
在技术上可能有效的更改可能会带来无法接受的性能成本,这可能很难在暂存环境中推断出来。如果您的部署过程有可能影响可用性,那将是更大的问题。
策略
考虑到上述问题,您如何决定迁移到新模式的最佳方式?根据您的需求、优先级和应用程序环境,有许多方法可以考虑。在某些情况下,结合使用多种策略可以帮助您防范更广泛的潜在问题。
计划内维护:使用停机时间升级模式
实现模式迁移的最古老且最不复杂的策略之一是简单地使您的数据库在迁移期间离线,并将停机时间视为可接受的成本。出于显而易见的原因,这种方法不适用于许多用例,因为正常运行时间和可用性通常是许多组织最高优先级的目标之一。
尽管如此,离线执行模式更改和数据突变仍然是一种有效的策略,在某些情况下可能很有用。如果这是一种选择,那么这种方法有很多优点
- 模式更改可以与客户端代码更改在单个步骤中协调实施。
- 可以检查和测试模式和存储数据的更改,而无需考虑它们对正在运行的进程的性能影响。
- 没有“过渡”期,在此期间,条件代码路径或同一数据结构的多个变体导致复杂性的暂时激增。
- 实施此方法所需的最小基础设施和系统。
- 与其他一些系统相比,大型更改可能更容易合并。
然而,缺点也不容忽视
- 可用性损失可能对 SLA、收入、声誉和其他重要指标产生巨大影响。
- 如果在受限的“维护”窗口内工作,部署期间的意外问题会更具影响力,因为它们直接影响到重新建立可用性的时间。
- 下游服务也将中断,导致任何依赖软件的级联停机。
- 计划内维护往往是“全员出动”的事件,这可能具有挑战性,特别是当它是您部署更改的主要方法时。
在停机期间部署数据库模式更改的过程在实践中相当简单。生成面向用户的响应的应用程序或组件应该理想地在维护块之前提前警告任何计划的停机时间。这可以帮助用户和下游服务就他们自己的需求做出决策。
在维护窗口之前,应设计一个计划或清单,以定义必须执行的确切操作。部署本身应尽可能脚本化和自动化,以减少人为错误并尽可能快地执行所需的操作。所有必需的资产和人员都应在服务关闭之前准备就绪。
在维护窗口期间,应用程序应更新其响应以指示计划内维护正在进行以及服务再次可用的任何估计时间范围。应将经过测试的更改过程应用于生产环境,之后,应检查和测试新的代码和数据模式。
部署完成后,可以重新启动应用程序并开始使用新代码和模式为请求提供服务。
蓝/绿部署
蓝/绿部署是另一种常用于在应用程序上下文中部署新代码的策略。它可以在一定程度上用于数据库模式更改,但确实存在一些明显的缺点。
蓝/绿部署是一种方法,它涉及为您的数据库客户端设置两组相同的基础设施,总共代表运行生产流量所需资源的双倍。
一组基础设施服务于当前的生产流量。另一组基础设施用于设置下一个版本。当一切就绪时,负载均衡器或其他流量导向器路由客户端请求,将流量从第一组切换到第二组以引入新的更改。如果出现问题,可以将流量切换回原始基础设施。如果一切顺利,原始的、现在未使用的基础设施将成为暂存下一个部署的目标。
蓝/绿部署很有吸引力,因为它们允许您在生产就绪的基础设施上部署更改,而不会影响当前的生产环境。通过将部署过程与更改的“发布”分离,开发人员可以在实际运行的基础设施上测试他们的更改,而不会停机。通过切换机制发布新代码和更改使您可以轻松地恢复更改。
虽然蓝/绿部署在许多情况下都很有帮助,但在模式更改中应用它们可能具有挑战性。当仅更改应用程序代码(没有与数据相关的更改)时,恢复有问题的代码就像将流量导向回原始基础设施集一样简单。但是,当数据模式更改时,可能会出现不兼容性。恢复到以前的基础设施可能会导致数据丢失,因为模式结构被删除等。
当将蓝/绿部署与模式更改一起使用时,避免这些问题的一种方法是使用扩展和收缩模式(稍后讨论)来构建您的部署。
功能标志
功能标志是软件开发中的一种设计模式,它允许开发人员在运行时基于应用程序外部设置的值来修改应用程序的控制流。当应用程序遇到某个代码路径时,它会检查众所周知的外部位置的当前值。该值告诉应用程序是否执行某个代码路径,或者在多个路径中选择哪个路径。
功能标志本身不是一种部署策略,而是一种技术,它可以通过允许您将新功能的部署与该功能的激活分离来使其他策略的实现更容易。应用程序可以像最初一样继续运行,直到它在检查标志时看到不同的值。然后,它可以立即切换到使用新功能。
在引入模式更改方面,功能标志尤其有价值,因为您的应用程序可以被设计为与模式的多个迭代进行交互。功能标志可以设置为指示当前部署的模式版本,从而选择已设计为与之交互的代码。
使用功能标志的缺点通常很小,但应予以考虑。如果尚无可用的适当的键/值存储,则可能需要额外的基础设施来存储您的标志值。此外,功能标志可能会在其使用期间增加代码的复杂性。一旦功能标志过时,清理和简化代码路径可以帮助将额外的条件逻辑保持在最低限度。
金丝雀发布
可以与其他方法结合使用的另一种策略是金丝雀发布。金丝雀发布是一种部署策略,它简单地意味着更改首先在一个或少数几个客户端上引入,然后再部署到您的其余基础设施。
这使您可以尽早发现您在之前的测试中没有看到的问题。运行新代码的客户端子集充当代码在其余系统上运行状况的指标,并允许您在对更改的稳定性和功能性获得信心时逐步推出其他系统。
在数据库模式更改方面,金丝雀发布允许您通过降低引入更改的风险来验证模式更改及其相关的客户端代码是否适合生产环境。不是影响所有客户端,而是使用一小部分来评估更改。这使您有机会在更改产生意外后果时尽早回滚,并使用一部分真实世界的生产流量来查看性能。金丝雀发布帮助您最大限度地减少代码更改的影响,这可以帮助您的模式更改更顺利地进行。
扩展和收缩模式
引入模式更改的最佳方法可能是扩展和收缩模式。扩展和收缩模式允许您在原始模式旁边引入模式更改,将旧数据迁移到新结构,并在计划阶段逐步将生产流量移动到新结构。它可以与我们之前讨论的许多策略和技术结合使用,以在发生问题时引入具有多层安全性的更改。
扩展和收缩模式可以通过执行以下步骤来实现
- 在原始模式旁边设计和部署所需的模式。
- 修改客户端代码以同时写入两个模式。
- 将现有数据从原始模式迁移到新模式,并在必要时对其进行修改以符合新结构。
- 测试新模式,以确保其功能正确且数据已正确传输。
- 修改客户端代码以开始从新模式读取数据。
- 修改客户端代码以停止写入原始模式。
- 删除原始模式。
通过执行上述阶段,您的模式更改会分布在更长的时间内。然而,这使您可以逐步更改生产环境中的应用程序代码以处理模式中的更改。
此策略的主要优势之一是将读取与写入新数据模式分离。这种方法意味着应用程序在新模式影响任何面向客户端的响应之前很久就使用了新模式,并且客户端代码积极参与将新数据写入模式,而旧数据可以在后台回填。
您可以在关于使用扩展和收缩模式进行模式更改的指南中阅读有关此方法的更多详细信息。
使用扩展和收缩以及功能标志的示例
通常,部署模式更改的最佳方法是结合多种技术。为了演练一个与模式更改相关的示例,假设您正在尝试引入一个模式更改,该更改修改了 names
表,以将原始 first_name
和 last_name
列合并为单个 full_name
列。
您已按照扩展和收缩模式的第一个阶段概述的那样,将您的新数据库模式与现有模式一起部署。现在,您有两个结构基本相同的表:names
,它是包含所有当前数据的原始结构,以及 new_names
,它是表示所需结构的新空表。
接下来,您要修改您的应用程序代码以写入您的新结构以及您的旧结构。为了实现这一点,您向您的应用程序引入了一个新的逻辑,该逻辑检查您的组织的 Redis 实例中 DATABASE_NAMES_TABLE_WRITE
的值。该值是要在修改 names
表时写入的表列表。
您也知道最终您将希望将读取从旧模式过渡到新模式。为了解决这个问题,您还包含一个门控,该门控检查 Redis 中 DATABASE_NAMES_TABLE_READ
变量的值,以确定要从哪个结构读取。
您在 Redis 实例中设置值以使用您的原始数据模式
rpush DATABASE_NAMES_TABLE_WRITE namesset 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) names2) new_names
如果您的功能标志代码使用这些值来确定写入位置,它现在将写入两个表。
现在您的应用程序正在写入两个位置,您开始在后台迁移数据。由于这是一个相当简单的更改,您可以通过从 names
表中读取 first_name
和 last_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
以再次完成模式更改。
结论
虽然您可以使用许多部署和迁移策略来实现模式更改,但最简单的方法通常存在一些明显的缺点。通过了解不同迁移策略的效果并了解您自己的组织需求和专业知识,您可以开发一个迁移过程,该过程将最大限度地减少停机时间并允许您安全地测试更改。
要使用 Prisma Client 执行迁移,请使用 Prisma Migrate 工具。Prisma Migrate 分析您的模式文件,生成迁移文件,并将它们应用于目标数据库。