支持滚动升级¶
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 和对象版本固定 中所述)为 ironic-api 和 ironic-conductor 服务固定 RPC 和 IronicObject 版本到相同的
FromVer。升级代码并一次重启一个 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字段和一个替换extra的新meta字段。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 影响¶
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 和 IronicObject 版本,如上文 RPC 和对象版本固定 中所述。
本规范不涉及尝试停止/回滚到之前的发布版本。
开发人员影响¶
代码贡献指南 [11] 将更新,以描述开发人员需要了解和遵循的内容。它将提及
在发布版本被切割之前,手动添加发布版本(命名或语义版本)与关联的 RPC 和对象版本的映射
新的 DB 模型变更策略
贡献指南将指向设计文档,这与新功能应该如何根据滚动升级进行实现有关
实现¶
负责人¶
主要负责人
xek
rloo
其他贡献者
mario-villaplana-j (文档)
工作项¶
添加新的配置选项
[DEFAULT]/pin_release_version以及发布版本的 RPC/对象版本映射。使对象与之前的版本兼容,并处理与 DB 和服务之间的交互。
使 IronicObjectSerializer 在通过 RPC 传递对象时降级对象。
添加测试。
添加文档和指针,用于 RPC 和 oslo 对象版本控制。
添加关于新的 DB 模型变更策略的文档。
添加管理员文档,供操作员使用,描述如何执行滚动升级,包括所有相关服务应该升级的顺序。
依赖项¶
需要新的命令 ironic-dbsync online_data_migration [12]。
需要多节点 grenade CI 正常工作。
测试¶
单元测试
多节点 grenade CI。 这测试了滚动升级过程从
fromVer到toVer是否继续有效。grenade 执行完全升级,不固定版本。 这测试了旧的 API/conductor 和新的 API/conductor 未固定的情况。 我们不会运行在线数据迁移,因此新的服务将读取旧格式的数据。
grenade 多节点将在子节点上运行旧的 API/conductor,该子节点不会被升级。 主节点将只有 conductor,它会被升级,但会被固定。 这测试了旧的 API + 旧的数据,以及 1. 旧的 conductor 和 2. 新的 conductor 与固定版本。 测试在升级前后运行。
我们也可以将 API 移动到主节点,升级它,并固定版本,以测试新的 API 代码与固定版本。 由于所有的对象转换都发生在对象层,因此这可能不需要,因为它可能不会测试到已经通过 conductor 测试过的很多内容。
如果 grenade 可以设置为停止子节点上的旧 API 服务,并在完成第一个测试后在升级的主节点上启动它,则可以将上述两个测试合并。
测试应该尽可能涵盖 发布版本之间的滚动升级 中描述的使用场景。
升级和向后兼容性¶
无;REST API 或 Driver API 没有变化。
文档影响¶
将为以下对象添加文档:
部署者。 这将描述滚动升级过程以及他们需要采取的步骤。 这应该记录在升级指南中,或链接到升级指南 [13]。
开发人员。 这将描述开发人员需要了解的内容,以便滚动升级过程继续有效。 这包括关于 RPC 和 oslo 对象版本控制以及 DB 模型变更策略的文档。