Openstack Cinder源码分析-理解Taskflow

本文介绍 TaskFlow 库如何提升 OpenStack 的任务管理能力,重点讲解了 TaskFlow 的核心概念、工作流类型及其实现细节,并通过 Cinder 创建卷的具体实例展示了线性工作流的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先简单介绍一下TaskFlow:

TaskFlow是OpenStack中的一个Python库,主要目的是让task(任务)执行更加容易可靠,能将轻量的任务对象组织成一个有序的流。

TaskFlow 能够控制应用程序中的长流程业务逻辑任务的暂停、重启、恢复以及回滚, 主要用于保证长流程任务执行的可靠性和一致性。主要应用场景有如 Cinder 的 create volume 这般复杂、冗长、容易失败, 却又要求保持数据与环境一致的业务逻辑.

如果在执行任务流的过程中失败了, TaskFlow 的回滚机制能够让程序流和执行环境回滚到初始状态, 并且可以重新开始执行.

总的来说, TaskFlow 非常适合于 面向任务 的应用场景.

服务停止、更新、重启
目前Openstack的大部分服务都没有对服务的强制停止做任何处理,使任务处于不可调和的状态。比如,一个任务在运行过程中被终止,可能会变为不可恢复的状态,或成为遗留资源。TaskFlow可以跟踪任务的关联状态,当服务重启后,可以很容易的恢复或者回滚。
Orphaned resources(僵尸资源)
由于现在OpenStack的项目缺乏事务语义,所以会留下一些资源成为孤儿状态,或ERROR的状态,在自动化系统(Heat)情况下,这种状况是非常不能令人接收的,因为非常难分析哪些是要被清除的孤儿资源。
Taskflow提供其以任务为导向的模型将能正确地追踪资源的变动,这就容许在一些资源上的动作可以自动地被撤销,以确定没有资源被称为“僵尸”。

Metrics and history(度量和历史)
当OpenStack服务被组织进 task 和 flow 的对象和模式时,通过将记录 task 在运行时的度量/历史,这些服务便自动获得了简单的增加度量报告和历史操作的能力。

进度/状态跟踪

Openstack中,有很多场景需要记录和跟踪任务的进度,TaskFlow提供了一种内建的通知机制实现任务进度的跟踪。

在Cinder代码中学习Taskflow

在看到cinder.volume.api.py模块中相关的create方法时,看到

try:
    # self.scheduler_rpcapi=cinder.scheduler.rpcapi.SchedulerAPI()
    sched_rpcapi = (self.scheduler_rpcapi if (not cgsnapshot and
                    not source_cg) else None)
    # self.volume_rpcapi=cinder.volume.rpcapi.VolumeAPI()
    volume_rpcapi = (self.volume_rpcapi if (not cgsnapshot and
                     not source_cg) else None)
    # 创建一个工作流flow
    # cinder.volume.flows.api.create_volume.get_flow
    flow_engine = create_volume.get_flow(self.db,
                                         self.image_service,
                                         availability_zones,
                                         create_what,
                                         sched_rpcapi,
                                         volume_rpcapi)
