<aside> ⏰ 调度 Pod 的时候,选择资源分配更为均匀和资源分配较少的节点

</aside>

1. NodeResourcesBalancedAllocation

该插件是在打分阶段,选择资源分配更为均匀的节点(CPU 和内存资源占用率相近的胜出),默认启用该插件,权重为1。同样直接分析插件的 Score 函数即可:

// pkg/scheduler/framework/plugins/noderesources/balanced_allocation.go

func (ba *BalancedAllocation) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
	nodeInfo, err := ba.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
	if err != nil {
		return 0, framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", nodeName, err))
	}

  // ba.score 有利于资源使用率均衡的节点
  // 它计算cpu和内存容量的差值,并根据这两个指标的接近程度来确定主机的优先级。
  // Detail: score = (1 - variance(cpuFraction,memoryFraction,volumeFraction)) * MaxNodeScore.
	return ba.score(pod, nodeInfo)
}

核心实现就是下面的 score 函数了,该函数的核心是计算内存与 CPU 容量的差值,根据这两个指标的接近程度来确定节点的优先级:

// pkg/scheduler/framework/plugins/noderesources/resource_allocation.go

// resourceToWeightMap 包含资源名称和对应的权重
type resourceToWeightMap map[v1.ResourceName]int64

// defaultRequestedRatioResources 定义默认的 CPU 和内存的 resourceToWeightMap
var defaultRequestedRatioResources = resourceToWeightMap{v1.ResourceMemory: 1, v1.ResourceCPU: 1}

// resourceAllocationScorer 包含计算资源分配分数的信息。
type resourceAllocationScorer struct {
	Name                string
	scorer              func(requested, allocable resourceToValueMap, includeVolumes bool, requestedVolumes int, allocatableVolumes int) int64
	resourceToWeightMap resourceToWeightMap
}

// resourceToValueMap 包含资源名称和分数
type resourceToValueMap map[v1.ResourceName]int64

func (r *resourceAllocationScorer) score(
	pod *v1.Pod,
	nodeInfo *framework.NodeInfo) (int64, *framework.Status) {
	node := nodeInfo.Node()
	......
	requested := make(resourceToValueMap, len(r.resourceToWeightMap))
	allocatable := make(resourceToValueMap, len(r.resourceToWeightMap))
	// 根据资源定义的权重来计算对应资源的分数
  for resource := range r.resourceToWeightMap {
		allocatable[resource], requested[resource] = calculateResourceAllocatableRequest(nodeInfo, pod, resource)
	}
	var score int64

  // 检查pod是否有volumes卷,可以添加到scorer函数中,以实现均衡的资源分配
	if len(pod.Spec.Volumes) >= 0 && utilfeature.DefaultFeatureGate.Enabled(features.BalanceAttachedNodeVolumes) && nodeInfo.TransientInfo != nil {
		score = r.scorer(requested, allocatable, true, nodeInfo.TransientInfo.TransNodeInfo.RequestedVolumes, nodeInfo.TransientInfo.TransNodeInfo.AllocatableVolumesCount)
	} else {
		score = r.scorer(requested, allocatable, false, 0, 0)
	}
	return score, nil
}

该函数的整体实现比较简单,根据资源定义的权重来计算节点上对应资源的分数,然后调用 scorer 回调函数计算出最后的得分。

默认定义的 CPU 和内存资源的权重是1:1,然后调用 calculateResourceAllocatableRequest 函数分别计算节点上这两种资源可分配和请求的分数,最后通过回调函数 scorer 函数来计算出最终的得分。

// pkg/scheduler/framework/plugins/noderesources/resource_allocation.go

