在线模式 Schema 迁移

https://blueprints.launchpad.net/neutron/+spec/online-schema-migrations

作者:

Mike Bayer <mike.bayer@redhat.com>

本文档讨论了数据库 Schema 迁移的问题,该迁移可以在允许 Neutron 数据库 API 的先前版本和更新版本同时针对该 Schema 运行的情况下进行。这是更大方案的一部分,该方案允许 Neutron 应用程序升级到新版本,而无需在数据库 Schema 迁移期间停机。

为了完全实现此目标,必须解决几个方面

  • 首先,可以应用数据库 Schema 迁移,而不会影响旧版本软件的运行。这些迁移被称为扩展性 (expansive) 迁移,它们仅添加新元素,绝不删除任何元素。一旦旧软件完全被新版本替换,就会单独运行另一系列迁移,称为收缩性 (contract) 迁移;这些迁移会删除不再使用的旧元素。

  • 新版本的软件还必须准备好适应旧版本软件可能仍在运行的事实,这意味着它可能需要同时读取和持久化来自旧 Schema 和新 Schema 结构的数据。

  • 在新的软件引用 Schema 的两个版本的情况下,必须制定一种数据迁移策略。这些迁移可以随着时间的推移作为数据访问代码本身缓慢地将数据移动到新格式的函数来运行,也可以作为单独的脚本或进程来运行。

  • 扩展/收缩、旧版本和新版本软件以及数据迁移之间的移动序列也必须得到充分的编排。前端 RPC 客户端和数据库访问服务通常独立于彼此升级,并且还需要确定何时可以安全地运行“收缩性”迁移。

此蓝图主要关注第一个要点,即组织 Schema 迁移,以便严格的“扩展性”迁移可以与“收缩性”迁移分开运行。上述其他要点需要单独考虑。

请注意,Nova 也采用了解决此处问题的一种方法,也称为“在线 Schema 迁移”。本文档中的规范建立在 Nova 的工作之上,提出了基本相同的概念,但实现方式略有不同,这样既不会与 Neutron 现有的使用 Alembic 迁移脚本的系统发生剧烈变化,也不会放弃使用标识 Schema 的明确已知状态的版本标识符。此外,还提出对 Alembic 进行上游更改,以便 Nova 的“实时”方法和这里的“脚本化”方法可以共享相同的代码库,并针对修订后的 Alembic 自动生成 API,该 API 允许更大的可扩展性。

问题描述

传统上,Neutron 和其他 Openstack 应用程序的数据库迁移涉及用另一个 Schema 替换某个版本的 Schema;表和列将被删除,并添加新的表和列。这种更改必然导致与 Schema 通信的软件也必须同时更改,旧版本在开始迁移之前完全关闭,然后迁移完全离线进行,然后启动新版本。就多个节点 Openstack 部署而言,这意味着所有节点上的整个应用程序必须全局同时完全关闭和升级。离线迁移在操作类型和目标数据库方面也可能非常耗时。

许多关键 Openstack 用户的业务需求是,同时升级所有节点以及在此停机期间运行完整的 Schema 迁移所涉及的停机时间已不再可接受;必须开发一种允许应用程序在迁移进行时保持运行的新方法,特别是对于 Nova、Neutron 和 Cinder 等关键 Openstack 组件。

提议的变更

在本文档中,我们将解决将 Schema 迁移组织成与主要发布版本相关的“扩展”和“收缩”阶段的目标。这些阶段如下

  • 在“扩展”下运行的迁移仅是“添加性”的(例如,仅创建表、列、索引和约束,不删除),并且可以在旧版本的应用程序继续运行时安全地运行。

  • 在“收缩”下运行的迁移是“减法性”的(例如,仅删除表、列、索引和约束,不创建),并且仅在所有节点上运行的软件都独占性地与新版本的 Schema 通信,并且所有数据都已迁移后才运行。

