设为首页 收藏本站
查看: 1216|回复: 0

[经验分享] openstack nova 源码

[复制链接]

尚未签到

发表于 2017-12-4 18:28:04 | 显示全部楼层 |阅读模式
  发现了一个关于nova 启动VM 源码阅读的好文章,作者地址
  OpenStack所有项目都是基于Python开发,并且都是标准的Python项目,通过setuptools工具管理项目,负责Python模块的安装和分发。想知道一个项目有哪些服务组成,最直接有效的办法就是找到入口函数(main函数)在哪里,只要是标准的基于setuptools管理的项目的所有入口函数都会在项目根目录的setup.cfg文件中定义,console_scripts就是所有服务组件的入口,比如nova(Mitaka版本)的setup.cfg的console_scripts如下:





[entry_points]
...
console_scripts =
nova-all = nova.cmd.all:main
nova-api = nova.cmd.api:main
nova-api-metadata = nova.cmd.api_metadata:main
nova-api-os-compute = nova.cmd.api_os_compute:main
nova-cells = nova.cmd.cells:main
nova-cert = nova.cmd.cert:main
nova-compute = nova.cmd.compute:main
nova-conductor = nova.cmd.conductor:main
nova-console = nova.cmd.console:main
nova-consoleauth = nova.cmd.consoleauth:main
nova-dhcpbridge = nova.cmd.dhcpbridge:main
nova-idmapshift = nova.cmd.idmapshift:main
nova-manage = nova.cmd.manage:main
nova-network = nova.cmd.network:main
nova-novncproxy = nova.cmd.novncproxy:main
nova-rootwrap = oslo_rootwrap.cmd:main
nova-rootwrap-daemon = oslo_rootwrap.cmd:daemon
nova-scheduler = nova.cmd.scheduler:main
nova-serialproxy = nova.cmd.serialproxy:main
nova-spicehtml5proxy = nova.cmd.spicehtml5proxy:main
nova-xvpvncproxy = nova.cmd.xvpvncproxy:main
...
  由此可知nova项目安装后会包含21个可执行程序,其中nova-compute服务的入口函数为nova/cmd/compute.py模块的main函数:





def main():
config.parse_args(sys.argv)
logging.setup(CONF, 'nova')
utils.monkey_patch()
objects.register_all()
gmr.TextGuruMeditation.setup_autorun(version)
if not CONF.conductor.use_local:
block_db_access()
objects_base.NovaObject.indirection_api = \
conductor_rpcapi.ConductorAPI()
else:
LOG.warning(_LW('Conductor local mode is deprecated and will '
'be removed in a subsequent release'))
server = service.Service.create(binary='nova-compute',
topic=CONF.compute_topic,
db_allowed=CONF.conductor.use_local)
service.serve(server)
service.wait()
  其它服务依次类推。
  OpenStack使用Python语言开发,而Python是动态类型语言,参数类型不容易从代码中看出,因此部署一个allinone的OpenStack开发测试环境非常有必要,建议使用RDO部署:Packstack quickstart,当然乐于折腾使用DevStack也是没有问题的。
  要想深入研究源码,最有效的方式就是一步一步跟踪代码执行,因此会使用debug工具是关键技能之一。Python的debug工具有很多,为了简便起见,pdb工具就够了,你也可以尝试ipdb、ptpdb之类的调试工具。使用方法也非常简单,只要在你想设置断点的地方,嵌入以下代码:





import pdb; pdb.set_trace()
  然后在命令行(不能通过systemd启动)直接运行服务即可。
  假如想跟踪nova创建虚拟机的过程,首先nova/api/openstack/compute/servers.py模块的create方法打上断点,如下:





