1. 程式人生 > >OpenStack之Nova分析——Nova Scheduler排程演算法

OpenStack之Nova分析——Nova Scheduler排程演算法

上篇文章介紹了Nova Scheduler服務的啟動流程,我們知道Nova Scheduler服務作為一個排程者,其核心便是排程演算法。這篇文章我們就來分析一下Nova Scheduler服務的排程演算法吧。

在配置檔案中,排程演算法預設的驅動類是FilterScheduler,該類位於nova/nova/scheduler/filter_scheduler.py中。其演算法的原理是比較簡單的,就是“過濾”和“稱重”的過程。

class FilterScheduler(driver.Scheduler):
    def scheduler_run_instance(self, context, request_spec,
                               admin_password, injected_files,
                               requested_networks, is_first_time,
                               filter_properties):
        #獲取排程所需引數
        payload = dict(request_spec=request_spec)
        #通知Nova API開始排程
        notifier.notify(context, notifier.publisher_id("scheduler"),
                        'scheduler.run_instance.start', notifier.INFO, notifier.INFO,
                        payload)
        ...
        #執行排程演算法,獲取加權主機列表
        weighted_hosts = self._schedule(context, "compute", request_spec,
                                        filter_properties, instance_uuids)
        ...
        #為每個虛擬機器分配計算節點
        for num, instance_uuid in enumerate(instance_uuids):
            ...
            try:
                try:
                    #選擇權值最高的計算節點
                    weighted_host = weighted_hosts.pop(0)
                except IndexError:
                    raise exception.NoValidHost(reason="")
                #在權值最高的計算節點上建立虛擬機器
                self._provision_resource(context, weighted_host,
                                         request_spec,
                                         filter_properties,
                                         requested_networks,
                                         injected_files, admin_password,
                                         is_first_time,
                                         instance_uuid=instance_uuid)
            except Exception as ex:
                ...
        #通知Nova API虛擬機器排程完畢
        notifier.notify(context, notifier.publisher_id("scheduler"),
                        'scheduler.run_instance.end', notifier.INFO, payload)

演算法的核心實現在FilterScheduler類的_scheduler方法中。後面的_provision_resource方法實際上是遠端呼叫了Nova Compute服務的run_instance方法。我們下面來重點看一下包含演算法核心的_scheduler方法。

def _schedule(self, context, topic, request_spec, filter_properties,
                  instance_uuids=None):
    #獲取使用者上下文資訊
    elevated = context.elevated()
    #獲取虛擬機器的資訊
    instance_properties = request_spec['instance_properties']
    #獲取虛擬機器規格
    instance_type = request_spec.get("instance_type", None)
    ...
    #獲取配置項
    config_options = self._get_configuration_options()
    properties = instance_properties.copy()
    if instance_uuids:
        properties['uuid'] = instance_uuids[0]
    self._populate_retry(filter_properties, properties)

    #構造主機過濾引數
    filter_properties.update({'context': context,
                              'request_spec': request_spec,
                              'config_options': config_options,
                              'instance_type': instance_type})
    self.populate_filter_properties(request_spec,
                                    filter_properties)

    #獲取全部活動的主機列表
    hosts = self.host_manager.get_all_host_states(elevated)
    selected_hosts = []
    #獲取需要啟動的虛擬機器數量
    if instance_uuids:
        num_instances = len(instance_uuids)
    else:
        num_instances = request_spec.get('num_instances', 1)
    #為每個要建立的虛擬機器,選擇權值最高的主機
    for num in xrange(num_instances):
        #獲取所有可用的主機列表
        hosts = self.host_manager.get_filter_hosts(hosts,
                filter_properties)
        if not hosts:
            break
        #計算可用主機的權值
        weighted_host.host_state.consume_from_instance(
                instance_properties)
        #這個引數定義了新的例項將會被排程到一個主機上,這個主機是隨機的從最好的(分數最高的)N個主機組成的子集中選擇出來的
        scheduler_host_subset_size = CONF.scheduler_host_subset_size
        if scheduler_host_subset_size > len(weighed_hosts):
            scheduler_host_subset_size = len(weighed_hosts)
        if scheduler_host_subset_size < 1:
            scheduler_host_subset_size = 1
        #從分數最高的若干主機組成的子集中,隨機的選擇一個主機出來
        chosen_host = random.choice(weighed_hosts[0:scheduler_host_subset_size])
        selected_hosts.append(chosen_host)
        #因為已經選好了一個主機,所以要在下一個例項選擇主機前,更新主機資源資訊
        chosen_host.obj.consume_from_instance(instance_properties)
        ...
    return selected_hosts