每个阶段涉及的步骤将在 Alembic 迁移脚本中呈现为明确的迁移指令,就像 Neutron 目前的情况一样。唯一的区别是,给定的迁移将分解为各个脚本,用于迁移包含的每个操作阶段。这些脚本将被组装成半独立的谱系,可以单独运行。这些谱系也将按发布版本进行分类,以便迁移谱系可以在发布和阶段级别进行定位,例如“扩展 liberty”、“收缩 M”等。

Alembic 最近添加了对长期分支、根、分支名称和单个文件目录的支持,该方案得到了支持。可以通过创建新的目录结构并使用 Alembic 的更新命令行工具手动将新的迁移文件组装到适当的分支中,从而在概念验证级别实现此工作流程,而无需编写任何新代码。

但是,为了便于使用 Alembic 自动生成,将添加新的功能到 Alembic 的自动生成 API,该 API 将允许创建自定义自动生成行为和文件系统流程。我们将构建一个新的工具,该工具将 Nova 当前的在线 Schema 迁移逻辑调整到这个新的 API,以便用于将迁移分组到“扩展”和“收缩”步骤中的逻辑现在可以将这些指令流式传输到单个迁移文件,并将其定位到上述文件结构中。希望该工具也能继续将迁移指令直接发送到数据库,从而允许 Nova 当前的“实时”方法合并到相同的代码库中。“扩展”/“收缩”工作流程系统的改进和行为契约将适用于“实时”和“脚本化”方法,从而使这两种方法更加互换。

Alembic 迁移

目前,Neutron 使用 Alembic 迁移,这涉及一系列迁移脚本,这些脚本组织在 neutron/db/migration/alembic_migrations/versions 目录中。这些脚本组织成一种反向链接列表结构,其中每个脚本由六字节哈希方案标识,并包含一个变量,该变量将其链接到序列中的前一个哈希值。这种链接结构的原因是,可以在不影响一个现有迁移文件的情况下,将新版本插入到链的中间;通过使用“反向”链接模型,可以将新版本添加到列表的末尾,而不会影响任何现有版本。

Alembic 的最新版本已经过增强,可以重新考虑这种“反向链接列表”结构只是更灵活结构(有向无环图或 DAG)的一种特化。在这种方法中,我们消除了每个迁移脚本只能引用单个祖先(例如,依赖项)的要求,以及仅一个迁移脚本可以引用特定祖先的要求。该结构基本上对版本控制系统中非常熟悉的“分支”和“合并”概念开放。Alembic 现在能够沿着数据库 Schema 中单独跟踪的分支运行升级或降级,这意味着 Schema 的“头”版本实际上是一系列哈希值,每个哈希值代表一个单独的修订流的“头”。分支可以选择性地从完全独立的根修订开始,而这些根修订之间没有依赖关系,并且可以组织到单独的子目录中。分支中的修订还可以引用其他分支中的特定修订作为依赖项,并且可以将分支合并回单个修订流。

扩展和收缩脚本

当前迁移脚本的设计包括它指示 Schema 的特定“版本”,并包括将所有必要更改应用于数据库的指令。例如,如果我们查看脚本 2d2a8a565438_hierarchical_binding.py,我们会看到

# .../alembic_migrations/versions/2d2a8a565438_hierarchical_binding.py

def upgrade():

    # .. inspection code ...

    op.create_table(
        'ml2_port_binding_levels',
        sa.Column('port_id', sa.String(length=36), nullable=False),
        sa.Column('host', sa.String(length=255), nullable=False),
        # ... more columns ...
    )

    for table in port_binding_tables:
        op.execute((
            "INSERT INTO ml2_port_binding_levels "
            "SELECT port_id, host, 0 AS level, driver, segment AS segment_id "
            "FROM %s "
            "WHERE host <> '' "
            "AND driver <> '';"
        ) % table)

    op.drop_constraint(fk_name_dvr[0], 'ml2_dvr_port_bindings', 'foreignkey')
    op.drop_column('ml2_dvr_port_bindings', 'cap_port_filter')
    op.drop_column('ml2_dvr_port_bindings', 'segment')
    op.drop_column('ml2_dvr_port_bindings', 'driver')

    # ... more DROP instructions ...

