Ironic Inspector 的高可用性

Ironic inspector 是一项允许对裸机节点进行动态内省的服务,目前尚不具备冗余性。此蓝图的目标是提出对 inspector 服务的概念性更改,使其具备冗余性,同时保持当前检查功能集和 API。

问题描述

Inspector 是一种复合服务,由 inspector API 服务、防火墙和 DHCP(PXE)服务组成。目前,这三个组件在每个 OpenStack 部署中都在共享主机上运行单个实例。主机或任何服务的故障都会导致内省不可用,并阻止云管理员注册新硬件或启动已注册的裸机节点。此外,Inspector 尚未设计为能够很好地应对大规模 Ironic 裸机使用所需的硬件量。考虑到站点规模为 10k 个裸机节点,我们的目标是 inspector 能够维持几百个内省/注册请求的批处理负载,并穿插几分钟的空闲时间,同时保持几千个防火墙黑名单条目。我们将此用例称为裸机到租户

下面我们描述当前的 Inspector 服务架构以及一些 Inspector 进程实例故障后果。

内省流程

节点内省是一个异步步骤序列,由 inspector API 服务控制,完成每个步骤需要不同的时间。可以将这些步骤描述为转换系统的状态,由事件推进,如下所示

  • starting 初始状态;系统通过接收内省 API 请求进入此状态。在此状态下执行内省配置和设置步骤。

  • waiting 内省镜像正在节点上启动。系统自动进入此状态。

  • processing 内省镜像已启动并从节点收集了必要的信息。插件正在处理此信息以验证节点状态。系统在收到 continue REST API 请求后进入此状态。

  • finished 内省完成,节点已关机。系统自动进入此状态。

如果 API 服务发生故障,处于 startingfinished 状态之间的节点将丢失其状态,并且可能需要手动干预才能恢复。由于 API 服务每个部署仅运行一个实例,因此无法处理更多节点。

防火墙配置

为了最大限度地减少对正常部署节点造成的干扰,inspector 部署了临时防火墙规则,以便只有正在检查的节点才能访问其 PXE 启动服务。它被实现为黑名单,其中包含 ironic 服务保留但 inspector 不保留的 MAC 地址。这是必需的,因为在节点首次启动之前无法知道 MAC 地址。

如果防火墙和 DHCP 服务完好无损,而 API 服务在防火墙配置过程中发生故障,防火墙配置可能会不同步,并可能导致对正常节点启动的干扰

  • 防火墙链设置(初始化阶段):Inspector 的 dnsmasq 服务暴露给所有节点

  • 防火墙同步周期性任务:添加到 Ironic 的新节点不会被列入黑名单

  • 节点内省完成:节点不会被列入黑名单

另一方面,如果所有服务(inspector、防火墙和 DHCP)都在同一主机上运行,则所有服务将一起丢失,因此预计不会发生启动干扰。在清理周期性任务期间丢失 API 服务,不应成为问题,因为相关节点将在服务停机期间保持在黑名单中。

DHCP(PXE)服务

Inspector 服务不直接管理 DHCP 服务,而是只需要 DHCP 设置正确并与 API 服务和防火墙共享主机。我们仍然想简要描述 DHCP 服务发生故障的后果。

如果 DHCP 服务发生故障,被检查的节点将无法启动内省 ramdisk,最终由于超时而无法被检查。节点可能会根据其固件配置循环重试启动。

从活动主机故障转移到备份主机(通常是 dnsmasq)将表现为正在内省的节点超时或已启动的节点(拥有地址租约)与其他节点启动时发生地址冲突。除了重试之外,没有太多方法可以帮助前一种情况。为了防止后者发生,为内省目的配置 DHCP 服务应考虑由 DHCP 实例提供的分离地址池,如 DHCP 故障转移协议 RFC 的 服务器之间的 IP 地址分配 部分中所建议的那样。我们还建议在 dnsmasq 配置文件中使用 dhcp-sequential-ip 以避免地址池内的冲突。有关该问题的更多详细信息,请参阅 相关错误报告。由于内省是一个临时事项,因此如果重新启动内省不是问题,则在 DHCP 实例之间同步租约是不必要的。

其他 Inspector 部分

  • 周期性内省状态清理,删除旧的内省数据并完成超时的内省

  • 与 ironic 同步节点

  • 使用共享锁和超时限制节点上电速率

提议的变更

