如何閱讀OpenStack原始碼
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專案通用骨骼脈絡
閱讀原始碼的首要問題就是就要對程式碼的結構瞭然於胸,
需要強調的是,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呼叫過程在以上的分析中省略了。
整個流程用一張圖表示為:
如果你對OpenStack的其它服務以及操作流程感興趣,可以參考我的openstack-workflow專案, 這個專案是我本人在學習過程中記錄,繪製成序列圖,上圖就是其中一個例項。專案地址為:
https://
github。com/int32bit/ope
nstack-workflow
。