如何阅读OpenStack源码(更新版)

栏目: 服务器 · 发布时间: 5年前

内容简介:OpenStack是一个开源的IaaS实现方案,企业构建私有云的主流选择之一。截至到2019年4月,OpenStack已经有9年的发展历史了,最新发布的版本为第19个版本,代号为Stein,下一个版本最初OpenStack只有两个子项目,分别为Nova和Swift,其中Nova不仅提供计算服务,还包含了网络服务、块存储服务、镜像服务以及裸机管理服务。之后随着项目的不断发展,从Nova中根据功能拆分为多个独立的项目,如nova-volume拆分为Cinder项目提供块存储服务,nova-image拆分为Gla

1 OpenStack介绍

OpenStack是一个开源的IaaS实现方案,企业构建私有云的主流选择之一。截至到2019年4月,OpenStack已经有9年的发展历史了,最新发布的版本为第19个版本,代号为Stein,下一个版本 Train 目前已经处于开发阶段,预计今年10月发布。

最初OpenStack只有两个子项目,分别为Nova和Swift,其中Nova不仅提供计算服务,还包含了网络服务、块存储服务、镜像服务以及裸机管理服务。

之后随着项目的不断发展,从Nova中根据功能拆分为多个独立的项目,如nova-volume拆分为Cinder项目提供块存储服务,nova-image拆分为Glance项目,提供镜像存储服务,nova-network则是neutron的前身,裸机管理也从Nova中分离出来为Ironic项目。

最开始容器服务也是由Nova提供支持的,作为Nova的Hypervisor driver实现,而后容器部分功能迁移到Heat,容器部署在虚拟机中。现在容器管理功能已经独立为一个单独的项目Magnum,提供容器编排服务,容器服务则由Zun项目负责。

目前OpenStack几个核心基础组件如下:

  • Keystone:认证服务。
  • Glance:镜像服务。
  • Nova:计算服务。
  • Cinder:块存储服务。
  • Neutorn:网络服务。
  • Swift:对象存储服务。

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

OpenStack项目越来越多,功能越来越全面,同时服务也越来越复杂,覆盖的技术生态越来越庞大,初次接触OpenStack感觉面临一个庞然大物,总会有种如”盲人摸象”的感觉。

不过不必先过于绝望,好在OpenStack项目具有非常良好的设计理念,虽然OpenStack项目众多,组件繁杂,但几乎所有的服务骨架脉络基本是一样的,熟悉了其中一个项目的架构,深入阅读了其中一个项目源码,再去学其他OpenStack项目自然会轻松很多。

本文接下来以Nova项目为例,一步一步剖析源码结构,阅读完之后,你再去看Cinder项目,发现会有一种轻车熟路的感觉。

2 工欲善其事必先利其器

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

如何阅读OpenStack源码(更新版)

3 OpenStack开发与测试基础

3.1 OpenStack项目源码入口导航

OpenStack所有项目都是基于 Python 语言开发,遵循Python标准Distutils,使用setuptools工具管理项目。

想知道一个项目有哪些服务组成,入口函数(main函数)在哪里,最直接的方式就是查看项目根目录下的 setup.cfg 文件,其中 console_scripts 就是所有服务组件的入口,它就像一个十字路口导航,告诉你目的地的入口在哪里,哪条路通向哪里。

比如Nova的 setup.cfgconsole_scripts 如下:

[entry_points]
...
console_scripts =
    console_scripts =
    nova-api = nova.cmd.api:main
    nova-api-metadata = nova.cmd.api_metadata:main
    nova-compute = nova.cmd.compute:main
    nova-conductor = nova.cmd.conductor:main
    nova-placement-api = nova.api.openstack.placement.wsgi:init_application
    ...

数了下目前最新的Nova大概有22个 main 函数入口,由此可知Nova项目安装后会包含22个可执行程序,其中 nova-compute 服务的入口函数为 nova/cmd/compute.py ( . -> / )模块的 main 函数:

def main():
    config.parse_args(sys.argv)
    logging.setup(CONF, 'nova')
    priv_context.init(root_helper=shlex.split(utils.get_root_helper()))
    objects.register_all()
    gmr_opts.set_defaults(CONF)
    # Ensure os-vif objects are registered and plugins loaded
    os_vif.initialize()

    gmr.TextGuruMeditation.setup_autorun(version, conf=CONF)

    cmd_common.block_db_access('nova-compute')
    objects_base.NovaObject.indirection_api = conductor_rpcapi.ConductorAPI()
    objects.Service.enable_min_version_cache()
    server = service.Service.create(binary='nova-compute',
                                    topic=compute_rpcapi.RPC_TOPIC)
    service.serve(server)
    service.wait()
    service.wait()

其它服务依次类推。

3.2 OpenStack开发测试环境准备

由于OpenStack使用Python语言开发,而Python是动态类型语言,参数类型只能在运行时确定,不容易从代码中看出,因此必须部署一个allinone的OpenStack开发测试环境,建议使用RDO部署: Packstack quickstart ,当然乐于折腾使用DevStack、Kolla也是没有问题的。

3.3 OpenStack代码调试

要想深入研究源码,最有效的方式就是一步一步跟踪代码执行,因此会使用debugger工具是关键技能之一。Python的debugger工具有很多,为了简便起见,pdb工具就够了。

使用方法也非常简单,只要在你想设置断点的地方,嵌入以下代码:

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'])
    description = name
    ...

然后注意需要通过命令行直接在终端运行 nova-api 服务,而不能通过systemd在后台启动:

su -c 'nova-api' nova

此时在另一个终端创建一个新的虚拟机,调用创建虚拟机API,nova-api进程就会马上弹出pdb shell,此时你可以通过 s 或者 n 命令一步一步执行了。更多关于OpenStack调试技巧可参考我的另一篇文章 《OpenStack断点调试方法总结》

4 OpenStack项目代码框架

阅读源码的首要问题就是就要对代码的结构了然于胸, 需要强调的是,OpenStack项目的目录结构并不是根据组件严格划分,而是根据功能划分 ,以Nova为例, nova/compute 目录并不是一定在nova-compute节点上运行,而主要是和compute相关(虚拟机操作相关)的功能实现,同样的,scheduler目录代码并不全在scheduler服务节点运行,但主要是和调度相关的代码。不过目录结构遵循一定的规律。

通常一个OpenStack项目的代码目录都会包含 api.pyrpcapi.pymanager.py ,这三个是最重要的模块。

api.py
rpcapi.py
manager.py

比如对一个虚拟机执行关机操作:

API节点
nova-api接收用户请求 -> nova-api调用compute/api.py -> compute/api调用compute/rpcapi.py -> rpcapi.py向目标计算节点发起stop_instance()RPC请求

计算节点
收到stop_instance()请求 -> 调用compute/manager.py的callback方法stop_instance() -> 调用libvirt关机虚拟机

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

  • nova/cmd :这是服务的启动脚本,即所有服务的main函数。看服务怎么初始化,就从这里开始。
  • nova/db : 封装数据库访问,目前支持的driver为sqlalchemy。
  • nova/conf :Nova所有配置项声明都放在这个目录。
  • nova/locale : 本地化处理。
  • nova/image : 封装Glance接口。
  • nova/network : 封装Neutron接口。
  • nova/volume : 封装Cinder接口。
  • nova/virt : 这是支持的所有虚拟化驱动实现,即compute driver实现,主流的如 libvirthypervironicvmwareapi 等。
  • nova/objects : 对象模型,封装了所有Nova对象的CURD操作,相对以前直接调用db的model更安全,并且支持版本控制。
  • nova/policies : API policy集合。
  • nova/tests : 测试代码,如单元测试、功能测试。
  • nova/hacking : Nova代码规范定义的一些规则。

以上同样适用于其它服务,比如Cinder等。

另外需要了解的是,所有的API入口都是从xxx-api开始的,RESTFul API是OpenStack服务的唯一入口,也就是说,阅读源码就从api开始。

而api组件也是根据实体划分的,不同的实体对应不同的controller,比如servers、flavors、keypairs等, controllerindex 方法对应 list 操作、 show 方法对应 get 操作、 create 对应创建操作、 delete 对应删除操作、 update 对应更新操作等。

根据进程阅读源码并不是什么好的实践,因为光理解服务如何初始化、如何通信、如何发送心跳等就很不容易,各种高级封装太复杂了。我认为比较好的阅读源码方式是追踪一个任务的执行过程,比如跟踪启动虚拟机的整个流程,因此接下来本文将以创建一台虚拟机为例,一步步分析其过程。