在考虑高可用性问题时,我们提出了一种解决方案,该解决方案由所有组成 ironic inspector 的服务的分布式、无共享、主动-主动实现组成。从用户角度来看,我们建议 API 服务通过负载均衡器(例如 HAProxy)提供服务,以便为 API 服务(例如浮动 IP 地址)维护单个入口点。

HA 节点内省分解

节点内省是一个状态转换系统,我们专注于去中心化它。因此,我们将当前的内省状态通过分布式存储在所有 inspector 进程实例中复制,以用于特定节点。我们建议自动状态推进请求和 API 状态推进请求都由独立的 worker 异步执行。

HA Worker

每个 inspector 进程提供一个异步 worker 池,该池从队列中获取状态转换请求。我们使用单独的 queue.getqueue.consume 调用以避免由于 worker 故障而丢失状态转换请求。但是,这为请求引入了至少一次传递语义。因此,我们依赖于 transition-function 来优雅地处理请求传递。我们建议两种类型的状态转换处理,关于至少一次传递语义

可重入任务转换规范

严格转换保护状态更改可能会导致内省状态与节点实际状态不一致——如果 worker 在成功执行任务后但在从队列中消费请求之前发生分区。因此,未被消费的转换请求将再次遇到(另一个)worker。可以将此行为称为可重入故障或似曾相识

由于目标是保护被检查的节点免于重复执行相同的任务,因此我们依赖于状态转换系统通过导航到 error 状态来处理这种情况。

删除节点

Ironic 同步周期性任务 将节点删除请求放入队列。worker 执行以下步骤来处理

存储删除节点失败在这里并不重要,因为周期性任务稍后会重试。因此,始终在这里消费请求是安全的。

关闭 HA Inspector 进程

所有 inspector 进程实例注册一个 SIGTERM 回调。为了通知 inspector worker 线程,SIGTERM 回调在信号传递时设置 sigterm_flag。该标志是进程本地的,其目的是允许 inspector 进程执行受控/优雅的关闭。为了使此机制有效,潜在的阻塞操作(例如 queue.get)必须在 worker 中使用可配置的超时值。所有过程实例中的睡眠调用都应可中断,可能实现为 sigterm_flag.wait(sleep_time) 或类似方式。

获取请求

  • 任何 worker 实例都可以执行队列中包含的任何请求

  • worker 从队列中获取状态转换或节点删除请求

  • 如果设置了 SIGTERM 标志,worker 停止

  • 如果 queue.get 超时(任务为 None),则再次轮询队列

  • 锁定与请求相关的 BM 节点

  • 如果锁定失败,worker 再次轮询队列,不消费请求

计算新节点状态

  • worker 为当前节点状态实例化一个状态转换系统实例

  • 如果实例化失败(例如,存储中没有该节点),worker 执行 重试请求

  • worker 推进状态转换系统

  • 如果状态机卡住(非法状态转换请求),worker 执行 消费请求

更新节点状态

内省状态保存在存储中,所有 worker 实例可见。

  • worker 将节点状态保存在存储中

  • 如果在存储中保存节点状态失败(例如,节点已被删除),worker 执行 重试请求

执行任务

  • worker 执行与转换请求绑定的任务

  • 如果任务结果是转换请求,worker 将其放入队列

消费请求

  • worker 从队列中消费状态转换请求

  • worker 释放相关的节点锁

  • worker 从头开始继续

重试请求

  • worker 释放节点锁

  • worker 从头开始继续,不消费请求以稍后重试

内省状态转换系统

节点内省状态由 worker 本地实例的状态转换系统管理。状态转换函数如下。

转换函数

State

事件

目标

N/A

检查

启动

启动*

检查

启动

启动*

S~

等待

等待

S~

等待

等待

超时

Error

等待

中止

Error

等待

继续!

处理

处理

继续!

Error

处理

F~

完成

完成+

检查

启动

完成+

中止

Error

错误+

检查

启动

图例

表达式

含义

State*

初始状态

State+

终端/接受状态

State~

来自 State 的自动事件

Event!

严格/非可重入转换事件

HA 单例周期性任务分解

Ironic inspector 服务包含几个周期性任务。在任何时候,最多只能运行一个“实例”的周期性任务类型,无论进程实例数量如何。为此,这些进程形成一个周期性任务分布式管理方。

进程实例注册一个 SIGTERM 回调,该回调在信号传递时使进程实例退出方并切换 reset_flag

进程实例安装对该方的监视。当方缩小规模时,进程重置其周期性任务(如果有设置),从而触发 reset_flag 并参与新的分布式周期性任务管理领导者选举。方增长对进程来说并不重要。

