跳转到: 导航, 搜索

结构化状态管理细节

起草人: Harlowja

修订日期:2013年5月25日,Harlowja

所需概念

为了实现这个新的状态管理层,以下关键概念必须从一开始就融入到设计中。

  1. 原子任务单元。
  2. 原子任务单元组合成工作流。
  3. 任务跟踪和恢复。
  4. 资源锁定和单一工作流所有权。
  5. 最小影响(允许此更改合并)。
  6. 高可用性。
  7. 容错升级。
  8. 任务取消(锦上添花

原子任务单元

为什么重要

通过代码或其他操作创建的任务必须是原子的,这样才能将任务作为一个单元视为已应用/完成,或者将任务作为一个单元视为失败(或已取消)。这允许将所述任务作为一个单元回滚。通常,由于工作流(例如,由任务 [1, 2, 3] 组成)可能在任何阶段失败,例如阶段 3,因此需要一个明确的路径来回滚 2 和 1(按该顺序)。这种任务回滚概念对于干净、正确地撤消在失败后发生的操作(或者由于取消,取消不一定是由传统意义上的失败引起的)是必需的。

如何解决

先前位于 nova 的 run_instance 路径(以及最终其他路径)中的状态和状态转换被重构为明确定义的任务(具有如下所示的 apply() 方法和 rollback() 方法)。这些任务被拆分,以便每个任务以原子方式执行明确定义且易于理解的单个工作单元(即,不是一个执行许多不同操作的大型任务),尽可能地。

注意:这种技术还有助于简化所述任务的测试(因为它将具有一组清晰的输入和一组清晰的预期输出/副作用,rollback() 方法应撤消这些副作用),如果没有这种任务重构工作,这是不可能的。

注意:如果下游服务(例如 quantum 和 quantum agent 以异步方式工作,无法取消),则回滚将更难正确完成。看起来大多数下游服务,如 libvirt、nova-network 等都提供 acquire/release 语义(或类似的机制),这是正确执行回滚(以及关联的正确取消)所必需的。

原子任务组合成工作流

为什么重要

nova 中的工作流通常可以组织成一系列较小的任务/状态,这些任务/状态可以作为一个单元应用(并作为一个单元回滚)。由于我们需要能够以可跟踪的方式运行单个任务(并且不应在多个地方复制此代码),因此需要一个任务链的概念(对于大多数线性 nova 情况),以便能够以易于使用的方式运行和回滚该任务集(这也有助于使代码更易于阅读!)。

注意:从线性任务链开始将满足许多典型的工作流操作。将来,可能需要更大的工作流“模式”集来适应其他用例。

任务跟踪与恢复

为什么重要

实现状态引擎 单元 横向扩展的关键是能够在一个状态引擎单元上恢复另一个状态引擎单元失败的工作。这使得这些单元能够在单个程序失败的情况下幸存下来,而不会将它们正在处理的任务置于ERROR状态,这在面向事务的工作流系统中是不需要的。这种恢复及其相关的跟踪还提供了一个有用的附加功能,即可以轻松地审计给定资源/请求在其生命周期内经历的任务和工作流。这具有许多不同的用途,并可能为以后创建新的有趣功能。

如何解决

首先需要创建一个任务日志,并在执行每个任务时相应地更新该日志。请注意,仅存储已完成任务的名称是不够的,还必须存储足够的元数据才能正确地撤消该任务。通常,在 nova 中,有一个概念,即每个任务都在修改的资源,在每个任务结束时,我们可以存储该资源(这将自动获取该资源的修改)。此任务日志和关联的元数据可以以不同的方式存储。例如,可以使用数据库,或者可以使用不太持久的方式,因为一旦工作流完成,可能不需要保留任务日志,当然,数据库(或类似持久的数据库,例如 zookeeper 持久节点)将允许进行工作流审计,但对于那些不关心审计的人,可以使用像 redis 这样的东西,它具有复杂的对象非持久的基于过期的时间存储。

问题:admin_password 条目是此资源对象的一部分,如果我们在任务日志元数据中存储它,则需要安全地存储它。如果使用 zookeeper,则可能需要查看其 acl 管理系统的集成(以避免未经授权的方读取所述资源)。如果需要,可能还需要解决并实现 SSL(参见 [1])。

恢复:为了实现恢复,需要一个工作流所有权的概念,以及一种方法来注意到当一个状态引擎单元失败时(生存能力)并能够释放该工作流的所有权,以便其他引擎单元可以接管所述所有权。这两个问题都可以通过 zookeeper 轻松解决,zookeeper 是一个久经考验的程序,专为所有权和触发任务而设计,工作流所有权和释放所有权是其中的典型示例。理解这一点的方法是注意到工作流的所有权是对象上的 (该对象是工作流或该工作流的预留),zookeeper 能够通过 watches 的概念通知其他方何时丢失了所述锁(由于失败或取消),这两个一般概念允许以分布式和高度可扩展的方式进行恢复。Zookeeper 甚至提供了一些可能有助于实现此目的的配方,请参阅 kazoo 配方 源代码中的一些示例(锁定队列概念非常非常有用)。

示例代码: https://github.com/harlowja/zkplayground

注意:使用纯数据库(例如 mysql)正确实现这些语义可能非常困难(或者需要进行大量的跳跃),因为数据库不提供监视/触发或分布式锁定(尤其是在复制、分片或集群时)。如果没有这些语义,很难(或干净地)实现自动恢复功能,而无需某种周期性表扫描任务(这本身并不能解决分布式锁定获取/释放语义)。

资源锁定和单一工作流所有权

为什么重要

为了确保一致的资源状态和一致的工作流状态,首先我们需要能够锁定我们正在处理的资源(例如实例),并锁定正在处理的工作流。这确保了只有单个实体才能同时处理所述工作流和资源,这将避免状态和资源不一致以及竞争条件。

参见: 结构化工作流锁

高可用性

为什么重要

处理给定工作流的实体失败时,整个工作流不应被置于ERROR状态(nova 目前很常见)。相反,所述处理工作流的实体应变得高度可用,以便即使单个实体失败,另一个(或 N 个其他实体中的一个)实体也能够声明未完成的工作流并从失败的位置继续。由于工作流由这些 HA 实体处理,因此可以确保正在处理的工作流也是高度可用的,并且可以容忍单个实体失败,从而提高规模、可靠性、稳定性并减少所需的支持请求和手动干预,以将工作流从ERROR状态恢复到恢复状态。

如何解决

Zookeeper 允许我们注意到处理工作流的实体何时失败(由于其心跳的概念)。这将解决检测失败的实体的问题,并将导致释放所述实体获取的锁。现在的问题是如何将工作流及其关联的任务转移到另一个未失败的实体。一种方法是使用 zookeeper 的 watch 概念,其他 2 个或更多实体可以在工作流“节点”上设置该 watch。这些实体将在失败的实体丢失所述获取时尝试获取该工作流“节点”。可以有一个根目录节点,该节点将接收这些子节点,并且当新项目出现在该根目录节点中时,其他 2+ 实体将设置对所述新节点的监视。这是一种潜在的解决方案。

另一种方法是使用 zookeeper 中已经存在的锁定队列的概念(参见 代码),其中 zookeeper 中的单个路径/目录可以设置为接收工作项(请注意,这里我们可以为例如,不同的作业类型)。然后,与触发监视和选择获取实体相关的工作由 zookeeper 本身完成(类似于上述过程)。

第三种方法是提供一种将没有实体处理的工作流转移到 zookeeper 中的“孤立”文件夹的方法,每个状态引擎可以具有一个定期任务来扫描此文件夹,或者可以具有可以由新项目自动触发的监视,该新项目移动到该文件夹中,这将触发所有权获取过程。

容错升级

为什么重要

目前,运行 OpenStack 的云中的升级过程充满麻烦。在所述升级过程中,API 被关闭,并且正在进行的事务通常会被丢弃(即,MQ 被重置),并且任何正在进行的事务需要手动清理或需要等待周期性任务来尝试修复(这可能或不可能正确完成,因为通常不可能仅通过本地知识来清理所有内容)。这通常涉及很多痛苦,因为正在进行的实例最终处于ERROR状态,资源通常被孤立(如果未被定期任务本地清理),并且必须手动回收,并且最终用户现在必须重新调用 API 以终止其损坏的实例,这会导致糟糕的最终用户体验。

如何解决

解决正在进行的事务被丢弃的第一步是使正在进行的事务可恢复。这通过上述恢复策略解决,以便即使 MQ 或 zookeeper 被关闭(由于计划内或计划外的原因),也可以参考与所述工作流关联的任务日志并从中恢复。这将解决当前操作和资源被放弃、孤立或置于ERROR状态的情况。如果这不可行,则持久的(或至少临时持久的 - 即持久到工作流完成)任务日志也可以参考,以确定需要执行哪些手动操作/清理来解决集群的状态,现在很难在没有大量代码知识的情况下做到这一点。

这里遇到的第二个问题是,如何处理升级期间发生的流程修改,例如,如果升级期间删除了流程的某个部分,或者添加了新的任务。可以通过始终保持 N-1 版本流程兼容性来解决这个问题,这将确保在旧版本中启动的流程可以在新版本中完成。但这会产生一些副作用,并且可能意味着我们需要至少为每个 [任务、流程、任务日志] 标记一个版本,以便在恢复流程时选择该版本。这意味着对所述任务日志的回滚必须适应 N-1 流程和任务产生的 N-1 个操作。

另一种方法是为 nova 定义一种维护模式,在这种模式下,API 仍然可以运行,但不会接受新的操作,直到取消所述维护模式。这将允许完成(或取消)所有正在进行的任务,从而排空所有正在进行的流程,这些流程之前曾导致问题。然后可以安装新软件并关闭维护模式。后一种方法不需要 N-1 流程兼容性,但涉及在升级过程中更多的操作开销。

取消

可能不需要一开始就考虑,但应该有可能/思考。

为什么重要

流程在任何时候都应该能够被用户或管理员/运维人员发起取消。目前在 nova 中,你可以看到代码尝试处理(但并非总是正确地处理)一个次要操作,该操作在所述第一个操作仍在进行时取消第一个操作,这部分借助了文件级锁的使用(这在分布式锁系统中将无法工作),以及反复检查的理念。似乎与其拥有这些类型的反复和临时检查来确保同时的操作不会中断正在进行的操作(即使使用 eventlet 也可能发生),不如支持正在进行的流程的取消,并通过上述锁定策略禁止对所述流程的并发操作,以避免抢占发生。

如何解决

为了解决对给定资源的同时访问和操作问题,这将通过必须存在的分布式锁概念来解决,这将避免使用文件级锁来避免相同的问题(当外部实体正在处理所述资源时,这是不可能的)。取消的第二个问题可以通过在每个任务之间添加检查来解决,以检查整个流程是否已被取消。由于状态管理实体将运行构成流程的完整任务集,因此它可以是唯一负责检查此“标志”并启动适当的回滚序列的实体。这允许不再需要存在对同时操作问题的临时检查。

注意: 这种级别的取消可以细化到所述任务的粒度,并允许通过多种方式触发此取消(可能通过添加一个 zookeeper 取消节点,或者在任务日志中写入一个标志),从而触发流程回滚(或只是告诉流程停止)。这可以允许高级功能,例如流程“暂停”(尽管这是一个不同的问题,因为不清楚在暂停时如何处理资源和锁),以及随后由外部发起过程触发所述流程的恢复。

最小影响

为什么重要

为了提交任何代码,我们必须在每次审查/提交时具有最小的影响(但仍然对更大的图景有清晰的对齐视图)。

可能发生的问题类型是

  • 路径文档/误解。
  • 一次性更改过多,难以审查(即大型提交)。
  • 对更改试图做什么缺乏协调或误解。
  • 在其他人正在处理所述路径时,对正在更改的路径产生冲突。
  • 单元测试需要重构(并且非常可能需要添加)以确保基础更改不会破坏现有的部署。

如何解决

路径文档/误解

在更改代码路径之前,我们需要深入了解和记录当前的流程,并提出如何将所述流程重构为新模型。这将作为非常有用的参考文档,并提高对当前存在的内容以及将要/可能存在的内容来替代现有模型的认识(以及相关的讨论)。

参见: StructuredWorkflows

一次性更改过多,难以审查(即大型提交)

这将通过工作和计划如何划分和组织所做的更改来解决,以便进行审查和理解(从而不会给代码审查员带来过多的负担)。我相信这是一个过程和划分问题,虽然它可能很痛苦,但似乎可以解决/管理。

对更改试图做什么缺乏协调或误解

这将通过确保我们对正在进行更改(无论是在微观层面还是宏观层面)以及完成所述更改后将带来的好处保持开放(通过像这样文档和讨论)来解决。需要跨项目协调,这将涉及每个项目的负责人以及执行这项工作的开发人员(和其他人)对正在更改的内容以及他们计划如何通过清晰的文档和沟通来实现宏观目标保持非常开放(即使那些不受所述更改直接影响的人)。

在其他人正在处理所述路径时,对正在更改的路径产生冲突

应尝试避免在重构其他人同时正在处理的路径时发生冲突。我们应该利用之前的协调来尽可能避免这种类型的冲突,如果发生冲突,则应根据具体情况重新处理所述重构以适应所述冲突。

单元测试

为了避免在所述基础更改期间破坏代码库的稳定性,我们需要确保每当我们更改路径时,确保当前的单元测试和集成测试继续工作(如果所述单元测试/集成测试在所述更改后仍然有意义),并且我们需要为几乎没有单元测试的路径添加新的单元测试(这可能在所述重构之后更容易实现)。