openstack開發實踐(四):Nova源碼解析

nova-api服務解析

nova-api服務介紹

nova作爲openstack雲中的計算組織控制器,負責支持openstack雲中實例生命週期的所有活動,先前在nova組件手動安裝https://blog.csdn.net/weixin_41977332/article/details/104031168 中介紹了nova架構中的各個服務組件及其功能。在nova的設計過程中,很注重各個服務組件之間的解耦性。nova內部不同服務之間是通過消息隊列來進行信息中轉(關於消息隊列的內容後續將有一篇文章專門進行介紹),而nova和外部其他組件諸如glance,neutron等則是通過REST API接口進行REST通信(關於RESTful API框架後續將有一篇文章進行介紹),在nova中這個api服務即通過nova-api實現。nova-api啓動時會被初始化爲一個WSGI Server,然後nova-api通過nova,然後nova-api通過nova/api/openStack:APIRouterV21(如下圖所示)與多個WSGI Application相關聯,即當有外部請求到達nova-api後,這個請求會通過APIRouterV21的父類nova/wsgi:Router發送到相應的WSGI Application中。每一個WSGI Application都會有相應的Controller並在該類中實現相應的執行方法(nova-api中該類在nova/api/openstack/compute/servers:ServersController,如下圖二所示),從Controller中相應的方法開始,Nova內部的通信機制正式運行,即請求到達Nova內部並被內部相關服務處理。總之nova-api作爲WSGI Server,一方面可以接收外部的請求,另一方面可以調用內部相關服務處理外部來的請求。
圖一
圖二

nova-api服務啓動流程

nova-api作爲Nova組件的入口,它在啓動過程中會進行許多初始化的操作,例如加載API、創建WSGI Server、加載策略等。對於openstack的所有組件而言,我們學習一個組件的源碼都可以從setup.cfg文件開始,該文件又叫做openstack組件的結構地圖,從這裏可以找到腳本的入口,如下圖所示
在這裏插入圖片描述
我們可以看出。啓動nova-api服務時,首先調用的就是nova.cmd.api中的main()方法,該方法的關鍵代碼如下圖所示。在這個方法中,首先會通過第41行的config.parse_args(sys.argv)讀取用戶的配置信息,該信息都會在nova/etc/nova.conf中進行設置,然後第54行通過service.process_launcher()創建一個ProcessLauncher的對象實例,該實例能夠啓動多個service子進程,接下來通過第59行service.WSGIService(api,use_ssl=should_use_ssl)來聲明一個WSGI實例,以API作爲參數傳入,而API的值是通過用戶通過enabled_apis設置的,之後通過第60行的launch_service()啓動WSGI Server。
在這裏插入圖片描述

nova-scheduler服務解析

nova-scheduler原理及代碼結構

nova-scheduler是nova中負責調度的服務,主要作用是將nova-api發送來的請求,通過一定的方式映射到某一計算節點上。它通過用戶設定的調度算法從系統資源池中篩選出一個計算節點用於創建虛擬機。nova-scheduler對於計算節點的選取分爲兩步,第一步是通過過濾器(Filter)選擇滿足條件的計算節點,第二步則是權重計算(Weight),通過計算權重,選出權值最小的計算節點並在其上創建虛擬機。nova-scheduler的代碼位與nova/scheduler,其代碼結構如下圖所示。過濾器和權重計算部分分別對應圖中的filters文件夾和weights文件夾,client文件夾中爲客戶端調用程序的入口,其他比較重要的文件包括driver.py(實現名爲Scheduler的類,它是nova-scheduler中所有調度器的基類,任何調度器都需要實現其中的接口),manager.py(實現名爲SchedulerManager的類,定義Host相關的管理操作函數,如刪除Host中的虛擬機信息等),host_manager.py(實現名爲HostManager和HostState兩個類,這兩個類描述了跟調度器相關的主機的操作實現),chance.py(實現名爲ChanceScheduler的類,繼承於driver.py中定義的Scheduler類,是一個主要實現隨機選取主機節點的調度器)。
在這裏插入圖片描述

調度過程