由于周期性任务由于方缩小而重置,因此必须使用自定义标志,而不是 sigterm_flag,来停止周期性任务。否则,由于方更改而设置 sigterm_flag 将停止整个服务。

领导进程执行周期性任务循环。发生异常或分区时,请注意 分区问题,领导者通过翻转 sigterm_flag 来停止 inspector 服务。最终,周期性任务循环停止,因为它执行 reset_flag.wait(period) 而不是睡眠。

周期性任务管理应该发生在独立的异步线程实例中,每个周期性任务一个。由于其错误(或分区)导致失去领导者不是问题——一个新的领导者最终会被选举出来,并且可能会浪费几个周期性任务的运行(包括与领导者一起失败的任务)。

HA 周期性清理分解

清理应该实现为独立的 HA 单例周期性任务,具有可配置的时间段,分别用于内省超时和 ironic 同步任务。

内省超时周期性任务

完成正在超时的内省

  • 选择内省正在超时的节点

  • 对于每个节点

  • 将超时内省的请求放入队列,供 worker 处理

Ironic 同步周期性任务

删除不再由 Ironic 跟踪的节点

  • 选择 Inspector 保留但 Ironic 不保留的节点

  • 对于每个节点

  • 将删除节点的请求放入队列,供 worker 处理

HA 重启节流分解

作为某些硬件的解决方法,应限制重启请求速率。为此,应使用单个分布式锁实例。在任何时候,只有单个 worker 可以持有锁,同时执行重启(上电)任务。在获取锁后,重启状态转换会以可中断的方式休眠一段可配置的时间。如果休眠确实被中断,worker 应该引发异常,停止重启过程并停止 worker 本身。这种中断应该发生在优雅关机机制的一部分。这应该使用 worker 用于检查待处理关机的相同 SIGTERM 标志/事件来实现:sigterm_flag.wait(timeout=quantum)

进程分区在这里不是问题,因为所有 worker 在持有锁时都会休眠。因此,分区会通过锁过期的时间量来减慢重启速度。应该能够通过配置完全禁用重启节流。

HA 防火墙分解

PXE 启动环境已在所有 inspector 主机上配置并处于活动状态。PXE 环境的防火墙保护在所有 inspector 主机上处于活动状态,阻止主机的 PXE 服务。在任何给定时间,最多只有一个 inspector 主机的 PXE 服务可用,并且它可供所有被检查的节点使用。

构建块

通用策略是允许所有,并且每个未被检查的节点都有一个阻止该通用策略的例外。由于其大小,黑名单在所有 inspector 主机上本地维护,周期性地或异步地从 pub–sub 通道拉取项目。

正在内省的节点在单独的防火墙规则集中列入白名单。首次发现的节点由于通用的允许所有黑名单策略而穿过黑名单。

HA 防火墙应该允许访问 PXE 服务的节点,存储在分布式存储中或异步地从 pub–sub 通道获取。进程实例 worker 根据需要将防火墙规则添加到(从)分布式存储中,或在 pub–sub 通道上宣布更改。防火墙规则是 (port_ID, port_MAC) 元组,用于白名单/黑名单。

进程实例使用自定义链来实现防火墙:白名单链和黑名单链。通过白名单链失败,数据包“继续”到黑名单链。通过黑名单链失败,数据包允许访问 PXE 服务端口。如果正在内省,节点端口规则可能同时存在于白名单和黑名单链中。

HA 分解

首先,进程轮询 Ironic 以构建其黑名单链,并设置本地周期性 Ironic 黑名单同步任务或设置 pub–sub 通道上的回调。

进程实例形成一个分布式防火墙管理方,它们监视该方是否有更改。进程实例注册一个 SIGTERM 回调,当发出信号时,该进程实例将离开该方并重置防火墙,完全阻止其 PXE 服务。

在方缩小后,进程重置其白名单链、黑名单链中的 *pass* 规则以及规则集监视(如果已设置),并参与分布式防火墙管理领导者选举。方增长对进程来说并不重要。

领导者进程的黑名单链包含 *pass* 规则,而其他进程的黑名单链则不包含。选举后,领导者进程构建白名单并注册对分布式存储或白名单 pub–sub 通道回调的监视,以使白名单防火墙链保持最新。其他进程实例不维护白名单链,该链对它们来说是空的。

在任何异常(或进程实例分区)发生时,进程将其防火墙重置为完全保护其 PXE 服务。

