Nova架构概览
Nova是OpenStack社区最核心的项目,也是社区诞生之时就一直存在的项目,它主要提供计算资源的服务,这个计算资源包含了虚机以及配套的存储,网络等资源。我比较喜欢把OpenStack和Linux做类比,我们知道进程(Task)是处于执行期的程序以及相关资源的总称,如果把虚机类比为进程,Nova就类似于Linux中的进程管理和调度模块。所以Nova会和很多其他的组件交互,不仅包括OpenStack自身的Neutron,Glance, Cinder等组件,还有不同的Hypervisor 包括KVM, Xen等。
Nova的组件构成
Nova组件有以下六部分组成:
1) API服务器 API Server(Nova-api)
2) 计算工作者Compute Workers(Nova-compute)
3) 网络控制器Network Controller(Nova-network)
4) 卷工作者Volume Worker(Nova-volume)
5) 调度器Schedule(Nova-schedule)
6) 消息队列Message Queue(rabbitmq server)
上图是Nova的软件架构图,Nova中的各个组件(除了消息队列组件以外)都是有Python代码编写的守护进程,由上图可以看出每个进程之间通过队列(Queue)和数据库(Nova database)来交换信息。
下面对Nova的组件进行介绍。
1) API服务器 API Server(Nova-api)
Nova-API对外提供一个与云基础设施交互的接口,也是外部可用于管理基础设施的唯一组件。它负责发起相应的类似运行新虚拟机实例这样的资源调度活动。
在实现层面上,nova-api是python实现的WSGI应用。(WSGI即Web服务器网关接口是Python应用程序或框架和Web服务器之间的一种接口,已经被广泛接受,它已基本达成可移植性方面的目标)
2) 计算工作者Compute Workers(Nova-compute)
Nova-compute处理管理实例生命周期,负责对虚拟机实例进行创建、终止、迁移、Resize的操作。
工作原理:队列中接收请求→执行→更新数据库状态
3) 网络控制器Network Controller(Nova-network)
Nova-network负责处理主机的网络配置,其中包括:IP地址分配,配置vlan,实现安全组,配置计算节点网络等任务。
工作原理:队列中接收网络任务→控制虚拟机的网络(创建桥接网络、改变iptables规则)
4) 卷工作者Volume Worker(Nova-volume)
Nova-volume提供卷管理,为虚拟机实例提供额外的volume访问
用来管理基于逻辑卷管理的实例卷。一个实例的重要数据总是要写在卷上,这样确保能在以后访问。
5) 调度器Schedule(Nova-schedule)
Nova-Scheduler负责为虚拟机实例指定运行的物理服务器,主要负责调度资源,有多种调度方法供选择
通过适当的调度算法从可用资源池获得一个计算服务。
6) 消息队列Message Queue(rabbitmq server)
Openstack节点之间通过消息队列使用AMQP(高级消息队列协议)完成通信(异步通信)。
Rabbitmq是对这个协议的一个实现,默认使用kombu消息框架,该部分本文不进行详细展开,将在另外一篇文章中进行讲述。
Nova Compute Service启动流程
Nova的服务类型分为两种,WsgiService和RpcService,每一种服务类型都会根据nova.conf的配置启动一个或多个进程。这其中WsgiService主要是用于组件之间的Restful接口交互,而组件内部的不同模块采取RpcService交互模式。
Nova Compute 启动流程分析
这里先以Nova Compute进程的启动过程为例,在/nava/bin目录下为所有的启动脚本入口,对源代码进行走读分析。
OpenStack软件包管理
软件包管理是每个OpenStack项目的基础,其目的是用来将项目代码打包成源码包或者二进制包进行分发。一个项目的代码可能会被打包放到PyPI上,这样你可以通过 pip 命令安装这个包;也可能会被打包放到项目的软件仓库里,这样你可以通过 apt-get install 或者 yum install 来安装这个软件包。
OpenStack也是使用setuptools工具来进行打包,不过为了满足OpenStack项目的需求,引入了一个辅助工具 pbr (Python Build Reasonableness)来配合setuptools完成打包工作。pbr是一个setuptools的扩展工具,被开发出来的主要目的是为了方便使用setuptools,其项目文档地址也在OpenStack官网内: http://docs.openstack.org/developer/pbr/ 。
先说一下pbr如何使用:
import setuptools
setuptools.setup(setup_requires=['pbr'], pbr=True)
按照上面的方式就可以配置setuptools工具使用pbr来协助完成打包工作。这里的 setup_requires 参数意思是setup函数在执行之前需要依赖的包的列表。这里的依赖的包的功能可以理解为生成setup的实际参数。你可以看到,当使用pbr的时候,setup函数只有两个参数,然而实际上 setuptools.setup 函数实际上是 disutils.core.setup 函数,会接收任何参数,这些参数可以通过在调用时指定,也可以通过所依赖的扩展来生成(比如pbr)
setup.cfg
由于OpenStack项目都使用了setuptools和pbr来执行打包工作,因此项目的元数据都放在 setup.cfg 文件中。我们以 Compute项目的setup.cfg文件为例来说明这个文件里一般会包含什么内容:
[metadata]
name = nova
summary = Cloud computing fabric controller
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://docs.openstack.org/developer/nova/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
[global]# 全局段
setup-hooks =
pbr.hooks.setup_hook
[files] # 文件段
packages =
nova
[entry_points] # 指定入口点
oslo.config.opts =
nova.conf = nova.conf.opts:list_opts
oslo.config.opts.defaults =
nova.conf = nova.common.config:set_middleware_defaults
oslo.policy.enforcer =
nova = nova.policy:get_enforcer
oslo.policy.policies =
# The sample policies will be ordered by entry point and then by list
# returned from that entry point. If more control is desired split out each
# list_rules method into a separate entry point rather than using the
# aggregate method.
nova = nova.policies:list_rules
nova.compute.monitors.cpu =
virt_driver = nova.compute.monitors.cpu.virt_driver:Monitor
nova.image.download.modules =
file = nova.image.download.file
console_scripts = # 指定要生成的可执行文件
nova-compute = nova.cmd.compute:main
wsgi_scripts =
nova-placement-api = nova.api.openstack.placement.wsgi:init_application
[build_sphinx] # 文档build相关信息
all_files = 1
build-dir = doc/build
source-dir = doc/source
[build_apiguide] # 文档build相关信息
all_files = 1
build-dir = api-guide/build
source-dir = api-guide/source
[egg_info] # 指定egg信息
tag_build =
tag_date = 0
tag_svn_revision = 0
[compile_catalog]
directory = nova/locale
domain = nova nova-log-critical nova-log-error nova-log-info nova-log-warning
[update_catalog]
domain = nova
output_dir = nova/locale
input_file = nova/locale/nova.pot
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext
mapping_file = babel.cfg
output_file = nova/locale/nova.pot
[wheel]
universal = 1
[extras]
osprofiler =
osprofiler>=1.4.0 # Apache-2.0
[pbr]
warnerrors = true
(上面有些未注释的部分我目前还不太清楚,后续补充,可以先参考 PEP301 )
这里说说一下 classifier 这个参数。这个参数是用来指定一个软件包的分类、许可证、允许运行的操作系统、允许运行的Python的版本的信息。
entry_points是一个字典,从entry point组名映射到一个表示entry point的字符串或字符串列表。Entry points是用来支持动态发现服务和插件的,也用来支持自动生成脚本。
requirements.txt
这个文件指定了一个项目依赖的包有哪些,并且支出了依赖的包的版本需求
软件包归档格式
Python的软件包一开始是没有官方的标准分发格式的。比如Java有jar包或者war包作为分发格式,Python则什么都没有。后来不同的工具都开始引入一些比较通用的归档格式。比如,setuptools引入了Egg格式。但是,这些都不是官方支持的,存在元数据和包结构彼此不兼容的问题。因此,为了解决这个问题, PEP 427 定义了新的分发包标准,名为 Wheel 。目前pip和setuptools工具都支持Wheel格式。这里我们简单总结一下常用的分发格式:
- tar.gz 格式:这个就是标准压缩格式,里面包含了项目元数据和代码,可以使用 python setup.py sdist 命令生成。
- .egg 格式:这个本质上也是一个压缩文件,只是扩展名换了,里面也包含了项目元数据以及源代码。这个格式由setuptools项目引入。可以通过命令 python setup.py bdist_egg 命令生成。
- .whl 格式:这个是Wheel包,也是一个压缩文件,只是扩展名换了,里面也包含了项目元数据和代码,还支持免安装直接运行。whl分发包内的元数据和egg包是有些不同的。这个格式是由PEP 427引入的。可以通过命令 python setup.py bdist_wheel 生成。
Nova Compute启动入口
了解了OPS的打包规则可以知道Nova Compute的程序入口就是在nova/cmd/compute.py,先列举源码:
"""Starter script for Nova Compute."""
CONF = nova.conf.CONF
LOG = logging.getLogger('nova.compute')
def main():
config.parse_args(sys.argv)
logging.setup(CONF, 'nova')
priv_context.init(root_helper=shlex.split(utils.get_root_helper()))
utils.monkey_patch()
objects.register_all()
# Ensure os-vif objects are registered and plugins loaded
os_vif.initialize()
gmr.TextGuruMeditation.setup_autorun(version)
cmd_common.block_db_access('nova-compute')
objects_base.NovaObject.indirection_api = conductor_rpcapi.ConductorAPI()
server = service.Service.create(binary='nova-compute',
topic=CONF.compute_topic)
service.serve(server)
service.wait()
在这里首先会调用config.parse_args(sys.argv)函数来做一些初始化的工作,包括RpcServer的传输层Driver的指定等工作。
接下来调用Create()函数创建RPC Service,并且设置Topic为CONF.compute_topic,在【OpenStack源码分析之二】RabbitMQ分析中有详细讲述RPC的使用。Create()函数调用的实例化对象会设置一个ComputeManager来负责处理所有的Rpc请求,具体接口请阅读源码Nova/compute/manager/ComputeManager类。
后面就是Serve函数,它会分配一个协程(关于协程的介绍请见对Python协程的理解)来调用Service的Start()函数,接下来我们分析一下RpcService的Start()函数,源码如下:
def start(self):
verstr = version.version_string_with_package()
LOG.info(_LI('Starting %(topic)s node (version %(version)s)'),
{'topic': self.topic, 'version': verstr})
self.basic_config_check()
self.manager.init_host()
self.model_disconnected = False
ctxt = context.get_admin_context()
self.service_ref = objects.Service.get_by_host_and_binary(
ctxt, self.host, self.binary)
if self.service_ref:
_update_service_ref(self.service_ref)
else:
try:
self.service_ref = _create_service_ref(self, ctxt)
except (exception.ServiceTopicExists,
exception.ServiceBinaryExists):
# NOTE(danms): If we race to create a record with a sibling
# worker, don't fail here.
self.service_ref = objects.Service.get_by_host_and_binary(
ctxt, self.host, self.binary)
self.manager.pre_start_hook()
if self.backdoor_port is not None:
self.manager.backdoor_port = self.backdoor_port
LOG.debug("Creating RPC server for service %s", self.topic)
target = messaging.Target(topic=self.topic, server=self.host)
endpoints = [
self.manager,
baserpc.BaseRPCAPI(self.manager.service_name, self.backdoor_port)
]
endpoints.extend(self.manager.additional_endpoints)
serializer = objects_base.NovaObjectSerializer()
self.rpcserver = rpc.get_server(target, endpoints, serializer)
self.rpcserver.start()
self.manager.post_start_hook()
LOG.debug("Join ServiceGroup membership for this service %s",
self.topic)
# Add service to the ServiceGroup membership group.
self.servicegroup_api.join(self.host, self.topic, self)
if self.periodic_enable:
if self.periodic_fuzzy_delay:
initial_delay = random.randint(0, self.periodic_fuzzy_delay)
else:
initial_delay = None
self.tg.add_dynamic_timer(self.periodic_tasks,
initial_delay=initial_delay,
periodic_interval_max=
self.periodic_interval_max)
这段代码涉及到了Oslo_messaging库,oslo.messaging的产生就不多说了,因为RPC的调用在各个项目中都有,以前各个项目分别维护一坨类似的代码,为了简化工作、方便打包等,社区就把RPC相关的功能作为OpenStack的一个依赖库。另一方面,也为后续支持非AMQP协议的消息中间件(ZeroMQ)的引入打下基础。
其实oslo.messaging库就是把rabbitmq的python库做了封装,考虑到了编程友好、性能、可靠性、异常的捕获等诸多因素。让各个项目的开发者聚焦于业务代码的编写,而不用考虑消息如何发送和接收。这对于各个项目开发者来说当然是好事,但对于一套OpenStack系统的运维人员来说,封装就意味着很多细节被隐藏,为了能够解决消息转发过程中出现的问题,需要再花费时间和精力去理解oslo.messaging的业务逻辑,对于本来就错综复杂的OpenStack核心业务来说,无疑是雪上加霜。
这里有几个概念:
- target:作为消息发送者,需要在target中指定消息要发送到的exchange, binding-key, consumer等信息(这些概念可能与target对象属性不一样)
- serializer:负责消息的序列化处理。就是负责把Nova中的对象转换成可以在网络中传送的格式。
- TRANSPORT:处理消息发送的抽象层。根据rpc_backend的配置确定真正处理消息发送的driver。一般我们会用到这个:rabbit = oslo_messaging._drivers.impl_rabbit:RabbitDriver。对于RabbitDriver,其相关配置项都在/oslo_messaging/_drivers/impl_rabbit.py中,它内部会维护一个connection pool,管理Connection对象。
- Endpoint:Transport Driver接收到消息之后会进行分发处理,这里会有个Dispatcher分发给相应的Endpoint处理,Endpoint就是设置成前文提到的ComputeManager。
总结
- Nova和外部模块的交互通过Restful接口调用,内部接口间采用异步RPC调用,而且正因为是异步,所以Eventlet库可以配套使用;
- 在RPC模式下,多个Nova-compute节点通过组合键(topic=self.topic, server=self.host)形成Routing Key来进行Binding
- Transport指定了底层的传输层机制,当前支持RabbitMQ和ZeroMQ
参考文献:
https://docs.openstack.org/ocata/config-reference/compute.html
http://www.infoq.com/cn/articles/OpenStack-demo-packagemanagement
http://www.openstack.cn/?p=3514