調度事件的觸發

虛擬機創建過程中,觸發nova-scheduler進行主機調度的服務是nova-conductor。當來自nova-api的創建虛擬機的請求發送到nova-conductor時(所謂的發送是指nova-api向消息隊列中發送build_instances的消息,nova/conductor/rpcapi:build_instance
會訂閱相關消息),會調用nova/conductor/manager.py:ComputeTaskManager的build_instance()方法,如下圖所示,該方法會在第529行調用_schedule_instances(context,request_spec,filter_properties),而_schedule_instances()方法會調用到第609行的scheduler_client.select_destinations(context,request_spec,instance_uuids)返回被選中的hosts,scheduler_client在ComputeTaskManager實例初始化時爲sheduler_client.SchedulerClient(),對應的是nova/scheduler/client/init.py中的SchedulerClient類,select_destinations也是類中的一個方法。
在這裏插入圖片描述
在這裏插入圖片描述

調度過程分析

在這裏插入圖片描述
調度過程的入口,爲nova/scheduler/client/init.py中的SchedulerClient類中的select_destinations方法,如上圖所示,該方法調用的是self.queryclient.select_destinations()方法,如下圖所示,該方法t通過scheduler_rpcapi.select_destinations(context,spec_obj)發送了一個RPC調用,如後一個圖所示,最後call的方法爲nova/scheduler/manager.py中的selection_destination方法
在這裏插入圖片描述
在這裏插入圖片描述
nova/scheduler/manager.py中的select_destinations()方法如下圖所示,第98行的self.driver.select_destinations()方法實現的功能就是過濾與計算權重,最終選出主機列表,具體方法實現的定義在nova/scheduler/filter_scheduler.py中的select_destinations(),如後一張圖所示,函數返回值爲按權重排序過的主機列表dests,按權重排序是通過第58行的self._schedule()方法來實現的。
在這裏插入圖片描述
在這裏插入圖片描述
得到按權重排序的主機列表之後,我們回到nova/conductor/manager.py:ComputeTaskManager的build_instance()方法(調度事件觸發部分的第一張圖),此時的hosts即爲該主機列表,該方法最後部分會調用self.compute_rpcapi_build_and_run_instance(),調動nova-compute服務,進入虛擬機創建過程,後續內容會在後面nova-compute虛擬機創建流程中進行介紹。
在這裏插入圖片描述

nova-compute服務解析

nova-compute服務的作用

nova-compute是nova中負責虛擬機生命週期管理的服務,通過底層的Libvirt驅動,實現虛擬化與openstack的結合,即從功能角度來說,nova-compute在nova中主要負責與Hypervisor進行交互,通過調用相應的Hypervisor的接口,實現對虛擬機的創建,刪除,掛起,resize等操作,換句話說,它可以與Hypervisor一起實現對虛擬機生命週期的管理。nova-compute可以使用不同的Hypervisor(最常用的爲基於KVM/Xen類型的Hypervisor),不同的Hypervisor可以通過修改位於部署了nova-compute的計算節點的/etc/nova/nova.conf中的compute_driver的值進行配置。
nova-compute創建虛擬機時,其主要任務就是通過用戶準備的各種參數,最終生成創建虛擬機的XML文件以供Hypervisor使用,主要步驟如下:

  1. 準備虛擬機所需要的資源(CPU資源,RAM資源以及鏡像資源等),網絡資源也會在這個階段一同分配
  2. 創建虛擬機的鏡像文件,nova-compute會調用glanceclient拉取鏡像,以該鏡像爲基礎創建鏡像文件。
  3. 生成創建虛擬機的XML文件
  4. 創建網絡並啓動虛擬機
    以上過程中生成的XML或者暫存在計算節點的鏡像都會存放在計算節點的/var/lib/nova/instances文件夾下以該虛擬機的UUID命名的子目錄中。

nova-compute服務的啓動流程