备注

周期性白名单存储轮询和白名单 pub–sub 通道回调是相互可选的设施,用于增强防火墙的响应速度,用户可以根据需要选择启用一个或另一个或同时启用两者。黑名单 Ironic 轮询和黑名单 pub–sub 通道回调也同样适用。

为了组装 MAC 地址的黑名单,进程可能需要周期性地轮询 ironic 服务以获取节点信息。可以选择保留此信息的缓存/代理,以减少对 Ironic 的负载。

防火墙设施可能会因领导者故障而丢失,新的领导者最终会被选举出来。一些正在内省的节点可能会在等待状态下超时并导致内省失败。

周期性 Ironic-防火墙节点同步和白名单存储轮询应实现为具有可配置时间段的独立线程,0<=period<=30s,理想情况下 0<=period<=15s,以便将节点引入 ironic 与在 inspector 防火墙中将其列入黑名单的时间窗口保持在用户分辨率以下。

作为优化,实现可以考虑将节点端口的 MAC 地址规则卸载到 IP 集

HA HTTP API 分解

我们假设负载均衡器 (HAProxy) 屏蔽用户与 inspector 服务之间的关系。所有 inspector API 进程实例都应导出相同的 REST API。每个 API 请求应在单独的异步线程实例中处理(就像现在使用 Flask 框架一样)。在任何时候,任何进程实例都可以服务任何请求。

分区问题

在连接异常/worker 进程分区时,受影响的实体应在宣布失败之前重试建立连接。对于 ironic、数据库、分布式存储、锁和队列服务,应配置重试计数和超时。超时应该是可中断的,可能通过等待适当的终止/SIGTERM 标志来实现,例如 sigterm_flag.wait(timeout)。如果重试失败,受影响的实体将完全破坏 worker inspector 服务,设置标志,以避免对资源造成损害——大多数时候,其他 worker 服务实体也会受到分区的影响。用户可以在解决分区问题后考虑重新启动受影响的 worker 服务进程实例。

HTTP API 服务实例的分区不是问题,因为它们是无状态的并且通过负载均衡器访问。

备选方案

HA Worker 分解

我们简要检查了 TaskFlow 库作为替代的任务机制。目前,TaskFlow 仅支持 有向无环图作为特定步骤之间的依赖关系结构。但是,Inspector 服务必须支持重新启动特定节点的内省,从而将循环引入图形;参见 transition-function。此外,TaskFlow 不 支持将外部事件传播到正在运行的流程,例如来自裸机节点的 continue 调用。因此,必须显式维护特定节点的内省的整体状态,如果采用 TaskFlow。

HA 防火墙分解

一旦 Neutron 采用 对子网 DHCP 选项的增强允许为未知主机提供 DHCP,防火墙设施就可以被 Neutron 替换。我们保留 Inspector 的防火墙设施,供对独立部署感兴趣的用户使用。

数据模型影响

队列

引入状态转换请求项目,它应包含这些属性(作为 oslo.versioned 对象)

  • 节点 ID

  • 转换事件

引入一个清理请求项目来删除一个节点。构成请求的属性

  • 节点 ID

Pub–sub 通道

引入两个通道:防火墙白名单和黑名单。消息格式如下

  • 添加/删除

  • 端口 ID,MAC 地址

存储

在节点表中引入节点状态列。

HTTP API 影响

API 服务由专用进程提供。

客户端 (CLI) 影响

无计划。

性能和可扩展性影响

我们希望此更改为 inspector 服务带来所需的冗余和可扩展性。但是,我们预计该更改会对网络利用率产生负面影响,因为内省任务需要队列和 DLM 来进行协调。

inspector 防火墙设施需要定期轮询 ironic 服务的库存,在每个 inspector 实例中。因此,我们预计 ironic 服务的负载会增加。

防火墙领导者分区会导致选举期间的启动服务中断。因此,一些节点可能在引导过程中超时。

每次防火墙领导者更新主机防火墙节点信息时,都会从 ironic 服务轮询。这可能会导致防火墙可用性延迟。如果正在内省的节点从 ironic 服务中删除,则更改不会传播到 Inspector,直到内省完成。

安全影响

引入可能需要加固和保护的新服务

  • 负载均衡器

  • 分布式锁定设施

  • 队列

  • pub–sub 通道

部署者影响