上述脚本包含“扩展”和“收缩”类别中的指令,以及一些数据迁移。op.create_table 指令是一个“扩展”;可以在旧版本的应用程序继续运行时安全地运行,因为旧代码只是不查找此表。op.drop_constraintop.drop_column 指令是“收缩”指令(删除列比删除约束更重要);至少运行 op.drop_column 指令意味着旧版本的应用程序将失败,因为它将尝试访问不再存在的这些列。

此脚本中的数据迁移是将新行添加到新添加的 ml2_port_binding_levels 表中。数据迁移可能或可能不“安全”地运行在“扩展”或“收缩”阶段,具体取决于数据的性质。预计大多数数据迁移将不再在迁移脚本中运行,而是作为应用程序运行时的模型/API 层的一部分来实现。

请注意,此规范建议但不要求 Neutron 迁移到在应用程序中实现的实时数据迁移,而不是迁移脚本。这部分需要单独考虑,并且超出规范的范围。

根据提议的计划,如果将上述脚本作为新架构的一部分添加,它将表示为两个脚本;一个“扩展”脚本和一个“收缩”脚本

# expansion operations
# .../alembic_migrations/versions/liberty/expand/2bde560fc638_hierarchical_binding.py

def upgrade():

    op.create_table(
        'ml2_port_binding_levels',
        sa.Column('port_id', sa.String(length=36), nullable=False),
        sa.Column('host', sa.String(length=255), nullable=False),
        # ... more columns ...
    )


# contraction operations
# .../alembic_migrations/versions/liberty/contract/4405aedc050e_hierarchical_binding.py

def upgrade():

    op.drop_constraint(fk_name_dvr[0], 'ml2_dvr_port_bindings', 'foreignkey')
    op.drop_column('ml2_dvr_port_bindings', 'cap_port_filter')
    op.drop_column('ml2_dvr_port_bindings', 'segment')
    op.drop_column('ml2_dvr_port_bindings', 'driver')

    # ... more DROP instructions ...

这两个脚本将存在于不同的子目录中,并且也是完全独立的版本流的一部分,如下面的“新迁移布局”部分所述。“扩展”操作在“扩展”脚本中,而“收缩”操作在“收缩”脚本中。

数据迁移被删除,因为预计这些迁移通常不再在 Schema 迁移中发生。但是,该方法仍然与允许在某些情况下手动将“安全”数据迁移放置在扩展或收缩脚本中兼容。

目前,在接受 Neutron 中的实时数据迁移之前,数据迁移规则属于一个脚本子树。

新的迁移布局

借助 Alembic 的新功能,我们可以提出一个新的结构,用于 Neutron 的迁移文件,该结构与“扩展”/“收缩”兼容,同时仍然与 Neutron 中现有的 Alembic 迁移文件流兼容。将布局一个新的目录/分支结构,该结构可以显示所有版本/流

neutron/db/migration/alembic_migrations/...

...versions/
             <existing version>.py
             <existing version>.py
             <existing version>.py
             ...

versions/liberty/
versions/liberty/expand/
                                <expansion script>.py
                                <expansion script>.py
                                ...

versions/liberty/contract/
                                <contract script>.py
                                <contract script>.py
                                ...

versions/M_release/
versions/M_release/expand/
versions/M_release/contract/
... etc

以上,现有的 /versions/ 目录及其所有当前的迁移脚本保持不变;这些版本仍然是使 Neutron 数据库至少达到 Kilo 的脚本。在这些脚本之后,添加了一系列新的子目录,这些子目录按主要的 Openstack 发布版本组织,并且在每个子目录中,将“扩展”和“收缩”系列的脚本分开。