虛擬機器排程演算法主要就是四個步驟:

1. 獲取可用的計算節點列表

(1) hosts = self.host_manager.get_all_host_states(elevated)

class HostManger(object):
    def get_all_host_states(self, context):
        #獲取所有計算節點
        compute_nodes = db.compute_node_get_all(context)
        seen_nodes = set()
        for compute in compute_nodes:
            #獲取節點的服務資訊
            service = compute['service']
            #節點上沒有服務,可能是過期節點
            if not service:
                continue
            #獲取節點的主機名
            host = service['host']
            node = compute.get('hypervisor_hostname')
            state_key = (host, node)
            #獲取HostManager物件快取的服務狀態和節點狀態資訊
            capabilities = self.service_states.get(state_key,None)
            host_state = self.host_state_map.get(state_key)
            #如果host_state存在,說明是舊節點
            if host_state:
                #更新節點的效能資訊
                host_state.update_capabilities(capabilities, dict
                (service.iteritems))
            else:
                #新增新節點的狀態資訊
                host_state = self.host_state_cls(host, node,capabilities=capabilities,service=dict(service.iteritems()))
                self.host_state_map[state_key] = host_state
            #更新計算節點的硬體資源資訊
            host_state.update_from_compute_node(compute)
            seen_nodes.add(state_key)
        #獲取不活動的節點列表
        dead_nodes = set(self.host_state_map.keys()) - seen_nodes
        #刪除不活動節點的快取資訊
        for state_key in dead_nodes:
            host, node = state_key
            del self.host_state_map[state_key]
        return self.host_state_map.itervalues()

可以看到,上面方法主要實現了兩個功能:獲取當前所有活動的計算節點列表;更新和維護HostManger物件快取的節點狀態資訊。

該方法首先呼叫db.compute_node_get_all,從資料庫中獲取當前活動的計算節點列表。列表中儲存了計算節點的CPU,記憶體和硬碟資源的最新資訊,該資訊由Nova Compute服務維護。Nova Compute服務會在每次執行完虛擬機器操作後更新計算節點的硬體資源資訊,同時還啟動了一個定時任務(update_available),定時更新硬體資源的資訊。變數capabilities儲存的是HostManager物件快取的計算節點效能資訊(包括節點的CPU、記憶體、硬碟的使用狀況),該效能資訊也是由Nova Compute服務的定時任務(update_capabilities)定時向Nova Scheduler服務報告節點的效能資訊。

(2) self.host_manager.get_filtered_hosts(hosts, filter_properties)

class HostManager(object):
    def get_filtered_hosts(self, hosts, filter_properties, filter_class_names=None):
        ...
        #獲取過濾器列表
        filter_classes = self._choose_host_filters(filter_class_names)
        ...
        #返回過濾後的主機列表
        return self.filter_handler.get_filtered_objects(filter_classes,
                                                        hosts, filter_properties)
為了確定計算節點是否可用,Nova Scheduler定義了多個過濾器,每個過濾器檢查節點的一種屬性。只有通過全部過濾器的節點,才被認為是可用的主機。上面的方法首先呼叫_choose_host_filters獲取過濾器列表。然後呼叫filter_handler變數的get_filtered_objects方法使用該過濾器。另外get_filtered_hosts方法還可以通過引數filter_properties傳入force_hosts和ignore_hosts兩個變數。

a. _choose_host_filters方法

class HostManager(object):
    def _choose_host_filters(self, filter_cls_names):
        #如果外部沒有傳入filter_cls_names引數,則使用預設的過濾器
        if filter_cls_names is None:
            filter_cls_names = CONF.scheduler_default_filters
        #將filter_cls_names封裝成列表
        if not isinstance(filter_cls_names, (list, tuple)):
            filter_cls_names = [filter_cls_names]
        good_filters = []
        bad_filters = []
        #遍歷所有配置的過濾器
        for filter_name in filter_cls_names:
            found_class = False
            #遍歷所有註冊的過濾器
            for cls in self.filter_classes:
                #如果filter_name對應的過濾器在註冊的過濾器列表中,則認為是好過濾器
                if cls.__name__ == filter_name:
                    good_filters.append(cls)
                    found_class = True
                    break
            #如果filter_name對應的過濾器不在註冊的過濾器列表中,則認為是壞過濾器
            if not found_class:
                bad_filters.append(filter_name)
        ...
        return good_filter
