如何閱讀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]n...nconsole_scripts =n nova-all = nova.cmd.all:mainn nova-api = nova.cmd.api:mainn nova-api-metadata = nova.cmd.api_metadata:mainn nova-api-os-compute = nova.cmd.api_os_compute:mainn nova-cells = nova.cmd.cells:mainn nova-cert = nova.cmd.cert:mainn nova-compute = nova.cmd.compute:mainn nova-conductor = nova.cmd.conductor:mainn nova-console = nova.cmd.console:mainn nova-consoleauth = nova.cmd.consoleauth:mainn nova-dhcpbridge = nova.cmd.dhcpbridge:mainn nova-idmapshift = nova.cmd.idmapshift:mainn nova-manage = nova.cmd.manage:mainn nova-network = nova.cmd.network:mainn nova-novncproxy = nova.cmd.novncproxy:mainn nova-rootwrap = oslo_rootwrap.cmd:mainn nova-rootwrap-daemon = oslo_rootwrap.cmd:daemonn nova-scheduler = nova.cmd.scheduler:mainn nova-serialproxy = nova.cmd.serialproxy:mainn nova-spicehtml5proxy = nova.cmd.spicehtml5proxy:mainn nova-xvpvncproxy = nova.cmd.xvpvncproxy:mainn...n
由此可知nova項目安裝後會包含21個可執行程序,其中nova-compute服務的入口函數為nova/cmd/compute.py模塊的main函數:
def main():n config.parse_args(sys.argv)n logging.setup(CONF, nova)n utils.monkey_patch()n objects.register_all()nn gmr.TextGuruMeditation.setup_autorun(version)nn if not CONF.conductor.use_local:n block_db_access()n objects_base.NovaObject.indirection_api = n conductor_rpcapi.ConductorAPI()n else:n LOG.warning(_LW(Conductor local mode is deprecated and will n be removed in a subsequent release))nn server = service.Service.create(binary=nova-compute,n topic=CONF.compute_topic,n db_allowed=CONF.conductor.use_local)n service.serve(server)n service.wait()n
其它服務依次類推。
OpenStack使用Python語言開發,而Python是動態類型語言,參數類型不容易從代碼中看出,因此部署一個allinone的OpenStack開發測試環境非常有必要,建議使用RDO部署:Packstack quickstart,當然樂於折騰使用DevStack也是沒有問題的。
要想深入研究源碼,最有效的方式就是一步一步跟蹤代碼執行,因此會使用debug工具是關鍵技能之一。Python的debug工具有很多,為了簡便起見,pdb工具就夠了,你也可以嘗試ipdb、ptpdb之類的調試工具。使用方法也非常簡單,只要在你想設置斷點的地方,嵌入以下代碼:
import pdb; pdb.set_trace()n
然後在命令行(不能通過systemd啟動)直接運行服務即可。
假如想跟蹤nova創建虛擬機的過程,首先nova/api/openstack/compute/servers.py模塊的create方法打上斷點,如下:
def create(self, req, body):n """Creates a new server for a given user."""nn import pdb; pdb.set_trace() # 設置斷點n context = req.environ[nova.context]n server_dict = body[server]n password = self._get_server_admin_password(server_dict)n name = common.normalize_name(server_dict[name])nn if api_version_request.is_supported(req, min_version=2.19):n if description in server_dict:n # This is allowed to be Nonen description = server_dict[description]n else:n # No default descriptionn description = Nonen else:n description = namen ...n
然後注意需要通過命令行直接運行,而不能通過systemd啟動:
su -c nova-api novan
此時調用創建虛擬機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節點nnova-api接收用戶請求 -> nova-api調用compute/api.pyn-> compute/api調用compute/rpcapi.py -> rpcapi.py向目標計算節點發起stop_instance()RPC請求nn計算節點n收到MQ RPC消息 -> 解析stop_instance()請求 -> 調用compute/manager.py的callback方法stop_instance() -> 調用libvirt關機虛擬機n
前面提到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):n """Creates a new server for a given user."""nn context = req.environ[nova.context]n server_dict = body[server]n password = self._get_server_admin_password(server_dict)n name = common.normalize_name(server_dict[name])nn ...nn flavor_id = self._flavor_id_from_req_data(body)n try:n inst_type = flavors.get_flavor_by_flavor_id(n flavor_id, ctxt=context, read_deleted="no")nn (instances, resv_id) = self.compute_api.create(context,n inst_type,n image_uuid,n display_name=name,n display_description=description,n availability_zone=availability_zone,n forced_host=host, forced_node=node,n metadata=server_dict.get(metadata, {}),n admin_password=password,n requested_networks=requested_networks,n check_server_group_quota=True,n **create_kwargs)n except (exception.QuotaError,n exception.PortLimitExceeded) as error:n raise exc.HTTPForbidden(n explanation=error.format_message())n...n
這裡的compute_api即前面說的nova/compute/api.py模塊,找到該模塊的create方法,該方法會創建資料庫記錄、檢查參數等,然後調用compute_task_api的build_instances方法:
self.compute_task_api.schedule_and_build_instances(n context,n build_requests=build_requests,n request_spec=request_specs,n image=boot_meta,n admin_password=admin_password,n injected_files=injected_files,n requested_networks=requested_networks,n block_device_mapping=block_device_mapping)n
compute_task_api即conductor的api.py。conductor的api並沒有執行什麼操作,直接調用了conductor_compute_rpcapi的build_instances方法:
def schedule_and_build_instances(self, context, build_requests,n request_spec, image,n admin_password, injected_files,n requested_networks, block_device_mapping):n self.conductor_compute_rpcapi.schedule_and_build_instances(n context, build_requests, request_spec, image,n admin_password, injected_files, requested_networks,n block_device_mapping)n
該方法就是conductor RPC API,即nova/conductor/rpcapi.py模塊,該方法除了一堆的版本檢查,剩下的就是對RPC調用的封裝,代碼只有兩行:
cctxt = self.client.prepare(version=version)ncctxt.cast(context, build_instances, **kw)n
其中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):n scheduler_utils.setup_instance_group(context, request_spec,n filter_properties)n # TODO(sbauza): Hydrate here the object until we modify then # scheduler.utils methods to directly use the RequestSpec objectn spec_obj = objects.RequestSpec.from_primitives(n context, request_spec, filter_properties)n hosts = self.scheduler_client.select_destinations(context, spec_obj)n return hostsn
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):n scheduler_utils.setup_instance_group(context, request_spec,n filter_properties)n # TODO(sbauza): Hydrate here the object until we modify then # scheduler.utils methods to directly use the RequestSpec objectn spec_obj = objects.RequestSpec.from_primitives(n context, request_spec, filter_properties)n hosts = self.scheduler_client.select_destinations(context, spec_obj)n return hostsn
毫無疑問,RPC封裝同樣是在scheduler的rpcapi中實現。該方法RPC調用代碼如下:
return cctxt.call(ctxt, select_destinations, **msg_args)n
注意這裡調用的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):n instance.availability_zone = (n availability_zones.get_host_availability_zone(context,n host[host]))n try:n # NOTE(danms): This saves the az change above, refreshes ourn # instance, and tells us if it has been deleted underneath usn instance.save()n except (exception.InstanceNotFound,n exception.InstanceInfoCacheNotFound):n LOG.debug(Instance deleted during build, instance=instance)n continuen ...n self.compute_rpcapi.build_and_run_instance(context,n instance=instance, host=host[host], image=image,n request_spec=request_spec,n filter_properties=local_filter_props,n admin_password=admin_password,n injected_files=injected_files,n requested_networks=requested_networks,n security_groups=security_groups,n block_device_mapping=bdms, node=host[nodename],n limits=host[limits])n
看到xxxrpc立即想到對應的代碼位置,位於compute/rpcapi模塊,該方法向nova-compute發起RPC請求:
cctxt.cast(ctxt, build_and_run_instance, ...)n
由於是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/openstack-workflow。
推薦閱讀:
※數據中心第三方服務、金融IT外包服務、社保醫療信息化解決方案,這三類業務是什麼關係,區別在哪?
※目前常見的雲服務有哪些,分別適用於什麼場景?
※AWS F1正式上線,深度解析FPGA與公有雲的親密接觸
※答讀者問:網吧怎麼用MATLAB?
※開啟新征程 金蝶全力向雲轉型只是個偽命題