5 实践案例:Nova创建虚拟机过程分析

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

5.1 nova-api

根据前面的理论,创建虚拟机的入口为 nova/api/openstack/compute/servers.pycreate 方法,该方法检查了一堆参数以及policy后,调用 compute_apicreate() 方法。

def create(self, req, body):
    """Creates a new server for a given user."""
    # ... 省略部分代码
    try:
        inst_type = flavors.get_flavor_by_flavor_id(
                flavor_id, ctxt=context, read_deleted="no")

        supports_multiattach = common.supports_multiattach_volume(req)
        supports_port_resource_request = \
            common.supports_port_resource_request(req)
        (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,
            check_server_group_quota=True,
            supports_multiattach=supports_multiattach,
            supports_port_resource_request=supports_port_resource_request,
                **create_kwargs)
    except (exception.QuotaError,
            exception.PortLimitExceeded) as error:
            # ...

这里的 compute_api 即前面说的 nova/compute/api.py 模块,找到该模块的 create 方法,该方法会创建数据库记录、检查参数等,然后调用 compute_task_apischedule_and_build_instances 方法:

@hooks.add_hook("create_instance")
def create(...):
    """Provision instances, sending instance information to the
    scheduler.  The scheduler will determine where the instance(s)
    go and will handle creating the DB entries.

    Returns a tuple of (instances, reservation_id)
    """
    # ...
    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,
        tags=tags)

compute_task_api 即conductor的 api.py 。conductor的api并没有执行什么操作,直接调用了 conductor_compute_rpcapischedule_and_build_instances 方法:

def schedule_and_build_instances(self, context, build_requests,
                                 request_spec, image,
                                 admin_password, injected_files,
                                 requested_networks, block_device_mapping,
                                 tags=None):
    self.conductor_compute_rpcapi.schedule_and_build_instances(
        context, build_requests, request_spec, image,
        admin_password, injected_files, requested_networks,
        block_device_mapping, tags)

该方法即conductor RPC调用api,即 nova/conductor/rpcapi.py 模块,该方法除了一堆的版本检查,剩下的就是对RPC调用的封装,代码只有两行:

def schedule_and_build_instances(...):
    cctxt = self.client.prepare(version=version)
    cctxt.cast(context, 'schedule_and_build_instances', **kw)

其中 cast 表示异步调用, schedule_and_build_instances 是RPC调用的方法, kw 是传递的参数。参数是字典类型,没有复杂对象结构,因此不需要特别的序列化操作。

截至到现在,虽然目录由 api->compute->conductor ,但仍在nova-api进程中运行,直到cast方法执行,该方法由于是异步调用,会立即返回,不会等待RPC返回,因此nova-api任务完成,此时会响应用户请求,虚拟机状态为 building

5.2 nova-conductor

由于是向nova-conductor发起的RPC调用,而前面说了接收端肯定是 manager.py ,因此进程跳到 nova-conductor 服务,入口为 nova/conductor/manager.pyschedule_and_build_instances 方法。

该方法首先调用了 _schedule_instances 方法,该方法首先调用了 scheduler_clientselect_destinations 方法:

def schedule_and_build_instances(...):
    # Add all the UUIDs for the instances
    instance_uuids = [spec.instance_uuid for spec in request_specs]
    try:
        host_lists = self._schedule_instances(context, request_specs[0],
                instance_uuids, return_alternates=True)
    except Exception as exc:
        ...
        
def _schedule_instances(self, context, request_spec,
                        instance_uuids=None, return_alternates=False):
    scheduler_utils.setup_instance_group(context, request_spec)
    with timeutils.StopWatch() as timer:
        host_lists = self.query_client.select_destinations(
            context, request_spec, instance_uuids, return_objects=True,
            return_alternates=return_alternates)
    LOG.debug('Took %0.2f seconds to select destinations for %s '
              'instance(s).', timer.elapsed(), len(instance_uuids))
    return host_lists

scheduler_clientcompute_api 以及 compute_task_api 都是一样对服务的client封装调用,不过scheduler没有 api.py 模块,而是有个单独的client目录,实现在 nova/scheduler/client 目录的 query.py 模块, select_destinations 方法又很直接的调用了 scheduler_rpcapiselect_destinations 方法,终于又到了RPC调用环节。

