超详细教你如何阅读 OpenStack 源码?
The following article is from int32bit Author int32bit
点击上方“Python编程时光”,选择“加为星标”
第一时间关注Python技术干货!
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.。如图:
3. OpenStack开发基础
3.1 OpenStack项目源码入口导航
OpenStack所有项目都是基于Python语言开发,遵循Python标准Distutils,使用setuptools工具管理项目。
想知道一个项目有哪些服务组成,入口函数(main函数)在哪里,最直接的方式就是查看项目根目录下的setup.cfg文件,其中console_scripts就是所有服务组件的入口,它就像一个十字路口导航,告诉你目的地的入口在哪里,哪条路通向哪里。
比如Nova的setup.cfg的console_scripts如下:
数了下目前最新的Nova大概有22个main函数入口,由此可知Nova项目安装后会包含22个可执行程序,其中nova-compute服务的入口函数为nova/cmd/compute.py(. -> /)模块的main函数:
其它服务依次类推。
3.2 OpenStack开发测试环境准备
由于OpenStack使用Python语言开发,而Python是动态类型语言,参数类型只能在运行时确定,不容易从代码中看出,因此必须部署一个allinone的OpenStack开发测试环境,建议使用RDO部署:Packstack quickstart,当然乐于折腾使用DevStack、Kolla也是没有问题的。
3.3 OpenStack代码调试
要想深入研究源码,最有效的方式就是一步一步跟踪代码执行,因此会使用debugger工具是关键技能之一。Python的debugger工具有很多,为了简便起见,pdb工具就够了。
使用方法也非常简单,只要在你想设置断点的地方,嵌入一行代码:
然后注意需要通过命令行直接在终端运行nova-api服务,而不能通过systemd在后台启动:
此时在另一个终端创建一个新的虚拟机,调用创建虚拟机API,nova-api进程就会马上弹出pdb shell,此时你可以通过s或者n命令一步一步执行了。更多关于OpenStack调试技巧可参考我的另一篇文章《OpenStack断点调试方法总结》。
4. OpenStack项目代码框架
阅读源码的首要问题就是就要对代码的结构了然于胸,需要强调的是,OpenStack项目的目录结构并不是根据组件严格划分,而是根据功能划分,以Nova为例,nova/compute目录并不是一定在nova-compute节点上运行,而主要是和compute相关(虚拟机操作相关)的功能实现,同样的,scheduler目录代码并不全在scheduler服务节点运行,但主要是和调度相关的代码。不过目录结构遵循一定的规律。
通常一个OpenStack项目的代码目录都会包含api.py、rpcapi.py、manager.py,这三个是最重要的模块。
api.py:通常是供其它组件调用的封装库。换句话说,该模块通常并不会由本模块调用。比如compute目录的api.py,通常由nova-api服务的controller调用。可以简单认为是供其他服务调用的sdk。
rpcapi.py:这个是RPC请求的封装,或者说是RPC封装的client端,该模块封装了RPC请求调用。
manager.py:这个才是真正服务的功能实现,也是RPC的server端,即处理RPC请求的入口,实现的方法通常和rpcapi实现的方法一一对应。
比如对一个虚拟机执行关机操作:
前面提到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实现,主流的如libvirt、hyperv、ironic、vmwareapi等。
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等,controller的index方法对应list操作、show方法对应get操作、create对应创建操作、delete对应删除操作、update对应更新操作等。
根据服务阅读源码并不推荐,因为光理解服务如何初始化、如何通信、如何发送心跳等就很不容易,各种高级封装太复杂了。
我认为比较好的阅读源码方式是追踪一个任务的执行过程,比如跟踪启动虚拟机的整个流程,因此接下来本文将以创建一台虚拟机为例,一步步分析其过程。
5. 实践案例:Nova创建虚拟机过程分析
这里以创建虚拟机过程为例,根据前面的理论基础,一步步跟踪其执行过程。需要注意的是,Nova支持同时创建多台虚拟机,因此在调度时需要同时选择调度多个宿主机。
5.1 nova-api
根据前面的理论,创建虚拟机的入口为nova/api/openstack/compute/servers.py的create方法,该方法检查了一堆参数以及policy后,调用compute_api的create()方法。
这里的compute_api即前面说的nova/compute/api.py模块,找到该模块的create方法,该方法会创建数据库记录、检查参数等,然后调用compute_task_api的schedule_and_build_instances方法:
compute_task_api即conductor的api.py。conductor的api并没有执行什么操作,直接调用了conductor_compute_rpcapi的schedule_and_build_instances方法:
该方法即conductor RPC调用api,即nova/conductor/rpcapi.py模块,该方法除了一堆的版本检查,剩下的就是对RPC调用的封装,代码只有两行:
其中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.py的schedule_and_build_instances方法。
该方法首先调用了_schedule_instances方法,该方法首先调用了scheduler_client的select_destinations方法:
scheduler_client和compute_api以及compute_task_api都是一样对服务的client封装调用,不过scheduler没有api.py模块,而是有个单独的client目录,实现在nova/scheduler/client目录的query.py模块,select_destinations方法又很直接的调用了scheduler_rpcapi的select_destinations方法,终于又到了RPC调用环节。
毫无疑问,RPC封装同样是在nova/scheduler的rpcapi.py中实现。该方法RPC调用代码如下:
注意这里调用的是call方法,说明这是同步RPC调用,此时nova-conductor并不会退出,而是等待直到nova-scheduler返回。因此当前nova-conductor为堵塞状态,等待nova-scheduler返回,此时nova-scheduler接管任务。
5.3 nova-scheduler
同理找到scheduler的manager.py模块的select_destinations方法,该方法会调用driver方法:
这里的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.py的schedule_and_build_instances方法。
因为可能同时启动多个虚拟机,因此循环调用了compute_rpcapi的build_and_run_instance方法:
看到xxxrpc立即想到对应的代码位置,位于nova/compute/rpcapi模块,该方法向nova-compute发起RPC请求:
由于是cast调用,因此发起的是异步RPC,因此nova-conductor任务结束,紧接着终于轮到nova-compute服务登场了。
5.5 nova-compute
终于等到nova-compute服务,服务入口为nova/compute/manager.py,找到build_and_run_instance方法,该方法调用关系如下:
这里的driver就是compute driver,通过compute配置组的compute_driver指定,这里为libvirt.LibvirtDriver,代码位于nova/virt/libvirt/driver.py,找到spawn()方法,该方法调用Libvirt创建虚拟机,并等待虚拟机状态为Active,nova-compute服务结束,整个创建虚拟机流程也到此结束。
6. 总结一下
本文首先简单介绍了OpenStack项目,然后介绍了OpenStack的基础和框架,最后通过Nova创建虚拟机为例分析OpenStack源码。
需要注意的是以上创建虚拟机的各个服务的交互过程以及调用关系,涉及数据库的操作,比如instance.save()
以及update
操作,如果配置use_local
为false
,则会向nova-conductor
发起RPC调用,由nova-conductor
代理完成数据库更新,而不是由nova-compute
直接访问数据库,这里的RPC调用过程在以上的分析中省略了。另外,在调度过程中调用placement api也省略了。
推荐阅读