滚动升级的数据库策略¶
https://blueprints.launchpad.net/glance/+spec/database-strategy-for-rolling-upgrades
本规范概述了Glance的数据库修改策略,该策略将促进零停机滚动升级,并使Glance能够断言assert:supports-zero-downtime-upgrade标签。
问题描述¶
为了将assert:supports-zero-downtime-upgrade标签[GVV2]应用于Glance,必须首先断言assert:supports-rolling-upgrade标签[GVV1]。它指出:
项目有一个明确的计划,允许运营商将新代码推广到服务的子集,从而无需同时重启所有服务以部署新代码。
为了断言assert:supports-zero-downtime-upgrade标签,Glance必须在升级期间完全消除控制平面的API停机。Glance在这方面的一个关键问题是如何在发布N-1代码仍在运行时处理发布N所需的数据库更改。下面我们将概述实现此目标的策略。
注意
在下文中,我们假设对滚动升级感兴趣的运营商将与我们合作,也就是说,将使用支持在线模式更改的最新版底层DBMS,以允许尽可能多的并发。
提议的变更¶
我们提出了一种扩展和收缩策略,以在单一版本范围内实现数据库更改,从而完成滚动升级。一些OpenStack服务(如Cinder)在解决此问题时选择通过一系列版本进行数据库更改,但我们相信,鉴于Glance的结构和典型使用模式,数据库更改可以在单一版本中完成并最终确定。这种方法对于一个开源项目来说是更可取的,因为在一个周期中,参与者的构成可能会发生很大变化。
我们首先概述升级策略,然后提供一个详细示例,说明它如何在Ocata版本中发生的更改中发挥作用。
概述¶
下图描述了OpenStack服务的典型升级。旧服务完全关闭(主要在维护窗口期间),部署新代码,最后启动新服务。显然,这涉及用户的停机时间。为了最小化/消除停机时间,服务可以以滚动方式升级,即每次升级少量服务。这导致旧服务(例如N-1)和新服务(例如N)必须共存一段时间。在没有数据库更改的直接升级中,服务可以从一开始就共存,因为它们都依赖相同的模式。
| | | | | | | | | -----------------------+ | | | +--------------------- | | Deploy N | | N-1 | | (upgrade code | | N | | and/or config | | -----------------------+ | and migrate db) | +--------------------- | | | Stop N-1 | Start N Services | Services | | | | | | | | | | | | | | | | | | <----------------> Downtime <---------------->
然而,在存在数据库更改的情况下,服务还无法共存。主要原因是目前我们进行数据库更改/迁移的方式。Glance中的典型迁移是一个原子更改,包括模式和相应的数据迁移。模式迁移执行对模式的必要添加/删除/修改,数据迁移执行对数据的相应更改。这种方法有时,取决于模式更改的性质,是向后不兼容的。也就是说,旧服务可能无法在新模式下运行。这实质上限制了旧服务和新服务共存的能力,因此禁止了滚动升级。
为了实现滚动升级,数据库迁移需要以旧服务和新服务可以在一段时间内共存的方式进行。一种众所周知的策略是以扩展 和 收缩样式重新构想数据库更改,而不是一次原子更改。通过扩展和收缩样式,我们通过两个不同的步骤实现所需的模式更改
扩展:在
扩展步骤中,我们只进行 新增更改,这些更改是新服务所必需的。这使模式保持不变,以便旧服务可以与新服务一起运行。属于此类别的典型模式更改是添加列和表。这种只进行新增更改的策略有一个例外,那就是可能需要删除某些约束以允许数据库触发器(下文讨论)工作。
收缩:所有其他更改,即
非新增更改,都归类到收缩步骤。此步骤中进行删除列、表和/或约束等更改。此外,如果在扩展步骤中删除了任何约束,它们将在收缩阶段恢复。在扩展阶段安装的任何数据库触发器也将在此时删除。
这种分解使我们能够首先执行所需的最小更改(同时保持与旧服务的模式兼容性),并将其他更改推迟到以后。因此,我们总是首先扩展数据库以开始滚动升级,而旧服务仍在运行。一旦数据库扩展,就会创建新列和新表。然而,它们将是空的。此时,我们应该开始将数据迁移到新列。但是,同时,保持新旧列同步也很重要。对旧列的任何写入都必须同步到新列。反之亦然(尽管我们尚未写入新列,但当新服务启动并开始写入新列时,我们必须在服务共存时保持旧列同步)。我们使用数据库触发器来保持列同步。
我们在数据库扩展期间添加触发器以及附加更改。此时,我们开始将数据迁移到新列。但是,由于旧服务此时处于活动状态,我们分小批次迁移数据,以避免数据库上的过度负载,从而避免对旧服务的任何中断。这些迁移可以安排在低流量时段运行,以最大限度地减少对旧服务的影响。一旦数据迁移完成,新列被填充并准备就绪,我们开始部署新服务。
我们通过将一些节点从轮换中取出,等待它们清空连接,升级服务并将节点重新投入轮换,从而小批量部署服务。在这一时期,旧服务和新服务共存。当新服务启动时,它们开始从新列读取和写入。写入新列的任何数据都会同步到旧列(通过数据库扩展期间添加的触发器),并可供旧服务使用。一旦所有旧服务都升级完毕,现在可以安全地收缩数据库。这确保我们达到所需的数据库模式状态。我们还在数据库收缩期间删除数据库触发器,因为旧列将不再存在,只有新列将被使用。
---------------------------------------+ | N-1 | | ---------------------------------------+ | | | | | | | | Finish | | | | | Data | Expand | Migrations| +---------------------------- Database | | | | & | | | | N Add | | | | Triggers| | | +---------------------------- | | | | | | | Start N | | | Start | Deploy | Contract | Data | | | Database | Migrations | | | & | | | | | Drop | | | | | Triggers | | | | Finish N | | | | | Deploy | | | | | | | | | | | | |
总结一下,如上图所示,我们将数据库迁移分为模式迁移和数据迁移。模式迁移可以是添加性的或收缩性的,或者两者兼有。添加性模式迁移在升级开始之前运行,以准备数据库以供新服务使用,同时它仍然可供旧服务使用。(此阶段也称为“数据库扩展”。)在数据库扩展期间,我们还会在新旧列上添加触发器以保持它们同步。一旦数据库扩展,我们开始以小批量的方式将数据迁移到新列。当数据迁移完成时,我们以滚动方式升级旧服务。一旦所有旧服务都升级完毕,我们对数据库运行收缩性迁移。(此阶段也称为“数据库收缩”。)触发器也在数据库收缩期间删除。
除了上面给出的过程描述外,以下是关于升级工作方式的一些限制:
典型的升级只有在执行完一个版本的整个扩展-迁移-收缩周期后才算完成。我们不建议在N-2到N-1升级进行中时支持N-1到N的升级。
不支持“跳级”版本(即,允许直接从N-2升级到N,跳过N-1)。
在一个版本中,可能会有多个功能(由不同的开发人员独立开发)需要某种数据库修改。我们在此规范中提出的是,对于每个版本,从操作员的角度来看,将只有一个扩展-迁移-收缩操作。换句话说,所有功能团队都必须协调,以便所有扩展都已执行,然后是所有迁移,最后是所有收缩。对于其更改完全独立的功能来说,这将很容易,但对于其他功能可能更困难。然而,一旦此规范获得批准,保持零停机时间数据库更改将成为Glance项目的优先事项,因此将在功能规范中解决此类交互。
注意
当前的Glance规范模板在“数据模型影响”部分提出了这个问题:
哪些数据库迁移将伴随此更改?
这应该按照以下思路进行修改。(注意:这只是一个建议,我们可以在修改规范模板的补丁上讨论最佳措辞。)
Glance致力于零停机数据库迁移。请解释此更改将伴随哪些数据库迁移。它们是否有可能干扰本周期已批准的其他规范的数据库迁移?
请记住,我们在这里的目标是获得升级标签。虽然不禁止超出这些标签,但它们确实指定了OpenStack社区采用的成就基线。因此,仅仅满足标签的要求就是一个值得追求的目标。
步骤¶
让我们更详细地了解 Glance 的滚动升级策略。考虑数据库更改的情况,其中在版本 N-1 中存储在“旧列”中的数据将存储在版本 N 中的“新列”中。以下是我们实现滚动升级所采取的步骤。
扩展数据库¶
目标:通过扩展数据库为N版本做好准备
如下图所示,最初,N-1 版本从旧列读取和写入。然后,我们为 N 版本扩展模式,引入新列,而 N-1 版本仍在运行。这应该对 N-1 服务几乎没有影响。
注意
需要注意的是,虽然数据库扩展操作严格要求是增量性的,但添加约束有时可能会造成破坏,因为它们已知会锁定表。MySQL 5.6 for InnoDB 中的在线 DDL 功能缓解了这种情况。因此,简单的更改可能不是问题。(无论如何,本规范中提出的计划只在收缩阶段添加约束。)
----------------------------------------------------------- N-1 -------+-----------------------+--------------------------- | | | | | | Read/Write | Read/Write | | Expand N | | | & | Start +----v-----+ Add +----v-----+----------+ Data | Old | Triggers | Old | New | Migrations +----------+ | +---------------------+ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------+ | +----------+----------+ | | ^ ^ | | | Triggers | | | +-------------+ | | | | <--------------------------------- > | | Expand Database | | <--------------------------------- > |
在扩展数据库时,我们还会添加触发器来保持新旧列同步。
可交付成果:我们建议通过使用 expand 命令扩展 glance-manage 实用程序来实现此功能。可以通过运行 glance-manage db expand 来扩展数据库。
迁移数据¶
目标:填充新列以供N版本使用
此时,只有版本N代码在运行,并且它继续从旧列读取和写入,如下图所示。N-1对旧列的所有写入都与新列同步。当触发器缓慢地填充新列时,我们开始后台数据迁移,以非侵入式方式将数据填充到新列中。
------------------------------------------------- N-1 -----------------+------------------------------- | | | | | Read/Write | | | | Start | Finish Data +----v-----+----------+ Data Migrations | Old | New | Migrations | +---------------------+ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------+----------+ | | ^ ^ | | | Triggers | | | +-------------+ | | | | <--------------------------------- > | | Migrate Data | | <--------------------------------- > |
可交付成果:我们建议扩展glance-manage工具以批量迁移数据。批处理大小可以通过可选参数(例如max_rows)控制。该参数将允许操作员一次调度不超过N行的迁移,以防他们拥有大型数据库并希望仅在非高峰时段运行迁移。如果没有可选参数,将迁移所有行。如果该工具运行时发现没有需要迁移的行,它将返回适当的响应。
例如:glance-manage db migrate --max_rows=10。
部署¶
- 目标:以滚动方式升级 N-1 版本来部署 N 版本,并使两个版本在部署期间共存
在部署期间共存
由于新列现在已准备好使用,我们开始小批量部署N。版本N-1不知道正在进行升级,但是版本N代码与N-1服务共存,如下图所示。当N-1和N服务分别使用旧列和新列时,触发器在每次数据库写入时保持两列同步。这使得N能够看到N-1所做的更新,反之亦然。
------------------------------------+ | N-1 | | --------------+---------------------+ | | | | | | | +------------------------------ | | | | | | N | | | | | +-------+---------------------- | | | | Read/ Read/ | | Write Write | | | | | | | | | | +---v------+----v-----+ Finish N | | Old | New | Deploy Start N +---------------------+ | Deploy | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +----------+----------+ | | ^ ^ | | | Triggers | | | +-------------+ | | | | | | <----------------------------> | | Deploy | | <----------------------------> | | |
注意
由于N-1和N服务共存,用户在某些情况下可能会注意到不一致的行为。通常,新版本向后兼容旧版本。因此,所有请求在两个版本中都应该表现出相似的行为。但是,API的一些更改(例如:错误修复)可能导致两个版本之间行为不同。因此,用户可能会根据哪个服务处理请求而看到对相似请求的不同响应。
同样,用户对新版本引入的新功能发出的请求在由旧服务处理时可能会失败。虽然这种不一致是不可取的,但它可以在升级期间发生停机的情况下被视为一个不错的折衷方案。
收缩数据库¶
目标:完成N版本中所需的模式迁移
当所有服务升级完毕后,旧列将被弃用。新列将是今后的事实来源。旧列已准备好删除。此时,我们收缩数据库,这将删除旧列以及数据库扩展期间添加的触发器。
| | ------------------------------------------- N -----------------------+------------------- | | Read/ | Write | | | | Contract +---v----+ Database | New | & +--------+ Drop | | Triggers | | | | | | | | | | | | | | | | | | | | | | | | | | | ---------+ | | | <-----------------------> | Contract Database | <-----------------------> |
注意
除了删除未使用的列和表外,还需要在此处设置 SQL 约束,例如可空性、唯一性和默认值。
可交付成果:我们建议通过使用 contract 命令扩展 glance-manage 实用程序来实现此功能。可以通过运行 glance-manage db contract 来收缩数据库。
运营商的滚动升级流程¶
以下是零停机升级 Glance 的过程
备份Glance数据库。
选择一个任意的Glance节点或配置一个新节点来安装新版本。如果选择现有Glance节点,请优雅地停止Glance服务。
使用上述选定的节点升级到新版本并相应地更新配置。但是,Glance服务目前绝不能启动。
使用升级后的节点,使用命令
glance-manage db expand扩展数据库。然后,使用命令
glance-manage db migrate --max_rows=<max. row count>调度数据迁移。数据迁移必须安排运行,直到没有更多的行需要迁移。
启动第一个节点上的 Glance 进程。
从剩余节点中一次取一个节点,停止Glance进程,升级到新版本(及相应的配置),然后启动Glance进程。
注意
在停止节点上的Glance进程之前,可以选择等待所有现有连接耗尽。这可以通过将节点从轮换中移除来实现。这样,所有当前正在处理的请求都将有机会完成处理。然而,一些Glance请求,如上传和下载镜像,可能持续很长时间。这会增加等待连接耗尽的时间,从而增加完全升级Glance的时间。另一方面,在连接耗尽之前停止Glance服务将向用户显示错误。这有时也可能被视为停机时间。因此,操作员在停止服务时必须谨慎。
从任何一个节点运行命令
glance manage db contract来收缩数据库。
示例¶
为了理解这将如何实际运作,请考虑以下针对 Ocata 提出的 Glance 数据库更改示例。
注意
这不规定实际的Ocata数据库更改。它在此处作为一个现实示例,用于对本提案进行健全性检查。
“旧列”:Newton (N-1 版本):images 表中的布尔类型 is_public 列。此列设置为 nullable=False 和 default=False。
“新列”:Ocata (N 版本):images 表中的枚举(或字符串...关键是它是不同的数据类型)visibility 列。此列可以具有“public”、“private”、“shared”、“community”中的一个值。数据库收缩完成后,此列将具有 nullable=False 和 default='private'。(在迁移和部署阶段,此列可能具有 nullable=true 且没有默认值。)
使用提议的策略,数据库升级将按以下方式进行。
预升级:版本N-1代码读/写到
is_public。扩展数据库:添加
visibility列和适当的触发器,以保持新旧值同步。迁移数据:遍历“images”表。对于任何
visibility为空的行,按如下方式设置visibility的值:如果
is_public为“1”:将visibility设置为public如果
is_public为“0”:如果镜像有任何成员,则将visibility设置为shared;否则将visibility设置为private
使用上述条件,迁移由N-1代码写入旧列的任何数据(使用触发器)。
部署:以滚动方式部署 N 代码。N 代码将开始使用
visibility列。以下是对数据库活动的分析。
写入操作
v1 API
无需担心,没有
visibility的概念
v2 API
visibility设置为publicN-1:将在
is_public中写入“1”* 触发器将在visibility中写入“public”N:将在
visibility中写入“public”* 触发器将在is_public中写入“1”
visibility设置为privateN-1:将在
is_public中写入“0”* 触发器将在visibility中写入“private”N:将在
visibility中写入“private”* 触发器将在is_public中写入“0”
visibility设置为communityN-1:调用将在API级别失败,永远不会命中数据库
N:将在
visibility中写入“community”* 触发器将在is_public中写入“0”注意
这实质上意味着社区镜像将被N-1视为私有镜像。因此,除了所有者之外,社区镜像将对任何人不可见。由于N-1没有社区镜像的概念,这种行为对于N-1来说可以被视为一致的。然而,这可能会让社区镜像的所有者感到困惑,因为在N版本中镜像显示为社区,而在N-1版本中显示为私有。因此,所有者可能会尝试再次更改可见性。为了阻止这种情况,当
is_public列为“0”且visibility列为community时,我们可能会禁止对is_public列进行任何写入。这可以通过再次使用我们在数据库扩展期间添加的相同触发器来完成。备选方案部分中提到的第一个备选方案避免了这种情况。
visibility设置为sharedN-1:调用将在API级别失败,永远不会命中数据库
N:将在
visibility中写入“shared”触发器将在
is_public中写入“0”;这将允许镜像共享继续在N-1版本的节点以及所有节点上的v1 API中正常工作。
读取操作
跨API版本和版本的读取操作应该不受影响,因为触发器通过适当转换数据来保持新旧列同步。
收缩数据库:仅运行版本N的API节点。
is_public列不再使用。删除is_public并向visibility列添加nullable=True和default=private。
备选方案¶
这是上述策略的一个小变体。上述策略的基本思想是:当两个版本共存时,同步一组服务所做的写入,使其可供其他服务使用。我们通过触发器来实现这一点。另一方面,如果我们消除了同步的需要呢?也就是说,如果我们在服务共存时禁止对新旧列进行任何写入呢?这可以通过再次使用触发器来实现。实质上,我们在数据库扩展步骤中添加的触发器将拦截并禁止对新旧列的写入。
对于上述示例,在服务共存的部署步骤期间,所有尝试更改镜像可见性的请求都将失败。但是,允许读取。一旦部署完成并且我们收缩数据库(此处删除触发器),将像往常一样允许对新列的写入。这为我们提供了一种消除跨列同步数据需求的方法。因此,触发器中的复杂性大大降低,升级也不太容易出错。但是,需要注意的是,由于不允许写入,在部署期间可能会看到错误率增加。尽管在此整个期间API将保持响应(因此“正常运行”),但5xx响应率的增加将使得无法断言
assert:zero-downtime-upgrade标签[GVV2]。由于能够断言此标签是本规范的目标,因此此替代方案不可接受。一个众所周知的替代方案是通过应用程序内部在线迁移数据来取代触发器的使用。虽然触发器方法在数据库写入操作时在线迁移数据,但另一种方法试图在数据库读取操作时按需迁移数据。
其他 Openstack 项目采用的方法
Nova:参见[NOV1]。
Cinder:参见[CIN1]。
Keystone:参见[KEY1]。
数据模型影响¶
无
REST API 影响¶
REST API契约本身不会受到影响。
安全影响¶
无
通知影响¶
无
其他最终用户影响¶
无
性能影响¶
后台数据迁移将消耗额外的数据库资源,但如果仔细编写迁移脚本,则可以进行管理。
其他部署者影响¶
打算以旧方式(即停机)部署 Glance 的部署人员不受影响。
迁移的每个步骤都需要操作员干预。
开发人员影响¶
任何开发需要数据库更改功能的开发人员都必须编写额外的代码来支持本文档中概述的滚动升级策略。然而,通过将数据库更改限制在单一版本中,N+1版本的开发人员不必担心完成在N-1到N版本迁移期间开始的程序。
实现¶
负责人¶
- 主要负责人
alex_bash hemanthm
- 其他贡献者
尼基尔
工作项¶
编写滚动升级文档(开发人员文档)。
编写滚动升级文档(操作员文档)。
引入扩展/收缩迁移流和相应的glance-manage CLI。
与需要数据库更改的 Ocata 功能开发人员合作,以实施遵循滚动升级策略的代码。这包括:
社区镜像和增强的镜像共享
镜像导入
依赖项¶
无
测试¶
为了断言滚动升级标签,Glance必须进行完整的堆栈集成测试,并采用代表对运营商有意义的滚动升级场景的服务安排。
理想情况下,这些测试将能够模拟大规模运行的Glance,因为如上所述,一些DBMS问题可能不会在小型测试数据库中发现。
文档影响¶
开发人员文档:升级策略。
操作员文档
将代码置于各种模式下的配置选项
运行数据库脚本
参考资料¶
https://governance.openstack.org/reference/tags/assert_supports-zero-downtime-upgrade.html
https://specs.openstack.org/openstack/cinder-specs/specs/mitaka/online-schema-upgrades.html
https://specs.openstack.org/openstack/keystone-specs/specs/mitaka/online-schema-migration.html