def create(self, req, body):
"""Creates a new server for a given user."""
import pdb; pdb.set_trace() # 设置断点
context = req.environ['nova.context']
server_dict = body['server']
password = self._get_server_admin_password(server_dict)
name = common.normalize_name(server_dict['name'])
if api_version_request.is_supported(req, min_version='2.19'):
if 'description' in server_dict:
# This is allowed to be None
description = server_dict['description']
else:
# No default description
description = None
else:
description = name
...


  此时调用创建虚拟机API,nova-api进程就会立即弹出pdb shell,此时你可以通过s或者n命令一步一步执行代码。

1.3 OpenStack项目通用骨骼脉络
  阅读源码的首要问题就是就要对代码的结构了然于胸,需要强调的是,OpenStack项目的目录结构并不是根据组件严格划分,而是根据功能划分,以Nova为例,compute目录并不是一定在nova-compute节点上运行,而主要是和compute相关(虚拟机操作相关)的功能实现,同样的,scheduler目录代码并不全在scheduler服务节点运行,但主要是和调度相关的代码。不过目录结构并不是完全没有规律,它遵循一定的套路。
  通常一个服务的目录都会包含api.py、rpcapi.py、manager.py,这三个是最最重要的模块。


  • api.py: 通常是供其它组件调用的封装库。换句话说,该模块通常并不会由本模块调用,而是类似提供其它服务SDK。比如compute目录的api.py,通常会由nova-api服务的controller调用。
  • rpcapi.py:这个是RPC请求的封装,或者说是RPC封装的client端,该模块封装了所有RPC请求调用。
  • manager.py: 这个才是真正服务的功能实现,也是RPC的服务端,即处理RPC请求的入口,实现的方法和rpcapi实现的方法一一对应。
  比如对一个虚拟机执行关机操作的流程为:



API节点
nova-api接收用户请求 -> nova-api调用compute/api.py
-> compute/api调用compute/rpcapi.py -> rpcapi.py向目标计算节点发起stop_instance()RPC请求
计算节点
收到MQ RPC消息 -> 解析stop_instance()请求 -> 调用compute/manager.py的callback方法stop_instance() -> 调用libvirt关机虚拟机

  前面提到OpenStack项目的目录结构是按照功能划分的,而不是服务组件,因此并不是所有的目录都能有对应的组件。仍以Nova为例:


  • cmd:这是服务的启动脚本,即所有服务的main函数。看服务怎么初始化,就从这里开始。
  • db: 封装数据库访问API,目前支持的driver为sqlalchemy,还包括migrate repository。
  • conf:Nova的配置项声明都在这里,想看Nova配置的作用和默认值可以从这个目录入手。
  • locale: 本地化处理。
  • image: 封装image API,其实就是调用python-glanceclient。
  • network: 封装网络服务接口,根据配置不同,可能调用nova-network或者neutron。
  • volume: 封装数据卷访问接口,通常是Cinder的client封装,调用python-cinderclient。
  • virt: 这是所有支持的hypervisor驱动,主流的如libvirt、xen等。
  • objects: 对象模型,封装了所有实体对象的CURD操作,相对直接调用db的model更安全,并且支持版本控制。
  • policies: policy校验实现。
  • tests: 单元测试和功能测试代码。
  以上同样适用于其它服务,比如Cinder等。
  另外需要了解的是,所有的API入口都是从xxx-api开始的,RESTFul API是OpenStack服务的唯一入口,也就是说,阅读源码就从api开始。而api组件也是根据实体划分的,不同的实体对应不同的controller,比如servers、flavors、keypairs等,controller的index方法对应list操作、show方法对应get操作、create创建、delete删除、update更新等。
  根据进程阅读源码并不是什么好的实践,因为光理解服务如何初始化、如何通信、如何发送心跳等就不容易,各种高级封装太复杂了。我认为比较好的阅读源码方式是追踪一个任务的执行过程,比如看启动虚拟机的整个流程。因此接下来本文将以创建一台虚拟机为例,一步步分析其过程。

2 创建虚拟机过程分析
  这里以创建虚拟机过程为例,根据前面的总体套路,一步步跟踪其执行过程。需要注意的是,Nova支持同时创建多台虚拟机,因此在调度时需要选择多个宿主机。