/expand//contract/ 中的脚本系列本身都源自独立的“根”;也就是说,每个目录中底部脚本的“向下”修订为 None

alembic revision 命令支持这些脚本的生成,该命令现在包括将文件放置在特定目录以及给定修订的“向下修订”是什么的选项,包括它可以是“根”,从而允许创建新的分支和根。

跨分支依赖项

为了适应 liberty/expand 中的脚本只能在 versions/ 中的所有旧脚本运行后才能运行的事实,以及 liberty/contract/ 中的单个脚本只能在相应的“扩展”运行后才能运行的事实,将使用 Alembic 的跨分支依赖项功能。从 DAG 的角度来看,这与脚本声明另一个脚本作为依赖项相同,但从 Alembic 的角度来看,该脚本不被视为任何类型的“向下修订”;只有需要先在目标 Schema 中调用一个脚本的版本,然后才能运行当前脚本。它们在 Alembic 脚本中指示为单独的指令

# revision identifiers, used by Alembic.
revision = '2a95102259be'
down_revision = '29f859a13ea'
branch_labels = None
depends_on=('55af2cb1c267', '4fcb78af5a01')

通过建立“depends_on”,特定的脚本指示其他分支中的哪些迁移脚本需要先运行,然后才能运行此脚本。当指示 Alembic 调用此迁移时,它将确保先运行所有依赖脚本。预计自动化脚本创建工具能够自动构建这些指令。

Alembic 分支依赖项在参考文献部分中提到的 Alembic 文档中讨论。

分支标签

Alembic 现在还提供“分支标签”,这意味着除了将我们的迁移文件放在不同的目录中,并跨独立的根版本化和分支化之外,我们还可以将一个或多个“标签”应用于整个分支,然后可以使用 Alembic 的命令行工具来寻址。虽然在 Neutron 中,我们看到一些迁移脚本被故意命名为 juno_release.pykilo_release.py,但我们可以将这些名称应用于整个分支。与分支依赖项类似,这些也作为迁移脚本中的单独指令指示;但是,分支标签只需要存在于分支中的任何单个修订脚本中。通常,分支中的第一个脚本是放置标签的好选择

# revision identifiers, used by Alembic.
revision = '2a95102259be'
down_revision = None  # because we are a "root"
branch_labels = ('liberty_expand', 'release_expand')
depends_on='55af2cb1c267'

因此,在上述情况下,我们将应用诸如 "liberty_expand""liberty_contract" 之类的名称到 liberty/expandliberty/contract 分支,以适当的方式。 这允许运行 Alembic 命令,这些命令引用整个分支,例如

alembic upgrade liberty_expand@head

在上述情况下,将运行直到 liberty_expand 分支的所有迁移(包括旧迁移文件系列的依赖版本,如果尚未运行)。 这将允许 Neutron 的命令套件适应新版本控制方案中的特定目标点,而无需了解特定的修订版本。

如果需要不依赖“release”(发布)的标签,例如指示“运行到当前发布的所有扩展步骤”的分支,我们可以添加额外的“最新发布”标签,这些标签会在建立新发布时移动到新的分支。

新的 Neutron 数据库命令

目前,Neutron 允许使用 neutron-db-manage 脚本运行数据库升级,该脚本链接到 Alembic 自身的 upgrade 命令。 该脚本将被增强,以便利用 Alembic 的新参数形式来运行单个迁移流。 alembic upgrade 命令仍然会被使用,但现在会传递特定于目标操作的适当分支标签,例如 neutron-db-manage expandneutron-db-manage contract

脚本自动化

前几节基本上使整个“扩展”/“收缩”工作流完全可行,以这种方式维护 Neutron 现有的 Alembic 版本控制脚本,而不会产生任何向后不兼容性。 但是,添加新的迁移脚本最初只能通过手动针对工作流的每个部分单独进行来实现。