Inspector 服务配置

  • 分布式锁定设施、队列、防火墙 pub–sub 通道和负载均衡器引入了新的配置选项,尤其是 URL/主机和凭据

  • worker 进程池大小,整数,0<size; size.default==processor.count

  • worker queue.get(timeout); 0.0s<timeout; timeout.default==3.0s

  • 清理周期 0.0s<period; period.default==30s

  • 清理内省报告过期阈值 0.0s<threshold; threshold.default==86400.0s

  • 清理内省超时阈值 0.0s<threshold<=900.0s

  • ironic 防火墙黑名单同步轮询周期 0.0s<=period<=30.0s; period.default==15.0s; period==0.0 以禁用

  • 防火墙白名单存储观察器轮询周期 0.0s<=period<=30.0s; period.default==15.0s; period==0.0 以禁用

  • 裸机重启节流,0.0s<=value; value.default==0.0s 完全禁用此功能

  • 对于 ironic 服务、数据库、分布式锁定设施和队列中的每一个,都应配置连接重试计数和连接重试超时

  • 所有 inspector 主机应共享相同的配置,仅在更新情况下除外

新服务和最小拓扑

  • 负载均衡器共享的浮动 IP 地址

  • 负载均衡器,为冗余而配置

  • WSGI HTTP API 实例 (httpd),通过负载均衡器以轮询方式寻址

  • 3 个 inspector 主机,每个主机运行一个 worker 进程实例、dnsmasq 实例和 iptables

  • 分布式同步设施主机,为冗余而配置,所有 inspector worker 都可以访问

  • 队列主机,为冗余而配置,所有 API 实例和 worker 都可以访问

  • 数据库集群,为冗余而配置,所有 API 实例和 worker 都可以访问

  • NTP 设置并配置所有服务

请注意,所有 inspector 主机都需要访问用于裸机节点启动的 PXE LAN。

可维护性考虑因素

考虑到服务更新,我们建议对每个 inspector 主机采用以下过程,一次一个

HTTP API 服务

  • 从负载均衡器服务中删除选定的主机

  • 停止主机上的 HTTP API 服务

  • 升级服务和配置文件

  • 启动主机上的 HTTP API 服务

  • 将主机注册到负载均衡器服务

Worker 服务

  • 对于每个 worker 主机

  • 停止主机上的 worker 服务实例

  • 更新 worker 服务和配置文件

  • 启动 worker 服务

关闭 inspector worker 服务可能会因 worker 线程执行长时间同步过程或在 queue.get(timeout) 方法中轮询新任务而挂起一段时间。

这种方法可能导致正在 inspector 更新主机上处理的节点内省(任务)失败。特别是转换函数的变化(新状态等)可能会导致内省错误。因此,更新应在没有正在进行的内省的情况下进行。失败的节点内省可以重新启动。

每次主机更新时,由于更新的领导者分区,可能会丢失几个周期性任务“实例”。每次更新主机时,HA 防火墙可能会丢失领导者选举期间,预计延迟应小于 10 秒,以免影响被检查节点的启动。

从非 HA Inspector 服务升级

由于非 HA 检查器服务是一个单进程实体,并且 HA 服务并未对其进行内部向后兼容(以便接管正在运行的节点检查),因此要执行升级,必须先在没有进行中的检查时停止非 HA 服务。升级前需要进行数据迁移。由于新服务需要队列和 DLM 才能运行,因此必须在升级前引入它们。工作服务必须在 HTTP API 服务之前启动。启动后,必须将 HTTP API 服务引入负载均衡器。

开发者影响

无计划。

实现

我们考虑以下实现方式,用于我们所依赖的设施

  • 负载均衡器:HAProxy

  • 队列:Oslo 消息传递

  • 发布-订阅防火墙通道:Oslo 消息传递

  • 存储:数据库服务

  • 分布式同步设施:Tooz

  • HTTP API 服务:WSGI 和 httpd

负责人

工作项

  • 用 Tooz DLM 替换当前的锁定机制

  • 引入状态机

  • 拆分 API 服务并引入调度器和队列

  • 将清理拆分为单独的超时和同步处理程序,并为这些定期过程引入领导者选举

  • 为防火墙设施引入领导者选举

  • 将发布-订阅通道引入防火墙设施

依赖项

在着陆 HA 之前,我们需要适当的检查器 grenade 测试,以便尽可能避免破坏用户。

测试

所有工作项都应作为单独的补丁进行测试,包括功能测试、单元测试以及使用 Grenade 进行的升级测试。

着陆所有必需的工作项后,应该可以专注于冗余和可扩展性来测试检查器。