S1 nova-api
  入口为nova/api/openstack/compute/servers.py的create方法,该方法检查了一堆参数以及policy后,调用compute_api的create方法。





def create(self, req, body):
"""Creates a new server for a given user."""
context = req.environ['nova.context']
server_dict = body['server']
password = self._get_server_admin_password(server_dict)
name = common.normalize_name(server_dict['name'])
...
flavor_id = self._flavor_id_from_req_data(body)
try:
inst_type = flavors.get_flavor_by_flavor_id(
flavor_id, ctxt=context, read_deleted="no")
(instances, resv_id) = self.compute_api.create(context,
inst_type,
image_uuid,
display_name=name,
display_description=description,
availability_zone=availability_zone,
forced_host=host, forced_node=node,
metadata=server_dict.get('metadata', {}),
admin_password=password,
requested_networks=requested_networks,
check_server_group_quota=True,
**create_kwargs)
except (exception.QuotaError,
exception.PortLimitExceeded) as error:
raise exc.HTTPForbidden(
explanation=error.format_message())
...
  这里的compute_api即前面说的nova/compute/api.py模块,找到该模块的create方法,该方法会创建数据库记录、检查参数等,然后调用compute_task_api的build_instances方法:





self.compute_task_api.schedule_and_build_instances(
context,
build_requests=build_requests,
request_spec=request_specs,
image=boot_meta,
admin_password=admin_password,
injected_files=injected_files,
requested_networks=requested_networks,
block_device_mapping=block_device_mapping)
  compute_task_api即conductor的api.py。conductor的api并没有执行什么操作,直接调用了conductor_compute_rpcapi的build_instances方法:





def schedule_and_build_instances(self, context, build_requests,
request_spec, image,
admin_password, injected_files,
requested_networks, block_device_mapping):
self.conductor_compute_rpcapi.schedule_and_build_instances(
context, build_requests, request_spec, image,
admin_password, injected_files, requested_networks,
block_device_mapping)
  该方法就是conductor RPC API,即nova/conductor/rpcapi.py模块,该方法除了一堆的版本检查,剩下的就是对RPC调用的封装,代码只有两行:





cctxt = self.client.prepare(version=version)
cctxt.cast(context, 'build_instances', **kw)
  其中cast表示异步调用,build_instances是远程调用的方法,kw是传递的参数。参数是字典类型,没有复杂对象结构,因此不需要特别的序列化操作。
  截至到现在,虽然目录由api->compute->conductor,但仍在nova-api进程中运行,直到cast方法执行,该方法由于是异步调用,因此nova-api任务完成,此时会响应用户请求,虚拟机状态为building。

S2 nova-conductor
  由于是向nova-conductor发起的RPC调用,而前面说了接收端肯定是manager.py,因此进程跳到nova-conductor服务,入口为nova/conductor/manager.py的build_instances方法,该方法首先调用了_schedule_instances方法,该方法调用了scheduler_client的select_destinations方法:





def _schedule_instances(self, context, request_spec, filter_properties):
scheduler_utils.setup_instance_group(context, request_spec,
filter_properties)
# TODO(sbauza): Hydrate here the object until we modify the
# scheduler.utils methods to directly use the RequestSpec object
spec_obj = objects.RequestSpec.from_primitives(
context, request_spec, filter_properties)
hosts = self.scheduler_client.select_destinations(context, spec_obj)
return hosts
  scheduler_client和compute_api以及compute_task_api都是一样对服务的client SDK调用,不过scheduler没有api.py,而是有个单独的client目录,实现在client目录的__init__.py,这里仅仅是调用query.py下的SchedulerQueryClient的select_destinations实现,然后又很直接地调用了scheduler_rpcapi的select_destinations方法,终于又到了RPC调用环节。





