nova-api服务解析
nova-api服务介绍
nova作为openstack云中的计算组织控制器,负责支持openstack云中实例生命周期的所有活动,先前在nova组件手动安装https://blog.csdn.net/weixin_41977332/article/details/104031168 中介绍了nova架构中的各个服务组件及其功能。在nova的设计过程中,很注重各个服务组件之间的解耦性。nova内部不同服务之间是通过消息队列来进行信息中转(关于消息队列的内容后续将有一篇文章专门进行介绍),而nova和外部其他组件诸如glance,neutron等则是通过REST API接口进行REST通信(关于RESTful API框架后续将有一篇文章进行介绍),在nova中这个api服务即通过nova-api实现。nova-api启动时会被初始化为一个WSGI Server,然后nova-api通过nova,然后nova-api通过nova/api/openStack:APIRouterV21(如下图所示)与多个WSGI Application相关联,即当有外部请求到达nova-api后,这个请求会通过APIRouterV21的父类nova/wsgi:Router发送到相应的WSGI Application中。每一个WSGI Application都会有相应的Controller并在该类中实现相应的执行方法(nova-api中该类在nova/api/openstack/compute/servers:ServersController,如下图二所示),从Controller中相应的方法开始,Nova内部的通信机制正式运行,即请求到达Nova内部并被内部相关服务处理。总之nova-api作为WSGI Server,一方面可以接收外部的请求,另一方面可以调用内部相关服务处理外部来的请求。
nova-api服务启动流程
nova-api作为Nova组件的入口,它在启动过程中会进行许多初始化的操作,例如加载API、创建WSGI Server、加载策略等。对于openstack的所有组件而言,我们学习一个组件的源码都可以从setup.cfg文件开始,该文件又叫做openstack组件的结构地图,从这里可以找到脚本的入口,如下图所示
我们可以看出。启动nova-api服务时,首先调用的就是nova.cmd.api中的main()方法,该方法的关键代码如下图所示。在这个方法中,首先会通过第41行的config.parse_args(sys.argv)读取用户的配置信息,该信息都会在nova/etc/nova.conf中进行设置,然后第54行通过service.process_launcher()创建一个ProcessLauncher的对象实例,该实例能够启动多个service子进程,接下来通过第59行service.WSGIService(api,use_ssl=should_use_ssl)来声明一个WSGI实例,以API作为参数传入,而API的值是通过用户通过enabled_apis设置的,之后通过第60行的launch_service()启动WSGI Server。
nova-scheduler服务解析
nova-scheduler原理及代码结构
nova-scheduler是nova中负责调度的服务,主要作用是将nova-api发送来的请求,通过一定的方式映射到某一计算节点上。它通过用户设定的调度算法从系统资源池中筛选出一个计算节点用于创建虚拟机。nova-scheduler对于计算节点的选取分为两步,第一步是通过过滤器(Filter)选择满足条件的计算节点,第二步则是权重计算(Weight),通过计算权重,选出权值最小的计算节点并在其上创建虚拟机。nova-scheduler的代码位与nova/scheduler,其代码结构如下图所示。过滤器和权重计算部分分别对应图中的filters文件夹和weights文件夹,client文件夹中为客户端调用程序的入口,其他比较重要的文件包括driver.py(实现名为Scheduler的类,它是nova-scheduler中所有调度器的基类,任何调度器都需要实现其中的接口),manager.py(实现名为SchedulerManager的类,定义Host相关的管理操作函数,如删除Host中的虚拟机信息等),host_manager.py(实现名为HostManager和HostState两个类,这两个类描述了跟调度器相关的主机的操作实现),chance.py(实现名为ChanceScheduler的类,继承于driver.py中定义的Scheduler类,是一个主要实现随机选取主机节点的调度器)。
调度过程
调度事件的触发
虚拟机创建过程中,触发nova-scheduler进行主机调度的服务是nova-conductor。当来自nova-api的创建虚拟机的请求发送到nova-conductor时(所谓的发送是指nova-api向消息队列中发送build_instances的消息,nova/conductor/rpcapi:build_instance
会订阅相关消息),会调用nova/conductor/manager.py:ComputeTaskManager的build_instance()方法,如下图所示,该方法会在第529行调用_schedule_instances(context,request_spec,filter_properties),而_schedule_instances()方法会调用到第609行的scheduler_client.select_destinations(context,request_spec,instance_uuids)返回被选中的hosts,scheduler_client在ComputeTaskManager实例初始化时为sheduler_client.SchedulerClient(),对应的是nova/scheduler/client/init.py中的SchedulerClient类,select_destinations也是类中的一个方法。
调度过程分析
调度过程的入口,为nova/scheduler/client/init.py中的SchedulerClient类中的select_destinations方法,如上图所示,该方法调用的是self.queryclient.select_destinations()方法,如下图所示,该方法t通过scheduler_rpcapi.select_destinations(context,spec_obj)发送了一个RPC调用,如后一个图所示,最后call的方法为nova/scheduler/manager.py中的selection_destination方法
nova/scheduler/manager.py中的select_destinations()方法如下图所示,第98行的self.driver.select_destinations()方法实现的功能就是过滤与计算权重,最终选出主机列表,具体方法实现的定义在nova/scheduler/filter_scheduler.py中的select_destinations(),如后一张图所示,函数返回值为按权重排序过的主机列表dests,按权重排序是通过第58行的self._schedule()方法来实现的。
得到按权重排序的主机列表之后,我们回到nova/conductor/manager.py:ComputeTaskManager的build_instance()方法(调度事件触发部分的第一张图),此时的hosts即为该主机列表,该方法最后部分会调用self.compute_rpcapi_build_and_run_instance(),调动nova-compute服务,进入虚拟机创建过程,后续内容会在后面nova-compute虚拟机创建流程中进行介绍。
nova-compute服务解析
nova-compute服务的作用
nova-compute是nova中负责虚拟机生命周期管理的服务,通过底层的Libvirt驱动,实现虚拟化与openstack的结合,即从功能角度来说,nova-compute在nova中主要负责与Hypervisor进行交互,通过调用相应的Hypervisor的接口,实现对虚拟机的创建,删除,挂起,resize等操作,换句话说,它可以与Hypervisor一起实现对虚拟机生命周期的管理。nova-compute可以使用不同的Hypervisor(最常用的为基于KVM/Xen类型的Hypervisor),不同的Hypervisor可以通过修改位于部署了nova-compute的计算节点的/etc/nova/nova.conf中的compute_driver的值进行配置。
nova-compute创建虚拟机时,其主要任务就是通过用户准备的各种参数,最终生成创建虚拟机的XML文件以供Hypervisor使用,主要步骤如下:
- 准备虚拟机所需要的资源(CPU资源,RAM资源以及镜像资源等),网络资源也会在这个阶段一同分配
- 创建虚拟机的镜像文件,nova-compute会调用glanceclient拉取镜像,以该镜像为基础创建镜像文件。
- 生成创建虚拟机的XML文件
- 创建网络并启动虚拟机
以上过程中生成的XML或者暂存在计算节点的镜像都会存放在计算节点的/var/lib/nova/instances文件夹下以该虚拟机的UUID命名的子目录中。
nova-compute服务的启动流程
nova-compute服务的入口位于nova/cmd/compute.py,如下图所示
其中第42行会读取配置文件进行一些初始化的工作,第55行代码用于创建RPC Service,并将此RPC Service的topic设置为CONF.compute_topic,类Service的定义位于nova/service.py,如下所示,其中第116行为实例化一个ComputeManager(nova/compute/manager.py)类,里面定义了接受并处理各种RPC请求的方法。
上图为service.Service.create()方法,通过提供的与RPC相关的topic等参数,返回该RPC service类。在nova/cmd/compute.py的第57行service.serve(server)用于启动RPC Service(),service.serve()的实现方法(nova/service.py)如下所示,通过oslo_service/service.py中的launch方法,启动相应数量的RPC Service进程,至此nova-compute服务启动。
nova-compute服务创建虚拟机流程解析
nova/conductor/manager.py:ComputeTaskManager的build_instance()方法在确定好按权重排序的主机列表之后,会调用self.compute_rpcapi_build_and_run_instance()方法,该方法的代码如下所示(nova/compute/rpcapi.py),该方法通过RPC调用nova/compute/manager.py:ComputeManager中的build_and_run_instance(),该方法如后一张图所示,里面定义了一个_locked_do_build_and_run_instance()方法,这里真正实现instance的创建和启动的,还有一个spawn方法调用,这是因为nova compute创建VM的时间会比较长,因为其中包括了下载VM镜像、创建和配置VM网卡信息、文件注入等任务,因此调用utils.spawn_n()方法返回RPC线程而不会造成阻塞。
关于_do_build_and_run_instance()方法,由于该部分的代码比较长,所以将源码粘贴如下,这部分内容的解读参考了https://blog.csdn.net/ksj367043706/article/details/89287020,_do_build_and_run_instance()方法主要完成了如下几个步骤
- 第11~16行:设置任务状态:修改vm_state为building,task_state为None,然后调用instance.save()将状态存入数据库中。
- 第26行如果起虚机时有文件注入,将会调用nova.compute.manager.ComputeManager._decode_files()完成文件注入处理。
- 第34~39行:开始创建VM并启动,调用nova.compute.manager.ComputeManager._build_and_run_instance()开始创建VM,这个方法为虚机申请了镜像,网络等资源(这部分代码比较复杂,限于篇幅,后续教程中将进行讲解),此时vm_stat为building,task_state为spawning,然后调用instance.save()方法将状态存入数据库中。如果虚拟机创建成功,则在第42行返回
build_results.ACTIVE,完成虚拟机创建
@hooks.add_hook('build_instance')
@wrap_exception()
@reverts_task_state
@wrap_instance_event(prefix='compute')
@wrap_instance_fault
def _do_build_and_run_instance(self, context, instance, image,
request_spec, filter_properties, admin_password, injected_files,
requested_networks, security_groups, block_device_mapping,
node=None, limits=None, host_list=None):
try:
LOG.debug('Starting instance...', instance=instance)
instance.vm_state = vm_states.BUILDING
instance.task_state = None
instance.save(expected_task_state=
(task_states.SCHEDULING, None))
except exception.InstanceNotFound:
msg = 'Instance disappeared before build.'
LOG.debug(msg, instance=instance)
return build_results.FAILED
except exception.UnexpectedTaskStateError as e:
LOG.debug(e.format_message(), instance=instance)
return build_results.FAILED
# b64 decode the files to inject:
decoded_files = self._decode_files(injected_files)
if limits is None:
limits = {}
if node is None:
node = self._get_nodename(instance, refresh=True)
try:
with timeutils.StopWatch() as timer:
self._build_and_run_instance(context, instance, image,
decoded_files, admin_password, requested_networks,
security_groups, block_device_mapping, node, limits,
filter_properties, request_spec)
LOG.info('Took %0.2f seconds to build instance.',
timer.elapsed(), instance=instance)
return build_results.ACTIVE
except exception.RescheduledException as e:
retry = filter_properties.get('retry')
if not retry:
# no retry information, do not reschedule.
LOG.debug("Retry info not present, will not reschedule",
instance=instance)
self._cleanup_allocated_networks(context, instance,
requested_networks)
self._cleanup_volumes(context, instance,
block_device_mapping, raise_exc=False)
compute_utils.add_instance_fault_from_exc(context,
instance, e, sys.exc_info(),
fault_message=e.kwargs['reason'])
self._nil_out_instance_obj_host_and_node(instance)
self._set_instance_obj_error_state(context, instance,
clean_task_state=True)
return build_results.FAILED
LOG.debug(e.format_message(), instance=instance)
# This will be used for logging the exception
retry['exc'] = traceback.format_exception(*sys.exc_info())
# This will be used for setting the instance fault message
retry['exc_reason'] = e.kwargs['reason']
# NOTE(comstud): Deallocate networks if the driver wants
# us to do so.
# NOTE(mriedem): Always deallocate networking when using Neutron.
# This is to unbind any ports that the user supplied in the server
# create request, or delete any ports that nova created which were
# meant to be bound to this host. This check intentionally bypasses
# the result of deallocate_networks_on_reschedule because the
# default value in the driver is False, but that method was really
# only meant for Ironic and should be removed when nova-network is
# removed (since is_neutron() will then always be True).
# NOTE(vladikr): SR-IOV ports should be deallocated to
# allow new sriov pci devices to be allocated on a new host.
# Otherwise, if devices with pci addresses are already allocated
# on the destination host, the instance will fail to spawn.
# info_cache.network_info should be present at this stage.
if (self.driver.deallocate_networks_on_reschedule(instance) or
utils.is_neutron() or
self.deallocate_sriov_ports_on_reschedule(instance)):
self._cleanup_allocated_networks(context, instance,
requested_networks)
else:
# NOTE(alex_xu): Network already allocated and we don't
# want to deallocate them before rescheduling. But we need
# to cleanup those network resources setup on this host before
# rescheduling.
self.network_api.cleanup_instance_network_on_host(
context, instance, self.host)
self._nil_out_instance_obj_host_and_node(instance)
instance.task_state = task_states.SCHEDULING
instance.save()
# The instance will have already claimed resources from this host
# before this build was attempted. Now that it has failed, we need
# to unclaim those resources before casting to the conductor, so
# that if there are alternate hosts available for a retry, it can
# claim resources on that new host for the instance.
self.reportclient.delete_allocation_for_instance(context,
instance.uuid)
self.compute_task_api.build_instances(context, [instance],
image, filter_properties, admin_password,
injected_files, requested_networks, security_groups,
block_device_mapping, request_spec=request_spec,
host_lists=[host_list])
return build_results.RESCHEDULED
except (exception.InstanceNotFound,
exception.UnexpectedDeletingTaskStateError):
msg = 'Instance disappeared during build.'
LOG.debug(msg, instance=instance)
self._cleanup_allocated_networks(context, instance,
requested_networks)
return build_results.FAILED
except Exception as e:
if isinstance(e, exception.BuildAbortException):
LOG.error(e.format_message(), instance=instance)
else:
# Should not reach here.
LOG.exception('Unexpected build failure, not rescheduling '
'build.', instance=instance)
self._cleanup_allocated_networks(context, instance,
requested_networks)
self._cleanup_volumes(context, instance,
block_device_mapping, raise_exc=False)
compute_utils.add_instance_fault_from_exc(context, instance,
e, sys.exc_info())
self._nil_out_instance_obj_host_and_node(instance)
self._set_instance_obj_error_state(context, instance,
clean_task_state=True)
return build_results.FAILED