Openstack Cinder源码分析-理解Taskflow

先简单介绍一下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




  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值