def select_destinations(...):
    return self.scheduler_rpcapi.select_destinations(context, ...)

毫无疑问,RPC封装同样是在 nova/schedulerrpcapi.py 中实现。该方法RPC调用代码如下:

def select_destinations(self, ...):
    # Modify the parameters if an older version is requested
    # ...
    cctxt = self.client.prepare(
        version=version,
        call_monitor_timeout=CONF.rpc_response_timeout,
        timeout=CONF.long_rpc_timeout)
    return cctxt.call(ctxt, 'select_destinations', **msg_args)

注意这里调用的是 call 方法,说明这是同步RPC调用,此时 nova-conductor 并不会退出,而是等待直到 nova-scheduler 返回。因此当前nova-conductor为堵塞状态,等待 nova-scheduler 返回,此时 nova-scheduler 接管任务。

5.3 nova-scheduler

同理找到scheduler的manager.py模块的 select_destinations 方法,该方法会调用driver方法:

@messaging.expected_exceptions(exception.NoValidHost)
def select_destinations(self, ctxt, ...):
    # ...
    selections = self.driver.select_destinations(ctxt, spec_obj,...)
    return selections

这里的 driver 其实就是调度驱动,在配置文件中 scheduler 配置组指定,默认为 filter_scheduler ,对应 nova/scheduler/filter_scheduler.py 模块,该算法根据指定的filters过滤掉不满足条件的计算节点,然后通过 weigh 方法计算权值,最后选择权值高的作为候选计算节点返回。调度算法实现这里不展开,感兴趣的可以阅读。

最后nova-scheduler返回调度的 hosts 集合,任务结束。由于nova-conductor通过同步方法调用的该方法,因此nova-scheduler会把结果返回给nova-conductor服务。

5.4 nova-condutor

nova-conductor等待nova-scheduler返回后,拿到调度的计算节点列表,回到 scheduler/manager.pyschedule_and_build_instances 方法。

因为可能同时启动多个虚拟机,因此循环调用了 compute_rpcapibuild_and_run_instance 方法:

for (build_request, request_spec, host_list, instance) in zipped:
    # ...
    with obj_target_cell(instance, cell) as cctxt:
        # ...
        with obj_target_cell(instance, cell) as cctxt:
            self.compute_rpcapi.build_and_run_instance(
                    cctxt, ..., host_list=host_list)

看到xxxrpc立即想到对应的代码位置,位于 nova/compute/rpcapi 模块,该方法向nova-compute发起RPC请求:

def build_and_run_instance(self, ctxt, ...):
    # ...
    client = self.router.client(ctxt)
    version = '5.0'
    cctxt = client.prepare(server=host, version=version)
    cctxt.cast(ctxt, 'build_and_run_instance', **kwargs)

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

5.5 nova-compute

终于等到nova-compute服务,服务入口为 nova/compute/manager.py ,找到 build_and_run_instance 方法,该方法调用关系如下:

build_and_run_instance()
  -> _locked_do_build_and_run_instance()
  -> _do_build_and_run_instance()
  -> _build_and_run_instance()
  -> driver.spawn()

这里的 driver 就是compute driver,通过 compute 配置组的 compute_driver 指定,这里为 libvirt.LibvirtDriver ,代码位于 nova/virt/libvirt/driver.py ,找到 spawn() 方法,该方法调用Libvirt创建虚拟机,并等待虚拟机状态为 Active ,nova-compute服务结束,整个创建虚拟机流程也到此结束。

6 总结

以上是创建虚拟机的各个服务的交互过程以及调用关系,需要注意的是,所有的数据库操作,比如 instance.save() 以及 update 操作,如果配置 use_localfalse ,则会向 nova-conductor 发起RPC调用,由 nova-conductor 代理完成数据库更新,而不是由 nova-compute 直接访问数据库,这里的RPC调用过程在以上的分析中省略了。

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


以上所述就是小编给大家介绍的《如何阅读OpenStack源码(更新版)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Database Design and Implementation

Database Design and Implementation

Edward Sciore / Wiley / 2008-10-24 / 1261.00 元

* Covering the traditional database system concepts from a systems perspective, this book addresses the functionality that database systems provide as well as what algorithms and design decisions will......一起来看看 《Database Design and Implementation》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具