// 计算节点指定资源的可分配值和请求值
func calculateResourceAllocatableRequest(nodeInfo *framework.NodeInfo, pod *v1.Pod, resource v1.ResourceName) (int64, int64) {
	// 计算 Pod 对应资源的请求值
  podRequest := calculatePodResourceRequest(pod, resource)
	switch resource {
	case v1.ResourceCPU:
    // 节点资源的请求值为节点上已有的请求CPU值+当前Pod请求的值
		return nodeInfo.Allocatable.MilliCPU, (nodeInfo.NonZeroRequested.MilliCPU + podRequest)
	case v1.ResourceMemory:
    // 节点资源的请求值为节点上已有的请求内存值+当前Pod请求的值
		return nodeInfo.Allocatable.Memory, (nodeInfo.NonZeroRequested.Memory + podRequest)
    // 节点资源的请求值为节点上已有的请求临时存储值+当前Pod请求的值
	case v1.ResourceEphemeralStorage:
		return nodeInfo.Allocatable.EphemeralStorage, (nodeInfo.Requested.EphemeralStorage + podRequest)
	default:
    // 节点资源的请求值为节点上已有的请求标量资源值+当前Pod请求的值
		if v1helper.IsScalarResourceName(resource) {
			return nodeInfo.Allocatable.ScalarResources[resource], (nodeInfo.Requested.ScalarResources[resource] + podRequest)
		}
	}
	return 0, 0
}

// 返回总的非零请求. 如果为 Pod 定义了 Overhead 并且启用了 PodOverhead 特性
// 这 Overhead 也会被计算在内。
// podResourceRequest = max(sum(podSpec.Containers), podSpec.InitContainers) + overHead
func calculatePodResourceRequest(pod *v1.Pod, resource v1.ResourceName) int64 {
	var podRequest int64
  // 所有容器的 Request 值
	for i := range pod.Spec.Containers {
		container := &pod.Spec.Containers[i]
    // 获取指定容器指定资源的非0请求值
		value := schedutil.GetNonzeroRequestForResource(resource, &container.Resources.Requests)
		podRequest += value
	}
  // 所有初始化容器的 Request 值,需要和普通容器的 Request 进行比较,取较大的值
	for i := range pod.Spec.InitContainers {
		initContainer := &pod.Spec.InitContainers[i]
		value := schedutil.GetNonzeroRequestForResource(resource, &initContainer.Resources.Requests)
		if podRequest < value {
			podRequest = value
		}
	}

	// 如果开启了 Overhead 特性,则也需要计算在 Request 中
	if pod.Spec.Overhead != nil && utilfeature.DefaultFeatureGate.Enabled(features.PodOverhead) {
		if quantity, found := pod.Spec.Overhead[resource]; found {
			podRequest += quantity.Value()
		}
	}

	return podRequest
}

calculateResourceAllocatableRequest 函数用来计算节点资源的可分配和请求的值,可分配的资源直接从 NodeInfo 获取即可,请求的资源是节点上所有 Pod 的非0总请求资源,然后加上当前 Pod 的请求资源值即可。

计算当前 Pod 的请求资源也很简单,算法为 max(sum(podSpec.Containers), podSpec.InitContainers) + overHead ,就是所有普通容器和初始化容器总的请求较大值,如果开启了 Overhead 特性,也需要计算在 Request 中,这里有一个重点是计算容器请求资源的时候,是计算非0的请求值,因为有一些 Pod 没有指定 Requests 值,那么就需要计算一个默认值:

// pkg/scheduler/util/non_zero.go

// 如果没有找到或者指定 Request 值则返回默认的资源请求值
func GetNonzeroRequestForResource(resource v1.ResourceName, requests *v1.ResourceList) int64 {
	switch resource {
	case v1.ResourceCPU:
		if _, found := (*requests)[v1.ResourceCPU]; !found {
      // 没有指定 CPU,默认返回 100(0.1core)
			return DefaultMilliCPURequest
		}
		return requests.Cpu().MilliValue()
	case v1.ResourceMemory:
		if _, found := (*requests)[v1.ResourceMemory]; !found {
      // 没有指定内存,默认返回200MB
			return DefaultMemoryRequest
		}
		return requests.Memory().Value()
	case v1.ResourceEphemeralStorage:
    // 如果本地存储容量隔离特性被禁用,则 Pod 请求为 0 disk。
		if !utilfeature.DefaultFeatureGate.Enabled(features.LocalStorageCapacityIsolation) {
			return 0
		}
    // 没有找到也返回0
		quantity, found := (*requests)[v1.ResourceEphemeralStorage]
		if !found {
			return 0
		}
		return quantity.Value()
	default:
    // 如果是标量资源没有找到返回0
		if v1helper.IsScalarResourceName(resource) {
			quantity, found := (*requests)[resource]
			if !found {
				return 0
			}
			return quantity.Value()
		}
	}
	return 0
}

