支持滚动升级¶
https://bugs.launchpad.net/ironic/+bug/1526283
本文档提出支持 ironic 的“滚动升级”,这将允许操作员在不必同时重启所有服务的情况下,将新代码部署到 ironic 服务。升级 ironic 服务时,几乎没有停机时间。在 ironic 升级期间,实例不受影响;它们应继续运行并具有网络访问权限。不过,在裸机节点上执行 ironic 操作可能会稍有延迟。
对滚动升级的支持将满足 OpenStack 治理中提到的标准 [1]。
问题描述¶
运营 OpenStack 云的人员面临同样的问题:如何将云升级到最新版本,以便享受新功能和更改。
通常一次升级云的一个组件。例如,可以按以下顺序升级 OpenStack 的这些组件
升级 keystone
升级 glance
升级 ironic
升级 nova
升级 neutron
升级 cinder
Ironic 已经支持“冷升级” [14],其中 ironic 服务在升级期间必须关闭。对于耗时的升级,服务不可用很长时间可能无法接受。执行冷升级的步骤 [13]可能是
停止所有 ironic-api 和 ironic-conductor 服务
卸载旧代码
安装新代码
更新配置
DB 同步(最耗时的任务)
启动 ironic-api 和 ironic-conductor 服务
滚动升级将为云的用户和操作员提供更好的体验。在 ironic 的滚动升级上下文中,这意味着不必像冷升级那样同时升级所有 ironic-api 和 ironic-conductor 服务。滚动升级允许一次升级单个 ironic-api 和 ironic-conductor 服务,而其余服务仍然可用。此次升级将具有最少的停机时间。
虽然我们希望此处提出的滚动升级解决方案与 nova 的升级流程相同 [2],但 nova 和 ironic 之间存在一些差异,这些差异阻止我们使用相同的解决方案。这些差异将在本规范的其他部分中提到。
提议的变更¶
为了使滚动升级具有最少的停机时间,应该至少有两个 ironic-api 服务和两个 ironic-conductor 服务正在运行。
由于 ironic 不支持数据库降级,因此不支持回滚。
为了支持 ironic 的滚动升级,需要以下内容
代码更改
开发人员和审查人员的贡献指南
一个多节点 grenade CI,以防止破坏滚动升级机制
操作员的滚动升级操作指南。
以下子部分描述了
ironic 版本之间的滚动升级支持
提议的滚动升级流程
需要进行的更改
版本之间的滚动升级¶
Ironic 遵循带有中间版本发布的发布周期模型 [6]。这些版本是语义化的 [7],格式为 <major>.<minor>.<patch>。我们将 ironic 的“命名版本”称为与开发周期(如 Mitaka)关联的版本。
此外,ironic 遵循标准的弃用策略 [8],该策略规定弃用期必须至少三个月且跨越一个周期边界。这意味着两个命名版本之间永远不会同时存在已弃用和删除的内容。
将支持以下滚动升级:
命名版本 N 到 N+1。(如果此功能合并到 Ocata 中,N 将从 Newton 开始。)
任何命名版本到其最新的修订版本,其中包含回移植的错误修复。由于这些错误修复可能包含升级过程的改进,操作员应在命名版本之间升级之前修补系统。
最新的命名版本 N(以及比 N 新的语义版本)到 master。与上述项目符号类似,我们可能会在 master 分支中引入一个错误或功能,我们希望在发布命名版本之前将其删除。弃用策略允许在 3 个月的时间范围内执行此操作 [8]。如果该功能在中间版本中包含并删除,应添加发行说明,其中包含有关如何从受影响的版本或版本范围升级到 master 的说明。这通常会指示操作员先升级到特定的中间版本,然后再升级到 master。
滚动升级流程¶
将 ironic 从版本 FromVer 升级到下一个版本 ToVer 的滚动升级流程如下:
在升级 ironic 之前,升级 Ironic Python Agent 镜像。
通过 ironic-dbsync upgrade 将 DB 模式升级到
ToVer。Ironic 已经具备执行此操作的代码。但是,需要记录新的 DB 迁移策略(如下文 新的 DB 模型更改策略 中所述)。通过下面描述的新配置选项,通过将 RPC 和 IronicObject 版本固定到 ironic-api 和 ironic-conductor 服务的相同
FromVer,从而固定 RPC 和 IronicObject 版本。升级代码并一次重启一个 ironic-conductor 服务。
升级代码并一次重启一个 ironic-api 服务。
取消固定 RPC 和对象版本,以便服务现在可以使用
ToVer中的最新版本。通过更新下面描述的新配置选项(在 RPC 和对象版本固定 中)并重新启动服务来完成此操作。应先重新启动 ironic-conductor 服务,然后再重新启动 ironic-api 服务。这样可以确保在未固定的 API 服务上公开新功能(通过 API 微版本)时,它可以在后端可用。运行新命令 ironic-dbsync online_data_migration,以确保所有 DB 记录都“升级”到新的数据版本。这个新命令在单独的 RFE 中讨论 [12](并且是这项工作的一个依赖项)。
升级 ironic 客户端库(例如 python-ironicclient)和其他使用新引入的 API 功能并依赖于新版本的服务。
上述流程将导致 ironic 服务以以下顺序运行 FromVer 和 ToVer 版本(其中“步骤”指的是上述步骤)
步骤 |
ironic-api |
ironic-conductor |
|---|---|---|
0 |
全部 FromVer |
全部 FromVer |
4.1 |
全部 FromVer |
部分 FromVer,部分 ToVer-固定 |
4.2 |
全部 FromVer |
全部 ToVer-固定 |
5.1 |
部分 FromVer,部分 ToVer-固定 |
全部 ToVer-固定 |
5.2 |
全部 ToVer-固定 |
全部 ToVer-固定 |
6.1 |
全部 ToVer-固定 |
部分 ToVer-固定,部分 ToVer |
6.2 |
全部 ToVer-固定 |
全部 ToVer |
6.3 |
部分 ToVer-固定,部分 ToVer |
全部 ToVer |
6.4 |
全部 ToVer |
全部 ToVer |
新的 DB 模型更改策略¶
这不是代码更改,但它会影响 SQLAlchemy DB 模型,并且需要开发人员和审查人员充分记录。新的 DB 模型更改策略如下:
支持将新项目添加到 DB 模型。
删除列/表和相应的对象字段受 ironic 弃用策略 [8] 的约束。但是,它的 alembic 脚本必须等待一个额外的弃用周期,否则当
FromVer服务访问 DB 时,将抛出“未知列”异常。这是因为 ironic-dbsync upgrade 会升级 DB 模式,但FromVer服务仍然在其 SQLAlchemy DB 模型中包含已删除的字段。不再支持 alter_column,例如重命名或调整大小。这必须拆分为多个操作,例如添加列,然后删除列。某些更改可能需要拆分为多个版本才能保持与旧 SQLAlchemy 模型的兼容性。
PostgreSQL 中某些 ALTER TABLE 的实现(例如添加外键)可能会施加表锁并导致停机。如果无法避免更改并且影响很大(表可以频繁访问和/或存储大型数据集),则必须在发行说明中提及这些情况。
RPC 和对象版本固定¶
为了使 ironic(ironic-api 和 ironic-conductor)服务在滚动升级期间同时运行旧版本和新版本,服务需要能够处理不同的 RPC 版本和对象版本。
[4] 很好地描述了为什么我们需要 RPC 版本控制,并描述了 nova 如何处理它。本文档建议在 ironic 中采用类似的方法。
对于对象版本控制,ironic 使用 oslo.versionedobjects。 [5] 描述了 nova 处理此问题的方法。不幸的是,ironic 的解决方案不同,因为 ironic 的情况更复杂。在 nova 中,所有数据库访问(读取和写入)都通过 nova-conductor 服务完成。这使得 nova-conductor 服务成为处理不同对象版本之间转换的唯一服务。(有关更多详细信息,请参阅 [5]。)对于 nova 滚动升级,所有非 nova-compute 服务都会关闭,然后使用新版本重新启动;nova-conductor 是第一个重新启动的服务 ([2])。因此,nova-conductor 服务始终运行相同的版本,并且不需要处理彼此之间不同的对象版本。一旦它们运行新版本,就可以处理来自运行旧版本或新版本的其他服务的请求。
与 ironic 形成对比,ironic-api 和 ironic-conductor 服务都会访问数据库进行读取和写入。这两个服务都需要了解不同的对象版本。例如,ironic-api 可以创建 Chassis、Ports 和 Portgroups 等对象,直接保存到数据库,而无需通过 conductor。我们无法像 nova-conductor 服务一样关闭 ironic-conductor,因为 ironic-conductor 所做的事情远不止与数据库交互,并且至少需要一个 ironic-conductor 在滚动升级期间运行。
将添加一个新的配置选项。它将用于固定所有 ironic 服务的 RPC 和 IronicObject(例如 Node、Conductor、Chassis、Port 和 Portgroup)版本。通过此配置选项,服务将能够正确处理不同版本服务之间的通信。
新的配置选项是:[DEFAULT]/pin_release_version。默认值为空表示 ironic-api 和 ironic-conductor 将使用 RPC 和 IronicObjects 的最新版本。其可能的值是发布版、命名版(例如 ocata)或语义版本(例如 7.0)。
在内部,ironic 将维护一个映射,指示与每个版本关联的 RPC 和 IronicObject 版本。此映射将手动维护。(添加映射的自动化过程是可能的,但超出本规范的范围。)这是一个例子
objects_mapping
{'mitaka': {'Node': '1.14', 'Conductor': '1.1', 'Chassis': '1.3', 'Port': '1.5', 'Portgroup': '1.0'}, '5.23': {'Node': '1.15', 'Conductor': '1.1', 'Chassis': '1.3', 'Port': '1.5', 'Portgroup': '1.0'}}
rpc_mapping
{'mitaka': '1.33', '5.23': '1.33'}
在滚动升级期间,使用新版本的服务应将此值设置为旧版本的名称(或版本)。这将指示使用新版本的服务,它们应该与使用旧版本的服务兼容的 RPC 和对象版本。
处理 RPC 版本¶
ConductorAPI.__init__() 已经设置了一个 version_cap 变量,将其设置为最新的 RPC API 版本,并将其作为初始化参数传递给 RPCClient。此 version_cap 用于确定 RPCClient 可以发送的最大请求消息版本。
为了使与先前版本兼容的 RPC 调用,代码将被更改,以便将 version_cap 设置为固定版本(对应于先前版本),而不是最新的 RPC_API_VERSION。然后,每个 RPC 调用将根据此 version_cap 自定义请求。
处理 IronicObject 版本¶
在内部,ironic 服务(ironic-api 和 ironic-conductor)将处理最新版本的 IronicObject。只有在这些边界处,当 IronicObject 进入或离开服务时,才需要处理对象版本控制。
从数据库获取对象:转换为最新版本
将对象保存到数据库:如果已固定,则保存为固定版本;否则保存为最新版本
序列化对象(通过 RPC 发送):如果已固定,则发送固定版本;否则发送最新版本
反序列化对象(接收来自 RPC 的对象):转换为最新版本
ironic-api 服务还需要根据 API 版本和对象版本是否支持某个功能来处理 API 请求/响应。例如,当 ironic-api 服务被固定时,它只能允许对象固定版本中可用的操作,并且不允许仅适用于该对象最新版本的操作。
为了支持这一点
在所有 IronicObject 的数据库表(SQLAlchemy 模型)中添加一个名为
version的新列。该值是保存到数据库中的对象的版本。此版本列最初将为空,并将由数据迁移脚本填充适当的版本。如果在 Ocata 中有需要迁移数据的更改,我们将检查新版本列中是否为空。
没有项目使用版本列机制来实现此目的,但如果没有它会更复杂。例如,Cinder 有一个跨越 4 个版本的迁移策略,其中数据会重复一段时间。Keystone 使用触发器在一个发布周期内维护重复数据。此外,版本列可能证明对零停机时间升级(未来)有用。
添加一个新的方法
IronicObject.get_target_version(self)。这将返回目标版本。如果已固定,则返回固定版本。否则,返回最新版本。添加一个新的方法
IronicObject.convert_to_version(self, target_version)。此方法会将对象转换为目标版本。目标版本可以是比对象现有版本更新或更旧的版本。大部分工作将在新的辅助方法IronicObject._convert_to_version(self, target_version)中完成。具有新版本的子类应重新定义此方法以执行实际转换。添加一个新的方法
IronicObject.do_version_changes_for_db(self)。这将在 将对象保存到数据库(API/conductor -> DB) 中描述。添加一个新的方法
IronicObjectSerializer._process_object(self, context, objprim)。这将在 通过 RPC 接收对象(API/conductor <- RPC) 中描述。
在以下内容中,
旧版本是
FromVer;它使用版本 ‘1.14’ 的 Node 对象。新版本是
ToVer;它使用版本 ‘1.15’ 的 Node 对象 – 该版本有一个已弃用的extra字段和一个新的meta字段,该字段取代了extra。db_obj[‘meta’] 和 db_obj[‘extra’] 是这些节点字段的数据库表示形式。
从数据库获取对象(API/conductor <– DB)¶
ironic-api 和 ironic-conductor 服务都从数据库读取值。这些值通过现有方法 IronicObject._from_db_object(context, obj, db_object) 转换为 IronicObject。此方法将被更改,以便即使对象在数据库中是旧版本,IronicObject 也将处于最新版本。无论服务是否已固定,都执行此操作。
请注意,如果对象转换为较新版本,则该 IronicObject 将保留因转换而产生的所有更改(以防对象稍后保存为最新版本)。
例如,如果数据库中的节点是版本 1.14 并且 db_obj[‘extra’] 已设置
一个
FromVer服务将获得一个 Node,其中 node.extra = db_obj[‘extra’](并且不知道 node.meta,因为它不存在)。一个
ToVer服务(已固定或未固定)将获得一个 Node,其中node.meta = db_obj[‘extra’]
node.extra = None
node._changed_fields = [‘meta’, ‘extra’]
将对象保存到数据库(API/conductor –> DB)¶
保存 IronicObject 到数据库所使用的版本确定如下
对于未固定的服务,对象将保存为最新版本。由于对象始终处于最新版本,因此不需要进行任何转换。
对于已固定的服务,对象将保存为固定版本。由于对象始终处于最新版本,因此在保存之前需要将对象转换为固定版本。
新的方法 IronicObject.do_version_changes_for_db() 将处理此逻辑,返回一个包含已更改字段及其新值的字典(类似于现有的 oslo.versionedobjects.VersionedObjectobj.obj_get_changes())。由于我们不内部跟踪对象的数据库版本,因此对象的 version 字段始终是这些更改的一部分。
滚动升级过程(在步骤 6.1 处)确保在对象可以保存为最新版本时,所有服务都正在运行较新版本(尽管其中一些可能仍然已固定),并且可以处理最新的对象版本。
当服务如步骤 6.1 中所述时,可能会发生一种有趣的情况。有可能将 IronicObject 保存为较新版本,然后将其保存为较旧版本。例如,一个 ToVer 未固定的 conductor 可能会保存一个版本为 1.5 的节点。随后的请求可能会导致一个 ToVer 已固定的 conductor 替换并保存相同的节点为版本 1.4!
通过 RPC 发送对象(API/conductor -> RPC)¶
当服务发出 RPC 请求时,作为该请求发送的任何 IronicObject 都会序列化为实体或原始类型(通过 oslo.versionedobjects.VersionedObjectSerializer.serialize_entity())。用于对象序列化的版本如下
对于未固定的服务,对象将序列化为最新版本。由于对象始终处于最新版本,因此不需要进行任何转换。
对于已固定的服务,对象将序列化为固定版本。由于对象始终处于最新版本,因此在序列化之前需要将对象转换为固定版本。转换后的对象将包括因转换而产生的更改;如果 RPC 请求的另一端的服务将对象保存到数据库,则这是必需的。
IronicObjectSerializer.serialize_entity() 方法将被修改以执行任何 IronicObject 转换。
通过 RPC 接收对象(API/conductor <- RPC)¶
当服务接收 RPC 请求时,请求中的任何实体都需要反序列化(通过 oslo.versionedobjects.VersionedObjectSerializer.deserialize_entity())。对于表示 IronicObject 的实体,我们希望反序列化过程产生处于最新版本的 IronicObject,无论它们发送的版本如何,无论接收服务是否已固定。同样,任何转换的对象都将保留因转换而产生的更改,如果该对象稍后保存到数据库,这将很有用。
反序列化方法调用 VersionedObjectSerializer._process_object() 来反序列化并获取 IronicObject。我们将添加 IronicObjectSerializer._process_object() 将 IronicObject 转换为其最新版本。
例如,一个 FromVer ironic-api 可以发出一个带有版本 1.4 的节点的 update_node() RPC 请求,其中 node.extra 已更改(因此 node._changed_fields = [‘extra’])。此节点将序列化为版本 1.4。接收的 ToVer 已固定的 ironic-conductor 对其进行反序列化并将其转换为版本 1.5。结果节点将具有 node.meta 设置(设置为 v1.4 中 node.extra 的更改值),node.extra = None,以及 node._changed_fields = [‘meta’, ‘extra’]。
备选方案¶
可以执行冷升级,但这意味着 ironic 服务将在升级期间不可用,这可能需要很长时间。
与其让服务始终以最新版本处理对象,可以使用不同的设计,例如,已固定的服务以固定版本处理其对象。但是,经过一些实验,这被证明具有更多的(边缘)情况需要考虑,并且更难以理解。这种方法将使将来维护和故障排除更加困难,假设审查者能够同意它首先有效!
如果我们更改 ironic-api 服务,使其只对 DB 具有只读访问权限,并且所有写入都通过 ironic-conductor 服务进行,会简化支持滚动升级所需的更改吗?也许吧;也许不会。(尽管作者认为无论如何,让所有写入都由 conductor 完成会更好。)无论是否进行此更改,我们需要确保对象不会保存为较新版本(即,比旧版本中的版本更新),直到所有服务都运行新版本 – 滚动升级过程 的步骤 5.2。本文档中描述的解决方案从步骤 6.1 开始将对象保存为最新版本,因为如果服务未固定,仅在最新版本保存对象在概念上易于理解。
当然,可能还有其他方法来处理此问题,例如让所有服务“注册”它们正在数据库中运行的版本并以某种方式利用这些数据。Dmitry Tantsur 思考过是否可以使用一些远程同步内容(例如 etcd)让服务了解升级过程。
理想情况下,ironic 应该使用“OpenStack 偏好的”方式来实现滚动升级,但这似乎不存在,因此这尝试利用 nova 所做的工作。
数据模型影响¶
采用 DB 迁移策略并在 新的 DB 模型更改策略 中介绍。
将添加一个新列 version 到所有 IronicObject 对象的数据库表中。其值将是保存到数据库中的对象的版本。
状态机影响¶
无
REST API 影响¶
此版本列最初将为空,并将由数据迁移脚本填充适当的版本。
在滚动升级过程中,API 服务可能同时以不同的版本运行。这两个 API 服务版本都应与旧版本中的 python-ironicclient 库兼容(我们已经使用微版本来保证这一点)。新的 API 功能仅在完成升级过程(包括取消固定 ironic 服务的内部 RPC 通信版本)后才可用。因此,在完成升级之前,不应为新的功能(即,使用新的 API 微版本)发出 API 请求,因为无法保证它们能正常工作。
作为未来的增强功能(超出此规范的范围),我们可以禁止 API 服务固定时请求新的功能。
客户端 (CLI) 影响¶
无
“ironic” CLI¶
无
“openstack baremetal” CLI¶
无
RPC API 影响¶
RPC API 本身没有变化,尽管需要进行更改以支持不同的 RPC API 版本。version_cap 将设置为固定版本(先前版本)以使先前版本的 RPC 调用兼容。
开发人员在更改现有的 RPC API 调用时应记住这一点。
驱动程序 API 影响¶
无
Nova 驱动程序影响¶
由于 REST API 没有变化,因此无需更改 nova 驱动程序。ironic 应该在 nova 之前升级,并且 nova 使用升级后的 ironic 中仍然受支持的特定微版本调用 ironic。因此,所有内容都可以在不更改 nova 驱动程序的情况下正常工作。
Ramdisk 影响¶
无
安全影响¶
无
其他最终用户影响¶
无
可扩展性影响¶
无
性能影响¶
涉及数据迁移的操作在升级期间可能需要更长时间。每次此类更改都应在受影响版本的发布说明中提及。
其他部署者影响¶
在滚动升级期间,部署程序将使用新的配置选项 [DEFAULT]/pin_release_version 来固定和取消固定如上所述在 RPC 和对象版本固定 中使用的 RPC 和 IronicObject 版本。
此规范不涉及尝试停止/回滚到以前的版本。
开发人员影响¶
代码贡献指南 [11] 将更新,以描述开发人员需要了解和遵循的内容。它将提及
在发布之前,手动添加发布(命名或语义版本化)与关联的 RPC 和对象版本的映射
新的 DB 模型更改策略
贡献指南将指向设计文档,因为它与如何根据滚动升级实现新功能有关。
实现¶
负责人¶
主要负责人
xek
rloo
其他贡献者
mario-villaplana-j(文档)
工作项¶
添加新的配置选项
[DEFAULT]/pin_release_version和 RPC/Object 版本映射到发布。使对象与先前版本兼容,并处理与 DB 和服务的交互。
使 IronicObjectSerializer 在通过 RPC 传递时降级对象。
添加测试。
添加文档和指针,用于 RPC 和 oslo 对象版本控制。
添加有关新的 DB 模型更改策略的文档。
添加管理员文档,供操作员使用,描述如何执行滚动升级,包括应升级所有相关服务的顺序。
依赖项¶
需要新的命令 ironic-dbsync online_data_migration [12]。
需要多节点 grenade CI 工作。
测试¶
单元测试
多节点榴弹 CI。 这将测试滚动升级过程从
fromVer到toVer是否继续有效。榴弹执行完全升级,不进行固定版本。 这将测试旧的 API/调度器和新的 API/调度器未固定版本的情况。 我们不会运行在线数据迁移,因此新的服务将读取旧格式的数据。
榴弹多节点将在子节点上运行旧的 API/调度器,该子节点不会被升级。 主节点将只有调度器,它会被升级,但会被固定版本。 这将测试旧的 API + 旧的数据,以及 1. 旧的调度器,和 2. 新的调度器与固定版本。 测试将在升级前后运行。
我们也可以将 API 移动到主节点,升级它,并固定版本,以测试新的 API 代码与固定版本。 由于所有对象转换都在对象层进行,这可能不需要,因为它可能不会测试到已经通过调度器测试过的很多内容。
如果榴弹可以设置为停止子节点上的旧 API 服务,并在完成第一个测试后在升级后的主节点上启动它,则可以将上述两个测试合并。
测试应尽可能涵盖 版本间的滚动升级 中描述的使用场景。
升级和向后兼容性¶
无;REST API 或 Driver API 没有更改。
文档影响¶
将为
部署者添加文档。 这将描述滚动升级过程以及他们需要采取的步骤。 这应该记录在升级指南中,或链接到升级指南 [13]。
开发者。 这将描述开发者需要了解的内容,以便滚动升级过程继续有效。 这包括关于 RPC 和 oslo 对象版本控制,以及数据库模型更改策略的文档。