现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

如何阅读OpenStack源码

2020-02-09 11:16 工业·编程 ⁄ 共 9813字 ⁄ 字号 暂无评论

1 OpenStack基础

1.1 OpenStack组件介绍

OpenStack是一个IaaS云计算平台开源实现,其对标产品为AWS。最开始OpenStack只有两个组件,分别为提供计算服务的Nova以及提供对象存储服务的Swift,其中Nova不仅提供计算服务,还包含了网络服务、块存储服务、镜像服务以及裸机管理服务。之后随着项目的不断发展,从Nova中根据功能拆分为多个独立的项目,如nova-volume拆分为Cinder项目提供块存储服务,nova-image拆分为Glance项目,提供镜像存储服务,nova-network则是neutron的前身,裸机管理也从Nova中分离出来为Ironic项目。最开始容器服务也是由Nova提供支持的,作为Nova的driver之一来实现,而后迁移到Heat,到现在已经独立为一个单独的项目Magnum,后来Magnum的愿景调整为主要提供容器编排服务,单纯的容器服务则由Zun项目接管。最开始OpenStack并没有认证功能,从E版开始才加入认证服务Keystone。

目前OpenStack基础服务组件如下:

· Keystone:认证服务。

· Glance:镜像服务。

· Nova:计算服务。

· Cinder:块存储服务。

· Neutorn:网络服务。

· Swift:对象存储服务。

E版之后,在这些核心服务之上,又不断涌现新的服务,如面板服务Horizon、编排服务Heat、数据库服务Trove、文件共享服务Manila、大数据服务Sahara、工作流服务Mistral以及前面提到的容器编排服务Magnum等,这些服务几乎都依赖于以上的基础服务。比如Sahara大数据服务会先调用Heat模板服务,Heat又会调用Nova创建虚拟机,调用Glance获取镜像,调用Cinder创建数据卷,调用Neutron创建网络等。

目前最新发布的版本为第15个版本,代号为Pike,Queens版本已经进入快速开发阶段。

OpenStack服务越来越多、越来越复杂,覆盖的技术生态越来越庞大,宛如一个庞然大物,刚接触如此庞大的分布式系统,都或多或少感觉有点如”盲人摸象”的感觉。不过不必先过于绝望,好在OpenStack项目具有非常良好的设计,虽然OpenStack项目众多,组件繁杂,但几乎所有的服务骨架脉络基本是一样的,熟悉了其中一个项目的架构,深入读了其中一个项目源码,再去看其它项目可谓轻车熟路

本文章会以Nova项目为例,一步一步剖析源码结构,希望读者阅读完之后再去看Cinder项目会是件非常轻松的事。

1.2 工欲善其事必先利其器

要阅读源代码首先需要安装科学的代码阅读工具,图形界面使用pycharm没有问题,不过通常在虚拟机中是没有图形界面的,首选vim,需要简单的配置使其支持代码跳转和代码搜索,可以参考GitHub - int32bit/dotfiles: A set of vim, zsh, git, and tmux configuration files

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

...

然后注意需要通过命令行直接运行,而不能通过systemd启动:

su -c 'nova-api' nova

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

1.3 OpenStack项目通用骨骼脉络

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调用过程在以上的分析中省略了。

如果你对OpenStack的其它服务以及操作流程感兴趣,可以参考我的openstack-workflow项目。

作者:int32bit

给我留言

留言无头像?