nova-compute服務的入口位於nova/cmd/compute.py,如下圖所示
在這裏插入圖片描述
其中第42行會讀取配置文件進行一些初始化的工作,第55行代碼用於創建RPC Service,並將此RPC Service的topic設置爲CONF.compute_topic,類Service的定義位於nova/service.py,如下所示,其中第116行爲實例化一個ComputeManager(nova/compute/manager.py)類,裏面定義了接受並處理各種RPC請求的方法。
在這裏插入圖片描述在這裏插入圖片描述
上圖爲service.Service.create()方法,通過提供的與RPC相關的topic等參數,返回該RPC service類。在nova/cmd/compute.py的第57行service.serve(server)用於啓動RPC Service(),service.serve()的實現方法(nova/service.py)如下所示,通過oslo_service/service.py中的launch方法,啓動相應數量的RPC Service進程,至此nova-compute服務啓動。
在這裏插入圖片描述
在這裏插入圖片描述

nova-compute服務創建虛擬機流程解析

nova/conductor/manager.py:ComputeTaskManager的build_instance()方法在確定好按權重排序的主機列表之後,會調用self.compute_rpcapi_build_and_run_instance()方法,該方法的代碼如下所示(nova/compute/rpcapi.py),該方法通過RPC調用nova/compute/manager.py:ComputeManager中的build_and_run_instance(),該方法如後一張圖所示,裏面定義了一個_locked_do_build_and_run_instance()方法,這裏真正實現instance的創建和啓動的,還有一個spawn方法調用,這是因爲nova compute創建VM的時間會比較長,因爲其中包括了下載VM鏡像、創建和配置VM網卡信息、文件注入等任務,因此調用utils.spawn_n()方法返回RPC線程而不會造成阻塞。
在這裏插入圖片描述
在這裏插入圖片描述
關於_do_build_and_run_instance()方法,由於該部分的代碼比較長,所以將源碼粘貼如下,這部分內容的解讀參考了https://blog.csdn.net/ksj367043706/article/details/89287020,_do_build_and_run_instance()方法主要完成了如下幾個步驟

  1. 第11~16行:設置任務狀態:修改vm_state爲building,task_state爲None,然後調用instance.save()將狀態存入數據庫中。
  2. 第26行如果起虛機時有文件注入,將會調用nova.compute.manager.ComputeManager._decode_files()完成文件注入處理。
  3. 第34~39行:開始創建VM並啓動,調用nova.compute.manager.ComputeManager._build_and_run_instance()開始創建VM,這個方法爲虛機申請了鏡像,網絡等資源(這部分代碼比較複雜,限於篇幅,後續教程中將進行講解),此時vm_stat爲building,task_state爲spawning,然後調用instance.save()方法將狀態存入數據庫中。如果虛擬機創建成功,則在第42行返回
    build_results.ACTIVE,完成虛擬機創建
    @hooks.add_hook('build_instance')
    @wrap_exception()
    @reverts_task_state
    @wrap_instance_event(prefix='compute')
    @wrap_instance_fault
    def _do_build_and_run_instance(self, context, instance, image,
            request_spec, filter_properties, admin_password, injected_files,
            requested_networks, security_groups, block_device_mapping,
            node=None, limits=None, host_list=None):

        try:
            LOG.debug('Starting instance...', instance=instance)
            instance.vm_state = vm_states.BUILDING
            instance.task_state = None
            instance.save(expected_task_state=
                    (task_states.SCHEDULING, None))
        except exception.InstanceNotFound:
            msg = 'Instance disappeared before build.'
            LOG.debug(msg, instance=instance)
            return build_results.FAILED
        except exception.UnexpectedTaskStateError as e:
            LOG.debug(e.format_message(), instance=instance)
            return build_results.FAILED

        # b64 decode the files to inject:
        decoded_files = self._decode_files(injected_files)

        if limits is None:
            limits = {}

        if node is None:
            node = self._get_nodename(instance, refresh=True)

        try:
            with timeutils.StopWatch() as timer:
                self._build_and_run_instance(context, instance, image,
                        decoded_files, admin_password, requested_networks,
                        security_groups, block_device_mapping, node, limits,
                        filter_properties, request_spec)
            LOG.info('Took %0.2f seconds to build instance.',
                     timer.elapsed(), instance=instance)
            return build_results.ACTIVE
        except exception.RescheduledException as e:
            retry = filter_properties.get('retry')
            if not retry:
                # no retry information, do not reschedule.
                LOG.debug("Retry info not present, will not reschedule",
                    instance=instance)
                self._cleanup_allocated_networks(context, instance,
                    requested_networks)
                self._cleanup_volumes(context, instance,
                    block_device_mapping, raise_exc=False)
                compute_utils.add_instance_fault_from_exc(context,
                        instance, e, sys.exc_info(),
                        fault_message=e.kwargs['reason'])
                self._nil_out_instance_obj_host_and_node(instance)
                self._set_instance_obj_error_state(context, instance,
                                                   clean_task_state=True)
                return build_results.FAILED
            LOG.debug(e.format_message(), instance=instance)
            # This will be used for logging the exception
            retry['exc'] = traceback.format_exception(*sys.exc_info())
            # This will be used for setting the instance fault message
            retry['exc_reason'] = e.kwargs['reason']
            # NOTE(comstud): Deallocate networks if the driver wants
            # us to do so.
            # NOTE(mriedem): Always deallocate networking when using Neutron.
            # This is to unbind any ports that the user supplied in the server
            # create request, or delete any ports that nova created which were
            # meant to be bound to this host. This check intentionally bypasses
            # the result of deallocate_networks_on_reschedule because the
            # default value in the driver is False, but that method was really
            # only meant for Ironic and should be removed when nova-network is
            # removed (since is_neutron() will then always be True).
            # NOTE(vladikr): SR-IOV ports should be deallocated to
            # allow new sriov pci devices to be allocated on a new host.
            # Otherwise, if devices with pci addresses are already allocated
            # on the destination host, the instance will fail to spawn.
            # info_cache.network_info should be present at this stage.
            if (self.driver.deallocate_networks_on_reschedule(instance) or
                utils.is_neutron() or
                self.deallocate_sriov_ports_on_reschedule(instance)):
                self._cleanup_allocated_networks(context, instance,
                        requested_networks)
            else:
                # NOTE(alex_xu): Network already allocated and we don't
                # want to deallocate them before rescheduling. But we need
                # to cleanup those network resources setup on this host before
                # rescheduling.
                self.network_api.cleanup_instance_network_on_host(
                    context, instance, self.host)

            self._nil_out_instance_obj_host_and_node(instance)
            instance.task_state = task_states.SCHEDULING
            instance.save()
            # The instance will have already claimed resources from this host
            # before this build was attempted. Now that it has failed, we need
            # to unclaim those resources before casting to the conductor, so
            # that if there are alternate hosts available for a retry, it can
            # claim resources on that new host for the instance.
            self.reportclient.delete_allocation_for_instance(context,
                                                             instance.uuid)

            self.compute_task_api.build_instances(context, [instance],
                    image, filter_properties, admin_password,
                    injected_files, requested_networks, security_groups,
                    block_device_mapping, request_spec=request_spec,
                    host_lists=[host_list])
            return build_results.RESCHEDULED
        except (exception.InstanceNotFound,
                exception.UnexpectedDeletingTaskStateError):
            msg = 'Instance disappeared during build.'
            LOG.debug(msg, instance=instance)
            self._cleanup_allocated_networks(context, instance,
                    requested_networks)
            return build_results.FAILED
        except Exception as e:
            if isinstance(e, exception.BuildAbortException):
                LOG.error(e.format_message(), instance=instance)
            else:
                # Should not reach here.
                LOG.exception('Unexpected build failure, not rescheduling '
                              'build.', instance=instance)
            self._cleanup_allocated_networks(context, instance,
                    requested_networks)
            self._cleanup_volumes(context, instance,
                    block_device_mapping, raise_exc=False)
            compute_utils.add_instance_fault_from_exc(context, instance,
                    e, sys.exc_info())
            self._nil_out_instance_obj_host_and_node(instance)
            self._set_instance_obj_error_state(context, instance,
                                               clean_task_state=True)
            return build_results.FAILED
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章