相反,我们可以增强 Neutron 对 Alembic “autogenerate”(自动生成)的使用,以便单个 revision 自动生成步骤可以根据需要生成多个文件;包含“扩展”和“收缩”指令的迁移将生成两个单独的脚本。

目前,Nova OSM 使用 Alembic autogenerate 来获取有关目标数据库与代码中建立的模型之间的差异信息。 它使用公共方法 compare_metadata() 来实现这一点; compare_metadata() 返回一个简单的“diff”(差异)列表,这些列表引用 schema 对象(如表、列和约束)的更改。 Nova OSM 然后将“operational”(操作)对象(如 AddTableDropColumnAddConstraint 等)与这些“diff”关联。 这些“operational”对象然后链接回 Alembic 的 API,与 Alembic 中相应的“operation”(操作)结构(如 op.create_table()op.drop_column()op.add_constraint())相关联。

Nova 的 OSM 基本上是在使用 Alembic “autogenerate diff”流并将其流式传输到 Alembic “run operations”(运行操作)流。 因此,Alembic 可以提供基础设施,以便可以直接将 autogenerate diff 流作为迁移操作流提供。 Nova 的“live”(实时)OSM 方法和此处提出的“scripted”(脚本化)方法都可以使用此相同的操作流,根据操作类型将其划分为“扩展”和“收缩”流,然后将这些流定向到实时数据库上下文以进行“实时”迁移定向到一系列迁移脚本以进行脚本化迁移。

alembic revision 命令也将被开放,以便插件可以建立由这些操作流的部分生成的开放式 revision 脚本序列。 最终结果是,Neutron 开发人员今天执行的 alembic revision --autogenerate 的单个调用将直接生成单独的“扩展”和“收缩”脚本。

这些新的 API 已经在上游开发分支中进行中,并由单独的 Alembic issue(问题)跟踪(参见参考文献)。 通过紧密链接“实时”迁移和“脚本化”迁移的实现,希望 OSM 中的大部分持续工作可以同时为两种方法做出贡献,从而降低如果放弃其中一种方法或如果两种方法都保持活跃使用而导致改进和工作流程开始发散的风险。

数据模型影响

扩展/收缩工作流本身不会直接影响数据模型。 在线模式迁移的其他方面,即同时支持 schema 的多个版本以及在这些结构之间移动运行时数据,具有巨大的影响;但是,这超出了本文的范围。

REST API 影响

安全影响

其他最终用户影响

最终用户将在执行模式升级时使用修改后的工作流,分别并在适当的时间运行“扩展”和“收缩”步骤。

在备份过程中,没有区别。 数据库始终表示头部修订版本的某个特定子集(所提出的功能与当前状态的唯一区别在于该子集包含多个元素)。

如果/一旦我们在 Neutron 中采用实时数据迁移,它也不会在数据库备份方面发生太大变化。 唯一的重要一点是,同一个逻辑对象现在可以由数据库行的多个版本表示,具体取决于该对象的迁移是否完成。 无论如何,备份仍然可以像往常一样工作。

性能影响

开发人员影响

开发人员应继续使用 alembic revision --autogenerate 来创建新的迁移脚本。 此操作将创建多个脚本,因此,在开发人员需要手动调整这些脚本的程度时,他们将处理多个脚本。 由于数据迁移通常不再包含在这些脚本中,并且由于我们现在可以通过 autogenerate 渲染自定义指令,因此希望 Nova 的“实时”OSM 方法可以自动化的任何内容也可以在“脚本化”方法中完全自动实现。

备选方案

实时迁移最初被提议为 SQLAlchemy-Migrate 的替代品,SQLAlchemy-Migrate 具有非常严格且不可行的编号方案以及依赖于完整表声明和反射的冗长迁移脚本等其他问题。 对于已经使用 Alembic 的项目来说,这些不是问题,因为 Alembic 旨在解决这些问题以及其他问题。

