感谢朋友支持本博客,欢迎共同探讨交流,由于能力和时间有限,错误之处在所难免,欢迎指正!
如果转载,请保留作者信息。
博客地址:http://blog.csdn.net/gaoxingnengjisuan
邮箱地址:dong.liu@siat.ac.cn
在这篇博客中,我将详细的分析task类VolumeCastTask具体是如何来实现根据请求信息进行卷的建立的。
我们来回顾类VolumeCastTask的源码实现:
class VolumeCastTask(base.CinderTask):
"""
远程调用实现卷的建立操作;
"""
def __init__(self, scheduler_rpcapi, volume_rpcapi, db):
super(VolumeCastTask, self).__init__(addons=[ACTION])
self.volume_rpcapi = volume_rpcapi
self.scheduler_rpcapi = scheduler_rpcapi
self.db = db
self.requires.update(['image_id', 'scheduler_hints', 'snapshot_id',
'source_volid', 'volume_id', 'volume_type',
'volume_properties'])
def __call__(self, context, **kwargs):
"""
远程调用实现卷的建立操作;
context = <cinder.context.RequestContext object at 0x4337e10>
kwargs = {'volume_properties': {'status': 'creating',
'volume_type_id': None,
'user_id': u'ef073287176048bd861dcd9d9c4d9808',
'availability_zone': 'nova',
'reservations': ['4d53f59d-f1ca-4a62-a6ff-7b4609bd6512',
'513f29df-26b2-49d3-bd96-951623e4d20f'],
'volume_admin_metadata': [],
'attach_status': 'detached',
'display_description': None,
'volume_metadata': [],
'metadata': {},
'encryption_key_id': None,
'source_volid': None,
'snapshot_id': None,
'display_name': u'shinian01',
'project_id': u'6c3c74779a614d3b81dd75518824e25c',
'id': '3e68f81a-3e9e-45f6-8332-55c31a3195dc',
'size': 1},
'source_volid': None,
'image_id': None,
'snapshot_id': None,
'volume_type': {},
'volume_id': '3e68f81a-3e9e-45f6-8332-55c31a3195dc',
'scheduler_hints': None}
"""
scheduler_hints = kwargs.pop('scheduler_hints', None)
request_spec = kwargs.copy()
filter_properties = {}
if scheduler_hints:
filter_properties['scheduler_hints'] = scheduler_hints
# filter_properties = {}
# _cast_create_volume:远程调用建立卷的方法,实现卷的建立操作;
self._cast_create_volume(context, request_spec, filter_properties)
进一步来看方法_cast_create_volume的源码实现:
def _cast_create_volume(self, context, request_spec, filter_properties):
"""
远程调用建立卷的方法,实现卷的建立操作;
request_spec = {'image_id': None,
'volume_type': {},
'volume_id': '3e68f81a-3e9e-45f6-8332-55c31a3195dc',
'snapshot_id': None,
'volume_properties': {'status': 'creating',
'volume_type_id': None,
'user_id': u'ef073287176048bd861dcd9d9c4d9808',
'availability_zone': 'nova',
'reservations': ['4d53f59d-f1ca-4a62-a6ff-7b4609bd6512',
'513f29df-26b2-49d3-bd96-951623e4d20f'],
'volume_admin_metadata': [],
'attach_status': 'detached',
'display_description': None,
'volume_metadata': [],
'metadata': {},
'encryption_key_id': None,
'source_volid': None,
'snapshot_id': None,
'display_name': u'shinian01',
'project_id': u'6c3c74779a614d3b81dd75518824e25c',
'id': '3e68f81a-3e9e-45f6-8332-55c31a3195dc',
'size': 1},
'source_volid': None}
filter_properties = {}
"""
source_volid = request_spec['source_volid']
# source_volid = None;
volume_id = request_spec['volume_id']
# volume_id = 3e68f81a-3e9e-45f6-8332-55c31a3195dc
snapshot_id = request_spec['snapshot_id']
# snapshot_id = None;
image_id = request_spec['image_id']
# image_id = None;
host = None
# snapshot_same_host:这个参数定义了是否根据快照在快照所处的主机上建立卷;
# 参数的默认值为True;
# snapshot_id = None,说明请求中不是要求根据快照来建立卷;
if snapshot_id and CONF.snapshot_same_host:
# 根据snapshot_id获取指定卷的快照;
snapshot_ref = self.db.snapshot_get(context, snapshot_id)
# 根据snapshot_ref['volume_id']获取volume;
source_volume_ref = self.db.volume_get(context, snapshot_ref['volume_id'])
# 根据卷的属性获取它所在的主机信息;
host = source_volume_ref['host']
# source_volid = None,说明请求中不是要求根据现有的指定的卷来建立卷;
elif source_volid:
# 根据source_volid获取volume;
source_volume_ref = self.db.volume_get(context, source_volid)
# 根据卷的属性获取它所在的主机信息;
host = source_volume_ref['host']
# 如果没有获取到主机信息,说明没有确定建立卷的目标主机,需要通过调度器择优获取目标主机,并实现远程调用方法create_volume,来进行建立卷的操作;
if not host:
# create_volume:远程调用实现卷的建立操作;
# self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI();
self.scheduler_rpcapi.create_volume(
context,
CONF.volume_topic,
volume_id,
snapshot_id=snapshot_id,
image_id=image_id,
request_spec=request_spec,
filter_properties=filter_properties)
# 如果获取到主机的信息,则直接通过volume_rpcapi实现远程调用方法create_volume,来进行卷的建立操作。
else:
now = timeutils.utcnow()
values = {'host': host, 'scheduled_at': now}
# 在卷上设置给定的属性,并进行更新;
volume_ref = self.db.volume_update(context, volume_id, values)
# 远程调用实现建立并导出卷;
# self.volume_rpcapi = volume_rpcapi.VolumeAPI();
self.volume_rpcapi.create_volume(
context,
volume_ref,
volume_ref['host'],
request_spec,
filter_properties,
allow_reschedule=False,
snapshot_id=snapshot_id,
image_id=image_id,
source_volid=source_volid)
这个方法主要实现了根据具体情况通过远程调用建立新卷的操作。在这个方法中,大致可以分为四个步骤:
1.从输入参数request_spec中获取相应的数据信息,主要用于鉴别采用何种建立卷的方法;
2.判断是否根据现有快照在快照所在的主机上进行新卷的建立,根据判断结果从具体的参数中获取建立新卷的目标主机;
3.如果没有获取到主机信息,说明没有确定建立卷的目标主机,需要通过调度器择优获取目标主机,并实现远程调用方法create_volume,来进行建立卷的操作;
4.如果获取到主机的信息,则直接通过volume_rpcapi实现远程调用方法create_volume,来进行卷的建立操作。
我们先来看上述第3步骤的具体实现过程,在这里我们知道self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI(),所以这里调用的建立卷的方法为/cinder/scheduler/rpcapi.py----class SchedulerAPI----def create_volume,我们来看方法的源码实现:
def create_volume(self, ctxt, topic, volume_id, snapshot_id=None,
image_id=None, request_spec=None,
filter_properties=None):
"""
远程调用实现建立卷的主机选择,以及卷的建立操作;
"""
request_spec_p = jsonutils.to_primitive(request_spec)
return self.cast(ctxt, self.make_msg(
'create_volume',
topic=topic,
volume_id=volume_id,
snapshot_id=snapshot_id,
image_id=image_id,
request_spec=request_spec_p,
filter_properties=filter_properties),
version='1.2')
我们可以看到,这个方法中通过广播的方式,发送请求远程调用建立卷的方法create_volume,这里具体执行的方法是/cinder/scheduler/manager.py----class SchedulerManager----def create_volume,我们来看方法的源码实现:
def create_volume(self, context, topic, volume_id, snapshot_id=None,
image_id=None, request_spec=None,
filter_properties=None):
"""
volume_rpcapi.create_volume是调用manager.py中的create_volume方法,
该方法先从数据库中取出volume的信息,然后调用volume_utils.notify_about_volume_usage方法(是不是通知RabbitMQ?)。
然后继续从volume信息中取vol_name,vol_size,
并且如果入参中有snapshot_id,说明从快照创建卷,则从DB中取该snapshot的信息,
如果入参中有source_volID,说明是从已有卷创建卷,则从DB中取该源卷信息,
如果入参中有image_id,说明是从镜像创建卷,则从glance中获取镜像服务器信息,镜像ID,镜像位置,镜像的metadata.
然后调用该类的私有方法_create_volume,该方法首先判断如果snapshot_ref,imag_id,srcvol_ref都是空,
则说明是创建一个空卷,就调用driver.create_volume去创卷,
如果有snapshot_ref参数,则调用driver.create_volume_from_snapshot方法去创卷,
如果请求中有源卷的参数,则调用driver.create_cloned_volume去创卷(实际上就是克隆一个卷)。
如果请求中有镜像参数,则调用driver.clone_image方法去创卷,
如果clone_image失败,则调用普通创卷方法先创建个空卷,
然后将卷状态置为downloading,
然后调用_copy_image_to_volume方法把镜像内容拷贝入卷中。
"""
# 构建并返回用于通过远程调度建立卷的flow;
flow = create_volume.get_scheduler_flow(db, self.driver,
request_spec,
filter_properties,
volume_id, snapshot_id,
image_id)
assert flow, _('Schedule volume flow not retrieved')
flow.run(context)
if flow.state != states.SUCCESS:
LOG.warn(_("Failed to successfully complete"
" schedule volume using flow: %s"), flow)
我们这里可以看到,这里再一次应用taskflow模式来实现通过调度器选取目标主机并实现建立卷的操作,taskflow模式的具体应用方法在前面的博客中我们已经进行过详细的解析,这里不再赘述,这里我们只是简单的看看源码实现:
def get_scheduler_flow(db, driver, request_spec=None, filter_properties=None,
volume_id=None, snapshot_id=None, image_id=None):
"""
Constructs and returns the scheduler entrypoint flow.
构建并返回用于通过远程调度建立卷的flow;
flow将会做以下的事情:
1. 为相关的task注入keys和values;
2. 实现了从输入的参数中提取调度器的规范信息的操作;
3. 对于出错的task进行处理,发送错误通知,记录错误信息等;
4. 远程调用实现在主机上建立卷;
"""
# flow_name:volume_create_scheduler;
flow_name = ACTION.replace(":", "_") + "_scheduler"
# 获取类Flow的实例化对象;
scheduler_flow = linear_flow.Flow(flow_name)
# This injects the initial starting flow values into the workflow so that
# the dependency order of the tasks provides/requires can be correctly
# determined.
# 添加一个给定的task到flow;
# 这个类实现了注入字典信息到flow中;
scheduler_flow.add(base.InjectTask({
'request_spec': request_spec,
'filter_properties': filter_properties,
'volume_id': volume_id,
'snapshot_id': snapshot_id,
'image_id': image_id,
}, addons=[ACTION]))
# This will extract and clean the spec from the starting values.
# 添加一个给定的task到flow;
# ExtractSchedulerSpecTask:实现了从输入的参数中提取调度器的规范信息的操作;
scheduler_flow.add(ExtractSchedulerSpecTask(db))
# The decorator application here ensures that the method gets the right
# requires attributes automatically by examining the underlying functions
# arguments.
@decorators.task
def schedule_create_volume(context, request_spec, filter_properties):
"""
实现通过合适的调度器算法选择建立卷的主机,并实现卷的建立;
"""
def _log_failure(cause):
"""
记录错误信息;
"""
LOG.error(_("Failed to schedule_create_volume: %(cause)s") %
{'cause': cause})
def _notify_failure(cause):
"""
When scheduling fails send out a event that it failed.
当调度出现错误时,则发送一个标志为错误的事件通知;
"""
topic = "scheduler.create_volume"
payload = {
'request_spec': request_spec,
'volume_properties': request_spec.get('volume_properties', {}),
'volume_id': volume_id,
'state': 'error',
'method': 'create_volume',
'reason': cause,
}
try:
publisher_id = notifier.publisher_id("scheduler")
notifier.notify(context, publisher_id, topic, notifier.ERROR,
payload)
except exception.CinderException:
LOG.exception(_("Failed notifying on %(topic)s "
"payload %(payload)s") % {'topic': topic,
'payload': payload})
try:
# 目前cinder中提供了三种调度器方法实现建立卷的主机的选择,并实现卷的建立;
driver.schedule_create_volume(context, request_spec, filter_properties)
except exception.NoValidHost as e:
# Not host found happened, notify on the scheduler queue and log
# that this happened and set the volume to errored out and
# *do not* reraise the error (since whats the point).
# 当调度出现错误时,则发送一个标志为错误的事件通知;
_notify_failure(e)
# 记录错误信息;
_log_failure(e)
_error_out_volume(context, db, volume_id, reason=e)
except Exception as e:
# Some other error happened, notify on the scheduler queue and log
# that this happened and set the volume to errored out and
# *do* reraise the error.
with excutils.save_and_reraise_exception():
_notify_failure(e)
_log_failure(e)
_error_out_volume(context, db, volume_id, reason=e)
# 添加一个给定的task到flow;
# schedule_create_volume:通过调度器获取目标主机并实现远程调用建立新卷的操作;
scheduler_flow.add(schedule_create_volume)
# 获取flow的调试信息;
return flow_utils.attach_debug_listeners(scheduler_flow)
在这个方法中,构建了flow任务流对象,并注入了三个task任务对象,其中前两个任务主要是完成若干信息数据的注入和获取,其中最重要的任务就是第三个任务,即通过调度器获取目标主机并实现远程调用建立新卷的操作。我们来看其具体实现方法的源码:
@decorators.task
def schedule_create_volume(context, request_spec, filter_properties):
"""
实现通过合适的调度器算法选择建立卷的主机,并实现卷的建立;
"""
def _log_failure(cause):
"""
记录错误信息;
"""
LOG.error(_("Failed to schedule_create_volume: %(cause)s") %
{'cause': cause})
def _notify_failure(cause):
"""
When scheduling fails send out a event that it failed.
当调度出现错误时,则发送一个标志为错误的事件通知;
"""
topic = "scheduler.create_volume"
payload = {
'request_spec': request_spec,
'volume_properties': request_spec.get('volume_properties', {}),
'volume_id': volume_id,
'state': 'error',
'method': 'create_volume',
'reason': cause,
}
try:
publisher_id = notifier.publisher_id("scheduler")
notifier.notify(context, publisher_id, topic, notifier.ERROR,
payload)
except exception.CinderException:
LOG.exception(_("Failed notifying on %(topic)s "
"payload %(payload)s") % {'topic': topic,
'payload': payload})
try:
# 目前cinder中提供了三种调度器方法实现建立卷的主机的选择,并实现卷的建立;
driver.schedule_create_volume(context, request_spec, filter_properties)
except exception.NoValidHost as e:
# Not host found happened, notify on the scheduler queue and log
# that this happened and set the volume to errored out and
# *do not* reraise the error (since whats the point).
# 当调度出现错误时,则发送一个标志为错误的事件通知;
_notify_failure(e)
# 记录错误信息;
_log_failure(e)
_error_out_volume(context, db, volume_id, reason=e)
except Exception as e:
# Some other error happened, notify on the scheduler queue and log
# that this happened and set the volume to errored out and
# *do* reraise the error.
with excutils.save_and_reraise_exception():
_notify_failure(e)
_log_failure(e)
_error_out_volume(context, db, volume_id, reason=e)
这个方法中最重要的语句就是:
driver.schedule_create_volume(context, request_spec, filter_properties)
这条语句所实现的作用就是调用指定的调度器算法实现目标主机的确定,并实现新卷的建立的操作。在H版的cinder模块中,暂时实现了三个调度器算法,即过滤-承重算法、随机获取主机算法和选取建立卷数量最少作为目标主机的算法。这里所调用的方法是由driver所确定的;
我们在类SchedulerManager的初始化方法中可以看到:
if not scheduler_driver:
scheduler_driver = CONF.scheduler_driver
self.driver = importutils.import_object(scheduler_driver)
所以具体调度器算法的确定就是由变量scheduler_driver所确定的,我们又可以看到:
scheduler_driver_opt = cfg.StrOpt('scheduler_driver',
default='cinder.scheduler.filter_scheduler.'
'FilterScheduler',
help='Default scheduler driver to use')
所以系统默认的调度器算法为:
cinder.scheduler.filter_scheduler.FilterScheduler.schedule_create_volume,所以我们先来看这个方法的源码实现:
def schedule_create_volume(self, context, request_spec, filter_properties):
"""
对主机进行过滤和称重操作,获取最优主机,并实现远程调用建立并导出卷;
"""
# 对主机进行过滤称重操作,获取最优的主机;
weighed_host = self._schedule(context, request_spec, filter_properties)
if not weighed_host:
raise exception.NoValidHost(reason="")
host = weighed_host.obj.host
volume_id = request_spec['volume_id']
snapshot_id = request_spec['snapshot_id']
image_id = request_spec['image_id']
# @@@@在卷上设置给定的属性,并进行更新;
updated_volume = driver.volume_update_db(context, volume_id, host)
# @@@@在最优主机选定之后,添加附加信息到过滤器属性;
self._post_select_populate_filter_properties(filter_properties,
weighed_host.obj)
# context is not serializable
filter_properties.pop('context', None)
# create_volume:远程调用实现建立并导出卷;
self.volume_rpcapi.create_volume(context, updated_volume, host,
request_spec, filter_properties,
allow_reschedule=True,
snapshot_id=snapshot_id,
image_id=image_id)
这里采用了对所有可用主机进行过滤和称重操作来实现确定目标主机,并调用create_volume方法实现远程调用在目标主机上建立新卷的操作。具体方法实现比较简单,脉络比较清晰,所以这里不进行详细的解析。
我们再来看随机选取目标主机的调度器算法的源码实现:
def schedule_create_volume(self, context, request_spec, filter_properties):
"""
Picks a host that is up at random.
实现随机选取主机;
远程调用实现在选取的主机上建立并导出卷;
"""
topic = CONF.volume_topic
# _schedule:实现随机选取主机;
host = self._schedule(context, topic, request_spec,
filter_properties=filter_properties)
volume_id = request_spec['volume_id']
snapshot_id = request_spec['snapshot_id']
image_id = request_spec['image_id']
# volume_update_db:在给定的卷的数据库中设置属性信息并进行更新;
updated_volume = driver.volume_update_db(context, volume_id, host)
# create_volume:远程调用实现建立并导出卷;
self.volume_rpcapi.create_volume(context, updated_volume, host,
request_spec, filter_properties,
snapshot_id=snapshot_id,
image_id=image_id)
我们也可以看到在这个方法中,也调用create_volume方法实现远程调用在目标主机上建立新卷的操作;
我们再来看选取最少卷作为目标主机的调度器算法的源码实现:
def schedule_create_volume(self, context, request_spec, filter_properties):
"""
Picks a host that is up and has the fewest volumes.
选取拥有最少卷的主机作为目标主机;
"""
elevated = context.elevated()
volume_id = request_spec.get('volume_id')
snapshot_id = request_spec.get('snapshot_id')
image_id = request_spec.get('image_id')
volume_properties = request_spec.get('volume_properties')
volume_size = volume_properties.get('size')
availability_zone = volume_properties.get('availability_zone')
zone, host = None, None
if availability_zone:
zone, _x, host = availability_zone.partition(':')
if host and context.is_admin:
topic = CONF.volume_topic
service = db.service_get_by_args(elevated, host, topic)
if not utils.service_is_up(service):
raise exception.WillNotSchedule(host=host)
updated_volume = driver.volume_update_db(context, volume_id, host)
self.volume_rpcapi.create_volume(context, updated_volume, host,
request_spec, filter_properties,
snapshot_id=snapshot_id,
image_id=image_id)
return None
results = db.service_get_all_volume_sorted(elevated)
if zone:
results = [(s, gigs) for (s, gigs) in results
if s['availability_zone'] == zone]
for result in results:
(service, volume_gigabytes) = result
if volume_gigabytes + volume_size > CONF.max_gigabytes:
msg = _("Not enough allocatable volume gigabytes remaining")
raise exception.NoValidHost(reason=msg)
if utils.service_is_up(service) and not service['disabled']:
updated_volume = driver.volume_update_db(context, volume_id,
service['host'])
self.volume_rpcapi.create_volume(context, updated_volume,
service['host'], request_spec,
filter_properties,
snapshot_id=snapshot_id,
image_id=image_id)
return None
msg = _("Is the appropriate service running?")
raise exception.NoValidHost(reason=msg)
我们也可以看到在这个方法中,也调用了create_volume方法实现远程调用在目标主机上建立新卷的操作;
我们可以看到,在这三个调度器算法中,最后都调用了方法create_volume实现远程调用在目标主机上建立新卷的操作。我们再回到方法_cast_create_volume中,可以看到也是调用了create_volume方法实现远程调用在目标主机上建立新卷的目标。
对于方法create_volume的实现过程,我将会在下一篇博客中进行详细解析。