当 Pod 没有指定 CPU 的 requests 的时候,默认返回 100m(0.1core),如果是内存没有指定,则默认为200MB。

到这里就请求如何计算节点可分配和总的请求资源值了,接下来就是去查看具体的回调 scorer 函数的实现。在 BalancedAllocation 插件初始化的时候传入了 scorer 函数的实现:

// pkg/scheduler/framework/plugins/noderesources/balanced_allocation.go

// 实例化 BalancedAllocation 插件
func NewBalancedAllocation(_ runtime.Object, h framework.FrameworkHandle) (framework.Plugin, error) {
	return &BalancedAllocation{
		handle: h,
		resourceAllocationScorer: resourceAllocationScorer{
			BalancedAllocationName,
			balancedResourceScorer,
			defaultRequestedRatioResources,
		},
	}, nil
}

scorer 回调函数就是这里的 balancedResourceScorer :

// pkg/scheduler/framework/plugins/noderesources/balanced_allocation.go

func balancedResourceScorer(requested, allocable resourceToValueMap, includeVolumes bool, requestedVolumes int, allocatableVolumes int) int64 {
	// 总的请求和可分配的资源的比例
  cpuFraction := fractionOfCapacity(requested[v1.ResourceCPU], allocable[v1.ResourceCPU])
	memoryFraction := fractionOfCapacity(requested[v1.ResourceMemory], allocable[v1.ResourceMemory])
  // 如果比例>=1,相当于 requested>=allocable,那么该主机绝对不应该优先调度了,所以返回分数为0
	if cpuFraction >= 1 || memoryFraction >= 1 {
		return 0
	}
  // 如果 Volumes 需要计算在内
	if includeVolumes && utilfeature.DefaultFeatureGate.Enabled(features.BalanceAttachedNodeVolumes) && allocatableVolumes > 0 {
		volumeFraction := float64(requestedVolumes) / float64(allocatableVolumes)
		if volumeFraction >= 1 {
			// 和上面一样,volume的请求和可分配的比例>=1了,不能优先调度
			return 0
		}
		// 计算这3个比率的方差(偏离程度)
		mean := (cpuFraction + memoryFraction + volumeFraction) / float64(3)  // 平均数
		variance := float64((((cpuFraction - mean) * (cpuFraction - mean)) + ((memoryFraction - mean) * (memoryFraction - mean)) + ((volumeFraction - mean) * (volumeFraction - mean))) / float64(3))
    // 方差越小,代表越稳定,所以分数应该更高,所以这里用 1 减去方差
		return int64((1 - variance) * float64(framework.MaxNodeScore))
	}
  
  // cpuFraction 和 memoryFraction 之间的差值范围为 -1 和 1,将差值 * `MaxNodeScore`,将其变为 0-MaxNodeScore
  // 0代表分配均衡较好,`MaxNodeScore`代表均衡不良
  // 从 `MaxNodeScore` 中减去,得到的分数也是从0到`MaxNodeScore`,而`MaxNodeScore`代表平衡良好。
  // 差值越小越平衡,所以分数更高
	diff := math.Abs(cpuFraction - memoryFraction)
	return int64((1 - diff) * float64(framework.MaxNodeScore))
}

func fractionOfCapacity(requested, capacity int64) float64 {
	if capacity == 0 {
		return 1
	}
	return float64(requested) / float64(capacity)
}

该计算分数的函数先计算得到 CPU 和内存的请求和可分配容器间的比率,如果需要计算 Volume,则计算这三种资源比率的方差,方差越小,代表越稳定,所以分数应该更高。同样如果只需要计算 CPU 和内存,则直接计算二者比率的差值即可,差值越小表示分配越平衡,理论上分数就应该更高,这就是这里我们提到的资源分配更均匀的算法。

2. NodeResourcesLeastAllocated