实时迁移还具有不需要生成或提交到源代码存储库中的任何脚本的优势,并且由于实际上无法改变迁移在每次更改时进行的方式,因此开发人员没有机会无意中生成非扩展性迁移或以不当方式编写导致执行数据迁移的语句。 这被认为允许采用“纯声明式”的迁移方法,其中模型代码是指示如何到达新模式的全部内容。

但是,此优势仅在有限的情况下有效。 虽然简单的情况可以通过两种方法轻松自动化,但解决特殊情况、不受支持的功能以及各种后端上的支持和/或可靠性的可变性的问题并未通过“实时”方法解决。 实时迁移仅提供在这些情况下上游迁移系统必须修改以适应目标情况,或者应用程序必须修改为不再需要此类模式迁移。 特殊情况包括对复杂类型(如 ENUM 和精度数字)的更改、涉及 CHECK 约束和某些类型的服务器默认值的操作、使用供应商特定扩展的索引等特殊结构,甚至无法区分添加/删除两个对象的表或列名更改等简单事情。

后端上反射和 autogenerate 功能的可用性和可靠性并不一定一致,Alembic / SQLAlchemy 项目也不能对此做出任何保证。 特别是,不太常见的后端(如 IBM DB2,独立于 SQLAlchemy 或 Alembic 发布)可能根本不支持某些操作,并且目前尚不清楚 autogenerate 和反射在棘手的情况下(如索引、唯一约束和列类型)产生准确结果的程度如何。 Alembic 的 autogenerate 功能并非旨在以实时迁移的方式使用,并且假设它在所有情况下都能在数千个生产系统上产生正确的结果是危险的。

虽然“脚本化”OSM 方法依赖于必须签入和偶尔编辑的显式迁移脚本,但此方法的优势在于,要运行的迁移步骤只需在受控环境中预先生成一次。 针对特殊类型或其他结构的异常迁移再次不是问题,因为可以显式地编写这些脚本。 然后,开发人员可以仔细审查和测试这些步骤,然后发布它们,而无需担心它们在不太常用的供应商的后端或具有异常配置上运行时会执行完全不同的操作。 它们还保持了操作员维护的模式结构不受影响的优势;“实时”方法记录了操作员需要在运行“收缩”后重新创建自己的模式结构。

为了确保脚本化迁移保持适当的扩展性/收缩性,并且在开发人员干预的情况下没有不适当的数据迁移,我们应该要求开发人员按原样使用为他们生成的自动生成的迁移脚本,并且他们不应修改这些脚本,除非是为了支持无法作为自动化迁移支持的操作。 模式迁移应作为 CI 过程的一部分进行测试,包括针对以前的 API 版本测试“在线升级”,并且迁移脚本当然要经过通常的 Gerrit 代码审查过程;识别非扩展性迁移或数据迁移并不困难。

实现

负责人

主要负责人

Mike Bayer

其他贡献者

Ann Kamyshnikova Henry Gessau Ihar Hrachyshka

工作项

  • 更新 neutron-db-manage 以支持多个头部升级。

  • 更新 neutron-db-manage revision 以生成多个脚本。

  • 更新 Alembic 以包含更新流生成。

  • 使用 Alembic autogenerate 功能将操作过滤到子树中。

  • 实现新的 expand/contract neutron-db-manage 命令。

  • 在用户和开发人员文档中记录升级流程的变化。

  • 引入对仅扩展模式升级的测试。

依赖项

Alembic 上游更改,用于 autogenerate 集成方面。

测试

功能测试应包括运行“扩展”迁移以及以前版本的 API 仍然可以完全适用于扩展的迁移。

文档影响

用户文档

扩展/收缩工作流需要记录。

开发人员文档

可以记录 autogenerate 与扩展/收缩工作流的使用方法。

参考资料