def get_flow(db_api, image_service_api, availability_zones, create_what,
             scheduler_rpcapi=None, volume_rpcapi=None):
    # 构造并返回api入口工作流
    """Constructs and returns the api entrypoint flow.
 
    This flow will do the following:
 
    1. Inject keys & values for dependent tasks.
    2. Extracts and validates the input keys & values.
    3. Reserves the quota (reverts quota on any failures).
    4. Creates the database entry.
    5. Commits the quota.
    6. Casts to volume manager or scheduler for further processing.
    1. 对各自独立的任务插入键值
    2. 提取并校验输入的键值
    3. 保存配额
    4. 创建数据库入口
    5. 提交配额
    6. 转交至volume manager或者scheduler 以继续执行程序
    """
 
    flow_name = ACTION.replace(":", "_") + "_api"
    # linear_flow线性执行工作流
    api_flow = linear_flow.Flow(flow_name)
 
    api_flow.add(ExtractVolumeRequestTask(
        image_service_api,
        availability_zones,
        rebind={'size': 'raw_size',
                'availability_zone': 'raw_availability_zone',
                'volume_type': 'raw_volume_type'})
    # 连续添加了三个任务
    # 每个execute的返回结果作为revert方法中的result参数
    api_flow.add(QuotaReserveTask(),
                 EntryCreateTask(db_api),
                 QuotaCommitTask())
 
    if scheduler_rpcapi and volume_rpcapi:
        # This will cast it out to either the scheduler or volume manager via
        # the rpc apis provided.
        api_flow.add(VolumeCastTask(scheduler_rpcapi, volume_rpcapi, db_api))
 
    # Now load (but do not run) the flow using the provided initial data.
    # 载入工作流
    return taskflow.engines.load(api_flow, store=create_what)
先不看各种Task的具体实现,这里主要讨论在get_flow函数中Taskflow的流程。

api_flow = linear_flow.Flow(flow_name)

表示此处执行的是线性工作流,taskflow支持多种工作流:

  • 线性:运行一个任务或流的列表,是一个接一个串行方式运行。
  • 无序:运行一个任务或流的列表,以并行的方式运行,顺序与列表顺序无关,任务之间不存在依赖关系。
  • 图:运行一个图标(组节点和边缘节点)之间组成的任务/流依赖驱动的顺序。
通过例子来看,可以很知道线性是什么概念

#!/usr/bin/python
# coding=utf-8

from __future__ import print_function
import taskflow.engines
from taskflow.patterns import linear_flow as lf
from taskflow import task

class A(task.Task):

    default_provides=set({'b_msg':'b'})
    @staticmethod
    def _execute(a_msg):
        print('A: {}'.format(a_msg))

    def execute(self, a_msg):
        self._execute(a_msg)
        b = 'b'
        return {'b_msg': b,}

class B(task.Task):

    def execute(self, b_msg, *args, **kwargs):
        print('B :{}'.format(b_msg))

flow = lf.Flow('simple-linear-test').add(
    A(),
    B())

engine = taskflow.engines.load(flow, store={'a_msg':'a',})

engine.run()
看输出

A: a
B :b

我们结合上述的例子和cinder中的代码可以知道:

  1. 每一个task(A()或者B()),需要继承自task.Task,称为一个Atom,cinder中的各种xxxTask最终也是继承自task.Task;
  2. 当生成了一个(线性)线性工作流后,需要调用add方法,添加Task;在线性工作流当中,执行的顺序取决于添加的顺序
  3. 每一个task中定义的execute方法将会在最终run()的时候被执行
  4. 调用engines.load方法生成一个engine对象,同时,store参数传入执行execute方法需要的参数,以字典的形式传入
  5. engine需要执行run方法才能执行工作流

在get_flow()中,store参数载入的是create_what参数,create_what由cinder.volume.api.py中的相关方法传入,也是一个字典

create_what = {
    'context': context,
    'raw_size': size,
    'name': name,
    'description': description,
    'snapshot': snapshot,
    'image_id': image_id,
    'raw_volume_type': volume_type,
    'metadata': metadata or {},
    'raw_availability_zone': availability_zone,
    'source_volume': source_volume,
    'scheduler_hints': scheduler_hints,
    'key_manager': self.key_manager,
    'source_replica': source_replica,
    'optional_args': {'is_quota_committed': False},
    'consistencygroup': consistencygroup,
    'cgsnapshot': cgsnapshot,
    'multiattach': multiattach,
}

分析get_flow()函数中的Task

先看工作流添加的第一个参数

api_flow.add(ExtractVolumeRequestTask(
    image_service_api,
    availability_zones,
    rebind={'size': 'raw_size',
            'availability_zone': 'raw_availability_zone',
            'volume_type': 'raw_volume_type'})

rebind的意思是通过设置rebind属性,能够在传入参数的时候,以A传入,最终在执行task的时候,对应的是B,即我们可以通过store传入raw_size的指定值,而最终传入execute的值是通过size对应的值,做了一个转换,类似变量绑定。

这里首先添加了ExtractVolumeRequestTask

class ExtractVolumeRequestTask(flow_utils.CinderTask):
    default_provides = set(['availability_zone', 'size', 'snapshot_id',
                        'source_volid', 'volume_type', 'volume_type_id',
                        'encryption_key_id', 'source_replicaid',
                        'consistencygroup_id', 'cgsnapshot_id',
                        'qos_specs'])
 
    def __init__(self, image_service, availability_zones, **kwargs):
        super(ExtractVolumeRequestTask, self).__init__(addons=[ACTION],
                                                   **kwargs)
        self.image_service = image_service
        self.availability_zones = availability_zones
    def execute(self, context, size, snapshot, image_id, source_volume,
                availability_zone, volume_type, metadata, key_manager,
                source_replica, consistencygroup, cgsnapshot):
    ...

上述代码并不完全,一些私有方法并没有列举出来,这里主要是要为了引入到default_provides这个概念

default_provides可以看作是一个task的输出,也就是可以被下一个task(Atom)所引用,有固定的格式,必需是字符串,集合,或者列表/元组的数据结构

第一个demo已经用到了default_provides,再通过一个例子来看看default_provides,这里的例子主要参考官方文档的图,我采用线性工作流补全了一下

#!/usr/bin/python
# coding=utf-8

import taskflow.engines
from taskflow.patterns import linear_flow as lf
from taskflow import task

class CallOnPhone(task.Task):

    default_provides='was_dialed'
    def execute(self, phone_number):
        print "Calling %s" % phone_number
        return True

    def revert(self, phone_number, result, flow_failures):
        if result:
            print "Hanging up on %s" % phone_number

class ChitChat(task.Task):

    def execute(self, was_dialed, phone_number):
        if was_dialed:
            print "Talking with %s" % phone_number

flow = lf.Flow('Call').add(
    CallOnPhone(),
    ChitChat())

engine = taskflow.engines.load(flow, store={'phone_number':'2222',})

engine.run()
Calling 2222
Talking with 2222
可以看到,我们只通过store传入了"phone_number'这个参数,但在ChitChat这个task中,可以用到CallOnPhone提供的默认输出'was_disled'


除了execute方法,还有一个revert方法,这就是工作流强大的地方,当某一个任务出现失败时,Taskflow能够有序的回滚,我们看一个demo

#!/usr/bin/env python
#filename: tasks.py
 
import taskflow.engines
from taskflow.patterns import linear_flow as lt
from taskflow import task
from taskflow.types import failure as task_failed
 
 
class CallJim(task.Task):
 
    default_provides = set(['jim_new_number'])
 
    def execute(self, jim_number, *args, **kwargs):
        print "Calling Jim %s." % jim_number
        print '=' * 10
        jim_new_number = jim_number + 'new'
 
        return {'jim_new_number': jim_new_number}
 
    def revert(self, result, *args, **kwargs):
        if isinstance(result, task_failed.Failure):
            print "jim result"
            return None
 
        jim_new_number = result['jim_new_number']
        print "Calling jim %s and apologizing." % jim_new_number
 
 
class CallJoe(task.Task):
 
    default_provides = set(['joe_new_number', 'jim_new_number'])
 
    def execute(self, joe_number, jim_new_number, *args, **kwargs):
        print "Calling jim %s." % jim_new_number
        print "Calling Joe %s." % joe_number
        print '=' * 10
        joe_new_number = joe_number + 'new'
 
        return {'jim_new_number': jim_new_number,
                'joe_new_number': joe_new_number}
 
    def revert(self, result, *args, **kwargs):
        if isinstance(result, task_failed.Failure):
            print "joe result"
            return None
 
        jim_new_number = result['jim_new_number']
        joe_new_number = result['joe_new_number']
 
        print "Calling joe %s and apologizing." % joe_new_number
 
 
class CallJmilkFan(task.Task):
 
    default_provides = set(['new_numbers'])
 
    def execute(self, jim_new_number, joe_new_number, jmilkfan_number,
                *args, **kwargs):
        print "Calling jim %s" % jim_new_number
        print "Calling joe %s" % joe_new_number
        print "Calling jmilkfan %s" % jmilkfan_number
        print '=' * 10
        jmilkfan_new_number = jmilkfan_number + 'new'
 
        raise ValueError('Error')
        new_numbers =  {'jim_new_number': jim_new_number,
                        'joe_new_number': joe_new_number,
                        'jmilkfan_new_number': jmilkfan_new_number}
 
        return {'new_numbers': new_numbers}
 
    def revert(self, result, *args, **kwargs):
        if isinstance(result, task_failed.Failure):
            print "jmilkfan result"
            return None
 
        jim_new_number = result['jim_new_number']
        joe_new_number = result['joe_new_number']
        jmilkfan_new_number = result['jmilkfan_new_number']
 
        print "Calling jmilkfan %s and apologizing." % jmilkfan_new_number
 
 
def get_flow(flow, numbers):
    flow_name = flow
    flow_api = lt.Flow(flow_name)
 
    flow_api.add(CallJim(),
                 CallJoe(),
                 CallJmilkFan())
 
    return taskflow.engines.load(flow_api,
                                 engine_conf={'engine': 'serial'},
                                 store=numbers)
 
def main():
    numbers = {'jim_number': '1'*6,
               'joe_number': '2'*6,
               'jmilkfan_number': '3'*6}
    try:
        flow_engine = get_flow(flow='taskflow-demo',
                               numbers=numbers)
 
        flow_engine.run()
    except Exception:
        print "TaskFlow Failed!"
        raise
 
    new_numbers = flow_engine.storage.fetch('new_numbers')
 
 
if __name__ == '__main__':
    main()
输出结果:

#OUTPUT
Calling Jim 111111.
==========
Calling jim 111111new.
Calling Joe 222222.
==========
Calling jim 111111new
Calling joe 222222new
Calling jmilkfan 333333
==========
jmilkfan result
Calling joe 222222new and apologizing.
Calling jim 111111new and apologizing.
TaskFlow Failed!
Traceback (most recent call last):
  File "tasks.py", line 114, in <module>
    main()
  File "tasks.py", line 105, in main
    flow_engine.run()
  File "/usr/local/lib/python2.7/dist-packages/taskflow/engines/action_engine/engine.py", line 159, in run
    for _state in self.run_iter():
  File "/usr/local/lib/python2.7/dist-packages/taskflow/engines/action_engine/engine.py", line 223, in run_iter
    failure.Failure.reraise_if_any(it)
  File "/usr/local/lib/python2.7/dist-packages/taskflow/types/failure.py", line 292, in reraise_if_any
    failures[0].reraise()
  File "/usr/local/lib/python2.7/dist-packages/taskflow/types/failure.py", line 299, in reraise
    six.reraise(*self._exc_info)
  File "/usr/local/lib/python2.7/dist-packages/taskflow/engines/action_engine/executor.py", line 82, in _execute_task
    result = task.execute(**arguments)
  File "tasks.py", line 66, in execute
    raise ValueError('Error')
ValueError: Error

这样,在执行卷创建这些过程中,如果有某个步骤失败了,可以通过revert恢复诸如QUATO等,保证数据的一致性

总结

  • 在 function get_flow 中使用 linear_flow.Flow 生成一个 TaskFlow(线性任务流) 对象 flow_api , 再通过flow_api.add method 添加要 顺序执行且倒序回滚 的 Task class(CallJim/CallJom/CallJmilkFan).
  • 使用 taskflow.engines.load method 来加载 TaskFlow(flow_api)对象/后台存储数据(store)/ engine配置 等信息并生成 Task Engine 对象
  • 最后调用 Task Engine 对象的 flow_engine.run method 来开始执行该任务流
  • 后台存储 store 的数据在该任务流中被所有 Task class 共享, 并且以 Task class 中的 execute method 的形参作为对接入口. e.g. 上述实现的 store 后台存储中含有 {jim_number: '1'*6}, 那么 CallJim 的 execute method 就可以通过形参 jim_number 来获取 '1'*6 的值.
  • Task class 的属性 default_provides 用于声明在执行过程中新添到后台存储的元素的名称, 其相应的值会自动的从 execute method 返回值中匹配获取, 最终存储后台存储. e.g. CallJim 的属性 default_provides = set(['jim_new_number']) 其中 jim_new_number 的值会从 execute method 的返回 return {'jim_new_number': jim_new_number} 中获取.(重要)
  •  provides 的实现能够有效的帮助传递 Task class 之间在执行时产生的新属性对象. 将上一个 Task 的结果传递给后一个 Task 使用

最后

Taskflow还有很多用法,包括其他形式的工作流,以及对task状态的监督,包括很有用的Retry用法等等;这里只是顺着cinder中的这块代码进行用法的探讨,以后如果在源码学习中遇到新的用法,再来补充吧~

参考文章

Taskflow 官方文档

OpenStack Taskflow介绍

OpenStack 通用技术之Taskflow




### 关于OpenStack Cinder备份服务文档和故障排除 #### Cinder Backup Service概述 Cinder作为OpenStack的一部分,提供了块存储管理功能。为了保护数据免受意外丢失的影响,Cinder支持创建卷快照以及将这些快照备份到外部位置的功能[^2]。 #### 配置Backup Backend 要配置Cinder的备份后端,在`cinder.conf`文件中定义相应的参数是非常重要的。通常这涉及到指定用于存储备份的目标系统(如Swift或其他兼容的对象存储解决方案),并设置必要的认证信息以便访问该目标系统。例如: ```ini [DEFAULT] backup_driver = cinder.backup.drivers.swift swift_container = backups swift_object_size = 52428800 swift_retry_attempts = 10 swift_retry_backoff = 20 ``` 上述配置片段展示了如何指向一个名为backups的容器来保存所有的备份副本,并设置了每次上传对象的最大尺寸以及其他重试策略的相关选项。 #### 创建与恢复备份 通过命令行工具可以轻松地执行创建新备份或将现有备份还原的操作。下面是一些常用的CLI指令示例: - **创建一个新的备份** ```bash openstack volume backup create --name my_backup <volume_id> ``` - **查看当前可用的备份列表** ```bash openstack volume backup list ``` - **从特定ID的备份中恢复数据** ```bash openstack volume backup restore <backup_id> <destination_volume_id> ``` 以上命令允许管理员高效管理和操作云环境中各个磁盘卷上的重要资料。 #### 故障排查技巧 当遇到问题时,检查日志是最基本也是最有效的方法之一。对于Cinder而言,默认情况下其日志会被记录在`/var/log/cinder/`目录下。如果发现任何异常情况,则应该仔细审查相关组件的日志文件以获取更多线索;另外也可以尝试提高调试级别从而获得更详细的输出帮助定位错误根源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值