筛选计算主机,按权重选择并申请资源
Nova 把必须满足的主机条件与偏好评分分开:过滤候选、归一化各项权重并排序,再尝试向 Placement 申领资源,失败则继续其他候选。
工程问题#
创建虚拟机既要满足资源与放置约束,也要在多个可用主机间选择。不同评分量纲不能直接相加,而查询到资源可用只是一个候选视图:并发调度器可能在实际分配前先占用同一资源。
实际实现#
调度器取得 Placement 候选后,由 HostManager 执行已配置的过滤器,再由 weighers 计算各项评分。BaseWeightHandler 将同项原始评分归一化,乘以该项在相应主机上的倍率,累加并按总分降序排列。调度器可打散最高同分组,并从配置允许的前若干名中随机选一台移到首位,以减少相似请求集中选择同一主机。随后按候选顺序申领资源,只有申领成功才记录该主机;批量创建还会消费本地 HostState,影响下一实例的过滤和评分。
处理流程#
- 从 Placement 候选建立 HostState,并应用硬约束过滤器
- 逐项计算评分,在候选间归一化后乘以配置倍率并累加
- 按总分排序,根据配置打散同分组或随机选择前部候选
- 按顺序向 Placement 申领资源;某个候选申领失败就尝试下一候选
- 成功后更新本地已消费资源,再为批量请求的下一实例重新评估
源码片段#
固定版本 · 原文摘录712 def _get_sorted_hosts(self, spec_obj, host_states, index):713 """Returns a list of HostState objects that match the required714 scheduling constraints for the request spec object and have been sorted715 according to the weighers.716 """717 filtered_hosts = self.host_manager.get_filtered_hosts(host_states,718 spec_obj, index)719720 LOG.debug("Filtered %(hosts)s", {'hosts': filtered_hosts})721722 if not filtered_hosts:723 return []724725 weighed_hosts = self.host_manager.get_weighed_hosts(726 filtered_hosts, spec_obj)先排除不满足条件的主机,再对可行集合评分;没有可行主机直接返回空列表。
43 if maxval is None:44 maxval = max(weight_list)4546 if minval is None:47 minval = min(weight_list)4849 maxval = float(maxval)50 minval = float(minval)5152 if minval == maxval:53 return [0] * len(weight_list)5455 range_ = maxval - minval56 return ((i - minval) / range_ for i in weight_list)默认使用本项评分的最大最小值,weigher 也可指定边界;归一化上下界相同时返回零向量。RAMWeigher 指定 minval 为 0,不能笼统地认为候选原始分数相同就一定归零。
155 log_data = {}156157 for i, weight in enumerate(weights):158 obj = weighed_objs[i]159 multiplier = weigher.weight_multiplier(obj.obj)160 weigher_score = multiplier * weight161 obj.weight += weigher_score162163 log_data[(obj.obj.host, obj.obj.nodename)] = (164 f"{multiplier} * {weight}")165166 LOG.debug(167 "%s: score (multiplier * weight) %s",168 weigher.__class__.__name__,169 {name: log for name, log in log_data.items()}170 )171172 return sorted(weighed_objs, key=lambda x: x.weight, reverse=True)每个已归一化分数乘以该主机的倍率后累加,最终降序排列;倍率可以因 host_state 不同而不同。
746 # We randomize the first element in the returned list to alleviate747 # congestion where the same host is consistently selected among748 # numerous potential hosts for similar request specs.749 host_subset_size = CONF.filter_scheduler.host_subset_size750 if host_subset_size < len(weighed_hosts):751 weighed_subset = weighed_hosts[0:host_subset_size]752 else:753 weighed_subset = weighed_hosts754755 chosen_host = random.choice(weighed_subset)756 weighed_hosts.remove(chosen_host)757 return [chosen_host] + weighed_hosts只从配置给定的前部候选中随机选一个移到首位,剩余候选仍保留供后续申领尝试;不是把全体主机完全随机排序。
455 # TODO(jaypipes): Loop through all allocation_requests instead456 # of just trying the first one. For now, since we'll likely457 # want to order the allocation_requests in the future based on458 # information in the provider summaries, we'll just try to459 # claim resources using the first allocation_request460 alloc_req = host.allocation_candidates[0]461 if utils.claim_resources(462 elevated, self.placement_client, spec_obj, instance_uuid,463 alloc_req,464 allocation_request_version=allocation_request_version,465 ):466 claimed_host = host467 break468469 if claimed_host is None:470 # We weren't able to claim resources in the placement API471 # for any of the sorted hosts identified. So, clean up any472 # successfully-claimed resources for prior instances in473 # this request and return an empty list which will cause474 # select_destinations() to raise NoValidHost475 LOG.debug("Unable to successfully claim against any host.")476 break当前版本每台主机只尝试它的第一个 allocation candidate;成功即选中,失败继续外层主机循环。不能描述成遍历每台主机的全部 allocation requests。
478 claimed_instance_uuids.append(instance_uuid)479 claimed_hosts.append(claimed_host)480 claimed_alloc_reqs.append(alloc_req)481482 # update the provider mapping in the request spec based483 # on the allocated candidate as the _consume_selected_host depends484 # on this information to temporally consume PCI devices tracked in485 # placement486 for request_group in spec_obj.requested_resources:487 request_group.provider_uuids = alloc_req[488 'mappings'][request_group.requester_id]489490 # Now consume the resources so the filter/weights will change for491 # the next instance.492 self._consume_selected_host(493 claimed_host, spec_obj, instance_uuid=instance_uuid)494495 # Check if we were able to fulfill the request. If not, this call will496 # raise a NoValidHost exception.497 self._ensure_sufficient_hosts(498 context, claimed_hosts, num_instances, claimed_instance_uuids)成功后更新请求的 provider 映射和本地已消费资源,使下一实例重新评分时看到本批次已经使用的资源;不足时进入统一失败清理检查。
六段原文覆盖过滤评分入口、归一化、加权排序、候选随机化、资源申领与批量消费。过滤器和 RAM weigher 的实际接入另附源码。
收益与取舍#
工程分析带来的收益
- 把可行性条件与排序偏好分开,避免用高评分补偿不满足的硬约束
- 归一化降低评分量纲差异的直接影响,倍率仍可显式表达偏好方向和强度
- 随机化与申领重试缓解多个调度器争抢同一候选的情况,不把候选查询误当作已完成的资源预留
付出的代价
- 评分结果随候选集合、weigher 的 min/max 与倍率变化;归一化并不提供全局最优放置或公平性保证
- 忽略过滤器和外部查询成本时,H 个候选、W 个评分器通常需要 O(WH) 评分及 O(H log H) 排序;批量创建会重复评估,申领还包含网络与服务端成本
- 加大随机候选范围可能降低争抢,也可能选择分数较低的主机;主机满足当时约束仍不保证后续构建成功,需要其他重试与清理机制
可比较的选择
轮询所有计算主机选择成本低,但不能直接表达异构资源、亲和约束及可调偏好,仍需额外可行性检查。
全局优化求解一批实例放置可以显式优化整体目标,但模型、求解时间和实时资源变化处理更复杂;Nova 此路径采用逐实例的启发式选择。
版本与适用范围#
固定 OpenStack Nova 31.0.0(2025.1),实现位于 nova/scheduler/manager.py,不是旧版本的 filter_scheduler.py。主分类属于云基础设施,Nova 是控制平面,不是 hypervisor;可与 libvirt、QEMU、Linux KVM 的执行路径关联。本例限定有 Placement allocation candidates 的普通创建路径;rebuild 等 legacy 路径另有处理。申领失败继续候选由 Nova 源码支持,但本例不据此推导 Placement 服务端的全部并发控制保证。
参考来源#
源码、官方文档与原始论文nova/scheduler/manager.py · _select_destinations · 逐主机申领
nova/scheduler/manager.py · _select_destinations · 消费状态并核验批量结果
nova/scheduler/manager.py · _select_destinations · 实例循环与候选遍历
nova/scheduler/manager.py · _get_sorted_hosts · 可配置的最高同分组打散
nova/scheduler/host_manager.py · HostManager · 已配置的过滤器与评分器
nova/scheduler/manager.py · _ensure_sufficient_hosts · 批量不足时清理申领
nova/scheduler/manager.py · _consume_selected_host · 本地资源与反亲和状态
相关实现与比较#
4 个相关案例Redis SDS 把字节长度与可用容量放进头部,追加按长度复制,正文允许包含零字节。
LLVM 对控制流图按边权从高到低选生成树,用并查集排除环,把边计数器放在非树边上;这是 Kruskal 的最大生成树变体。
Kubernetes 控制器按对象键排队,用 dirty 和 processing 两个集合合并重复通知;处理期间发生的更新会在本轮结束后触发下一轮。
runc 用 procReady/procRun 握手建立父子进程的执行边界:父进程完成限制设置并保存容器状态后,才让子进程继续初始化。