def _schedule_instances(self, context, request_spec, filter_properties):
scheduler_utils.setup_instance_group(context, request_spec,
filter_properties)
# TODO(sbauza): Hydrate here the object until we modify the
# scheduler.utils methods to directly use the RequestSpec object
spec_obj = objects.RequestSpec.from_primitives(
context, request_spec, filter_properties)
hosts = self.scheduler_client.select_destinations(context, spec_obj)
return hosts
  毫无疑问,RPC封装同样是在scheduler的rpcapi中实现。该方法RPC调用代码如下:





return cctxt.call(ctxt, 'select_destinations', **msg_args)
  注意这里调用的call方法,即同步RPC调用,此时nova-conductor并不会退出,而是堵塞等待直到nova-scheduler返回。因此当前状态为nova-conductor为blocked状态,等待nova-scheduler返回,nova-scheduler接管任务。

S3 nova-scheduler
  同理找到scheduler的manager.py模块的select_destinations方法,该方法会调用driver方法,这里的driver其实就是调度算法实现,通常用的比较多的就是Filter Scheduler算法,对应filter_scheduler.py模块,该模块首先通过host_manager拿到所有的计算节点信息,然后通过filters过滤掉不满足条件的计算节点,剩下的节点通过weigh方法计算权值,最后选择权值高的作为候选计算节点返回。最后nova-scheduler返回调度结果的hosts集合,任务结束,返回到nova-conductor服务。

S4 nova-condutor
  回到scheduler/manager.py的build_instances方法,nova-conductor等待nova-scheduler返回后,拿到调度的计算节点列表。因为可能同时启动多个虚拟机,因此循环调用了compute_rpcapi的build_and_run_instance方法。





for (instance, host) in six.moves.zip(instances, hosts):
instance.availability_zone = (
availability_zones.get_host_availability_zone(context,
host['host']))
try:
# NOTE(danms): This saves the az change above, refreshes our
# instance, and tells us if it has been deleted underneath us
        instance.save()
except (exception.InstanceNotFound,
exception.InstanceInfoCacheNotFound):
LOG.debug('Instance deleted during build', instance=instance)
continue
...
self.compute_rpcapi.build_and_run_instance(context,
instance=instance, host=host['host'], image=image,
request_spec=request_spec,
filter_properties=local_filter_props,
admin_password=admin_password,
injected_files=injected_files,
requested_networks=requested_networks,
security_groups=security_groups,
block_device_mapping=bdms, node=host['nodename'],
limits=host['limits'])
  看到xxxrpc立即想到对应的代码位置,位于compute/rpcapi模块,该方法向nova-compute发起RPC请求:





cctxt.cast(ctxt, 'build_and_run_instance', ...)
  由于是cast调用,因此发起的是异步RPC,因此nova-conductor任务结束,紧接着终于轮到nova-compute登场了。

S5 nova-compute
  到了nova-compute服务,入口为compute/manager.py,找到build_and_run_instance方法,该方法调用了driver的spawn方法,这里的driver就是各种hypervisor的实现,所有实现的driver都在virt目录下,入口为driver.py,比如libvirt driver实现对应为virt/libvirt/driver.py,找到spawn方法,该方法拉取镜像创建根磁盘、生成xml文件、define domain,启动domain等。最后虚拟机完成创建。nova-compute服务结束。

3 一张图总结
  以上是创建虚拟机的各个服务的交互过程以及调用关系,略去了很多细节。需要注意的是,所有的数据库操作,比如instance.save()以及update()操作,如果配置use_local为false,则会向nova-conductor发起RPC调用,由nova-conductor代理完成数据库更新,而不是直接由nova-compute更新数据库,这里的RPC调用过程在以上的分析中省略了。
  整个流程用一张图表示为:

  nova 启动虚拟及的组件调用
DSC0000.png

  方法调用
DSC0001.png

  qq 212966054

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-420556-1-1.html 上篇帖子: OpenStack(企业私有云)万里长征第三步 下篇帖子: openstack之安装文档
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表