該方法遍歷filter_cls_names引數中所有的過濾器,從中提取好的過濾器,所謂好的過濾器就是指這個過濾器之前被註冊過。這個註冊過程在HostManager類的初始化方法中通過呼叫filter_handler物件的get_matching_classes方法完成,get_matching_classes方法會註冊nova.scheduler.filters包下定義的所有過濾器。

b. get_filtered_objects方法

class BaseFilterHandler(loadables.BaseLoader):
    def get_filtered_objects(self, filter_classes, objs, filter_properties):
        #遍歷每個過濾器
        for filter_cls in filter_classes:
            #呼叫過濾器類的filter_all方法
            objs = filter_cls().filter_all(objs, filter_properties)
        return list(objs)
該方法使用上面指定的過濾器,檢查計算節點是否可用,最終返回可用的計算節點列表。

方法依次呼叫了每個過濾器的filter_all方法,返回一個迭代器物件,該迭代器物件包含了通過該過濾器檢查的主機列表。每個過濾器物件都繼承自BaseHostFilter類,BaseHostFilter類繼承自BaseFilter類。filter_all方法定義在BaseFilter類中,其定義如下

class BaseFilter(object):
    def filter_all(self, filter_obj_list, filter_properties):
        for obj in filter_obj_list:
            if self._filter_one(obj, filter_properties):
                yield obj
filter_obj_list是待過濾的計算節點列表。filter_all方法對每個計算節點都呼叫了_filter_one方法,如果_filter_one方法返回True,則返回該主機的引用。BaseFilter類的_filter_one方法總是返回True,子類BaseHostFilter重寫了_filter_one方法,它會呼叫每個過濾器自身的host_pass方法。BaseHostFilter類的_filter_one方法定義如下
class BaseHostFilter(filters.BaseFilter):
    def _filter_one(self, obj, filter_properties):
        return self.host_pass(obj, filter_properties)
當主機通過了過濾器檢查時,host_pass方法返回True。只有當主機通過了所有過濾器檢查時,才被認為是可用的。

2. 計算可用計算節點的權值

get_weighed_hosts方法

class HostManager(object):
    def get_weighed_hosts(self, hosts, weight_properties):
        return self.weight_handler.get_weighed_objects(self.weight_classes,
                                                       hosts, weight_properties)
get_weighed_hosts方法較get_filtered_hosts方法要簡單。它不需要外部傳入類似weight_class_names的變數,而是直接使用預先註冊權值類(self.weight_classes = self.weight_handler.get_matching_classes(CONF.scheduler_weight_classes)),目前G版本的Nova只支援RAMWeigher權值類。

與get_filtered_hosts方法類似,get_weighed_host方法會呼叫weight_handler物件的get_weighed_objects方法來執行計算權值的方法,其定義如下

class BaseWeightHandler(loadables.BaseLoader):
    def get_weighed_objects(self, weigher_classes, obj_list, weighing_properties):
        if not obj_list:
            return []
        #將主機封裝成WeighedObject物件
        weighed_objs = [self.object_class(obj, 0.0) for obj in obj_list]
        #遍歷所有權值類
        for weigher_cls in weigher_classes:
            #建立權值物件
            weigher = weigher_cls()
            weigher.weigh_objects(weighed_objs, weighing_properties)
        #將主機列表按權值從高到低排序    
        return sorted(weighed_objs, key=lambda x: x.weight, reverse=True)
上面程式碼的核心部分是呼叫了權值物件的weigh_objects方法,每個權值物件都繼承自BaseHostWeigher類,BaseHostWeigher類繼承自BaseWeigher類。weigh_objects方法定義如下
class BaseWeigher(object):
    def weigh_objects(self, weighed_obj_list, weight_properties):
        for obj in weighed_obj_list:
            #主機權值=原來的權值+權重*當前權值物件賦予主機的權值
            obj.weight += (self._weight_multiplier() *
                           self._weigh_object(obj.obj, weight_properties))
可以看到,主機的權值實際上是各個權值類賦予主機的權值的加權和。其中_weight_multiplier方法返回當前權值類的權重,_weigh_object方法返回當前全之類賦予主機的權值。

由於當前Nova只支援RAMWeigher權值類,所以具體到這個權值類,我們來看一下_weight_multiplier和_weigh_object這兩個方法。它的權重由nova.conf配置檔案的ram_weight_multiplier配置項定義,預設值為1.0。其_weigh_object方法返回的是主機剩餘的記憶體大小。

3. 從權值最高的scheduler_host_subset_size個計算節點中隨機選擇一個計算節點作為建立虛擬機器的節點

4. 更新選擇的計算節點的硬體資源資訊,為虛擬機器預留資源