Openstack Cinder源码分析-理解Taskflow

TaskFlow 在 OpenStack 中的应用
本文介绍 TaskFlow 库如何提升 OpenStack 的任务管理能力,重点讲解了 TaskFlow 的核心概念、工作流类型及其实现细节,并通过 Cinder 创建卷的具体实例展示了线性工作流的应用。
先简单介绍一下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




<think>嗯,用户的问题是解决OpenStack Cinder版本不一致导致volume服务无法启动的问题,尤其是在controller和compute节点上通过yum安装时出现的版本差异。首先,我需要回忆一下OpenStack各组件之间的版本兼容性要求。通常,OpenStack的各个组件(如Nova、Cinder、Glance等)需要保持版本一致,尤其是在同一个发行版中,比如Queens、Rocky等。如果Cinder的版本在controller和compute节点上不同,可能会导致API不兼容,服务无法正常通信。 接下来,参考用户提供的引用内容,比如引用[2]提到了安装nova相关的软件包,引用[3]提到了Cinder与Ceph的区别以及对接方法,但用户当前的问题是关于Cinder版本不一致。用户还提到yum安装版本差异,可能需要检查yum仓库中的版本是否一致。 首先,解决方案的第一步应该是确认各节点上的Cinder版本是否一致。可以通过在controller和compute节点上执行`cinder --version`或者查看已安装的RPM包版本。如果版本不一致,需要统一版本。这可能涉及到卸载旧版本,添加正确的yum仓库,然后安装指定版本。 然后,检查yum仓库配置,确保所有节点使用相同的OpenStack发行版仓库。例如,如果使用Queens版本,那么所有节点的yum源都应指向Queens的仓库。如果仓库配置不同,可能导致安装的软件包版本不一致。 另外,用户提到volume服务无法启动,可能需要查看日志文件,通常是/var/log/cinder/volume.log,来获取具体的错误信息。日志中可能会提示缺少依赖、配置错误或API版本不匹配等问题。例如,引用[1]中提到的libguestfs-winsupport缺失的问题,虽然属于Nova的BUG,但也提示了依赖包的重要性,可能需要检查Cinder的相关依赖是否安装。 此外,配置文件的检查也很重要。确保所有节点上的/etc/cinder/cinder.conf中的配置参数,如transport_url、auth_url、project_name等,指向正确的服务版本和端点。如果版本不一致,这些配置可能需要调整以适应不同版本的API。 在升级或降级Cinder版本后,需要运行数据库迁移命令,如`cinder-manage db sync`,以确保数据库模式与当前版本兼容。同时,重启所有相关的Cinder服务,如volume、scheduler、api等,并检查服务状态。 用户可能遇到的问题还包括依赖包冲突。例如,在安装特定版本的Cinder时,可能需要其他组件(如Nova、Glance)也保持兼容版本。引用[2]和[3]中提到的安装其他组件的方法,可能需要在解决依赖时参考。 另外,如果使用了Ceph等存储后端,版本不一致可能导致驱动不兼容。需要确认Cinder volume使用的存储驱动是否支持当前版本,并检查相关配置是否正确。 最后,建议用户在进行版本调整后,进行基本的功能测试,如创建、挂载卷,确保volume服务正常工作。同时,考虑后续的版本管理策略,避免再次出现版本不一致的问题。</think>### OpenStack Cinder版本不一致导致Volume服务无法启动的解决方案 #### 1. 确认版本差异 首先通过以下命令检查所有节点上的Cinder版本: ```bash cinder --version # 查看Cinder客户端版本 rpm -qa | grep openstack-cinder # 查看已安装的RPM包版本 ``` 如果版本差异超过一个主版本(如Rocky与Stein),需强制统一版本。若差异在一个主版本内(如Queens的多个小版本),可通过配置兼容性参数临时解决[^3]。 --- #### 2. 统一软件版本 **步骤1:配置统一Yum源** 在所有节点创建相同版本的repo文件: ```bash # /etc/yum.repos.d/openstack-queens.repo [openstack-queens] name=OpenStack Queens baseurl=http://mirror.centos.org/centos/7/cloud/x86_64/openstack-queens/ enabled=1 gpgcheck=0 ``` **步骤2:强制指定版本安装** ```bash # 卸载旧版本 yum remove openstack-cinder -y # 安装指定版本(示例为Queens) yum install openstack-cinder-1:12.0.0-1.el7 -y [^2] ``` --- #### 3. 关键配置检查 在`/etc/cinder/cinder.conf`中确保以下参数跨版本兼容: ```ini [DEFAULT] transport_url = rabbit://openstack:密码@controller # 消息队列地址必须一致 enable_v3_api = true # 当跨主版本时必须显式声明[^3] [keystone_authtoken] auth_url = http://controller:5000/v3 # 必须使用相同API版本 project_name = service ``` --- #### 4. 数据库迁移 在控制节点执行数据库同步: ```bash cinder-manage db sync # 强制同步到当前版本数据库结构 ``` --- #### 5. 服务重启与验证 ```bash systemctl restart openstack-cinder-volume # 计算节点 systemctl restart openstack-cinder-scheduler openstack-cinder-api # 控制节点 # 检查服务状态 cinder service-list # 应显示所有服务为"up" ``` --- #### 6. 特殊情况处理 若出现类似`UnboundLocalError`的底层错误(参考[^1]),需检查依赖库: ```bash yum install libguestfs-winsupport -y # 安装虚拟化支持库 ``` --- #### 版本兼容参考表 | 组件 | Queens (12.x) | Rocky (13.x) | Stein (14.x) | |------------|---------------|--------------|--------------| | Nova | 17.x | 18.x | 19.x | | Ceph | Luminous | Mimic | Nautilus | | Python SDK | ≤1.8.1 | ≤2.1.0 | ≥3.0.0 | [^1]: 虚拟化依赖缺失导致的异常 [^2]: OpenStack组件安装方法 [^3]: Cinder与存储后端的版本关联性 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值