【转载】调度插件之 NodeResourcesFit

610 阅读5分钟

介绍

prefilter插件的主要作用进行一些预置的检查和为后面的扩展点提前准备数据,后续插件需要的状态数据都是通过CycleState来进行存储和检索,一个插件存储的状态数据可以被另一个插件读取,修改和删除。

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

const (
	// Name is the name of the plugin used in the plugin registry and configurations.
	// 定义插件的名称
	Name = names.NodeResourcesFit

	// preFilterStateKey is the key in CycleState to NodeResourcesFit pre-computed data.
	// Using the name of the plugin will likely help us avoid collisions with other plugins.
	// preFilterStateKey 是存放在 CycleState 中的关于 NodeResourcesFit 预计算数据的 key
	preFilterStateKey = "PreFilter" + Name
)

PreFilter

比如这里我们选择NodeResourceFit这个插件来进行说明,该插件的核心就是实现PreFilter函数:

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

// computePodResourceRequest returns a framework.Resource that covers the largest
// width in each resource dimension. Because init-containers run sequentially, we collect
// the max in each dimension iteratively. In contrast, we sum the resource vectors for
// regular containers since they run simultaneously.
// computePodResourceRequest 返回一个涵盖每个资源维度中最大宽度的 framework.Resource
// 因为 initCOntainers 是按照顺序运行的,所以我们循环收集每个维度中的最大值
// 相反,由于普通容器是同时运行的,所以我们对他们的资源向量是进行求和计算
//
// The resources defined for Overhead should be added to the calculated Resource request sum
// 此外如果启用了 PodOverhead 这个特性并且指定了 Pod Overhead
// 则也需要为 Overhead 定义的资源将被添加到计算的 Resource 请求总和上
//
// Example:
//
// Pod:
//   InitContainers
//     IC1:
//       CPU: 2
//       Memory: 1G
//     IC2:
//       CPU: 2
//       Memory: 3G
//   Containers
//     C1:
//       CPU: 2
//       Memory: 1G
//     C2:
//       CPU: 1
//       Memory: 1G
//
// Result: CPU: 3, Memory: 3G
// 初始化容器: IC1和IC2是顺序执行,所以获取两个中最大的资源,即 CPU:2,Memory:3G
// 普通容器: C1和C2是同时运行的,所以需要的资源是两者之和: CPU: 2+1=3, Memory: 1+1=2G
// 最后需要的资源请求是初始化容器和普通容器中最大的: CPU:3,Memory: 3G
func computePodResourceRequest(pod *v1.Pod) *preFilterState {
	result := &preFilterState{}
	// 普通容器 Requests 资源相加
	for _, container := range pod.Spec.Containers {
		result.Add(container.Resources.Requests)
	}

	// take max_resource(sum_pod, any_init_container)
	for _, container := range pod.Spec.InitContainers {
		result.SetMaxResource(container.Resources.Requests)
	}

	// If Overhead is being utilized, add to the total requests for the pod
	// 如果正在使用 Overhead 特性,则也需要计算到总和里面
	if pod.Spec.Overhead != nil {
		result.Add(pod.Spec.Overhead)
	}
	return result
}

// PreFilter invoked at the prefilter extension point.
// 在 prefilter 扩展点被调用
func (f *Fit) PreFilter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod) (*framework.PreFilterResult, *framework.Status) {
	// 计算pod请求所需的资源,然后存储到 CycleState 中,方便后续其他插件获取数据
	cycleState.Write(preFilterStateKey, computePodResourceRequest(pod))
	return nil, nil
}

也就是在 prefilter 这个扩展点的时候会获取到当前我们要调度的 Pod 需要的 Requests 资源,然后将其存入 CycleState。然后其他插件中如果需要用到这个数据就可以直接获取了,简单来说 CycleState 就是用于调度周期上下文数据传递共享的。

prefilter 扩展点注册的插件执行完成后,接着就是执行 filter 扩展点的插件了。

Filter

由于插件较多,这里我们也暂时挑选一个进行简单说明,例如我们可以看到在Filter中也注册了一个noderesources.FitName的插件,这其实就是上面的prefilter阶段使用过的NodeResourcesFit插件,这其实也说明了某些插件是可能在任何一个扩展点出现的,现在是在filter扩展点,那么我们重点要看的是该插件的Filter()函数的实现:

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


// Filter invoked at the filter extension point.
// Checks if a node has sufficient resources, such as cpu, memory, gpu, opaque int resources etc to run a pod.
// It returns a list of insufficient resources, if empty, then the node has all the resources requested by the pod.
// 在Filter扩展点调用Filter
// 检查一个节点是否有足够的资源,如cpu,内存,gpu 等来运行一个pod
// 它返回一个资源不足的列表,如果为空,则说明该节点拥有Pod请求的所有资源
func (f *Fit) Filter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
	// 获取 prefilter 阶段存储在 CycleState 中的数据
	s, err := getPreFilterState(cycleState)
	if err != nil {
		return framework.AsStatus(err)
	}

	insufficientResources := fitsRequest(s, nodeInfo, f.ignoredResources, f.ignoredResourceGroups)

	// 不足的资源大小不为0
	if len(insufficientResources) != 0 {
		// We will keep all failure reasons.
		// 保留所有失败的原因
		failureReasons := make([]string, 0, len(insufficientResources))
		for i := range insufficientResources {
			failureReasons = append(failureReasons, insufficientResources[i].Reason)
		}
		// 直接返回调度失败
		return framework.NewStatus(framework.Unschedulable, failureReasons...)
	}
	return nil
}


func fitsRequest(podRequest *preFilterState, nodeInfo *framework.NodeInfo, ignoredExtendedResources, ignoredResourceGroups sets.String) []InsufficientResource {
	insufficientResources := make([]InsufficientResource, 0, 4)
	// 当前节点允许的Pod数量,默认是110
	allowedPodNumber := nodeInfo.Allocatable.AllowedPodNumber
	// 如果现有的Pod数 + 1(当前调度的pod) > 节点允许的Pod数,则提示太多pods
	if len(nodeInfo.Pods)+1 > allowedPodNumber {
		insufficientResources = append(insufficientResources, InsufficientResource{
			ResourceName: v1.ResourcePods,
			Reason:       "Too many pods",
			Requested:    1,
			Used:         int64(len(nodeInfo.Pods)),
			Capacity:     int64(allowedPodNumber),
		})
	}

	// 如果没有配置 Requests 资源,则直接返回
	if podRequest.MilliCPU == 0 &&
		podRequest.Memory == 0 &&
		podRequest.EphemeralStorage == 0 &&
		len(podRequest.ScalarResources) == 0 {
		return insufficientResources
	}

	// 节点可分配的CPU不够
	if podRequest.MilliCPU > (nodeInfo.Allocatable.MilliCPU - nodeInfo.Requested.MilliCPU) {
		insufficientResources = append(insufficientResources, InsufficientResource{
			ResourceName: v1.ResourceCPU,
			Reason:       "Insufficient cpu",
			Requested:    podRequest.MilliCPU,
			Used:         nodeInfo.Requested.MilliCPU,
			Capacity:     nodeInfo.Allocatable.MilliCPU,
		})
	}

	// 可分配的内存不够
	if podRequest.Memory > (nodeInfo.Allocatable.Memory - nodeInfo.Requested.Memory) {
		insufficientResources = append(insufficientResources, InsufficientResource{
			ResourceName: v1.ResourceMemory,
			Reason:       "Insufficient memory",
			Requested:    podRequest.Memory,
			Used:         nodeInfo.Requested.Memory,
			Capacity:     nodeInfo.Allocatable.Memory,
		})
	}

	// 临时存储不够
	if podRequest.EphemeralStorage > (nodeInfo.Allocatable.EphemeralStorage - nodeInfo.Requested.EphemeralStorage) {
		insufficientResources = append(insufficientResources, InsufficientResource{
			ResourceName: v1.ResourceEphemeralStorage,
			Reason:       "Insufficient ephemeral-storage",
			Requested:    podRequest.EphemeralStorage,
			Used:         nodeInfo.Requested.EphemeralStorage,
			Capacity:     nodeInfo.Allocatable.EphemeralStorage,
		})
	}

	// 查看其他标量资源
	for rName, rQuant := range podRequest.ScalarResources {
		if v1helper.IsExtendedResourceName(rName) {
			// If this resource is one of the extended resources that should be ignored, we will skip checking it.
			// rName is guaranteed to have a slash due to API validation.
			// 如果该资源是应该忽略的扩展资源之一,我们将跳过检查
			// 由于API验证,rName保证有斜杠。
			var rNamePrefix string
			if ignoredResourceGroups.Len() > 0 {
				rNamePrefix = strings.Split(string(rName), "/")[0]
			}
			if ignoredExtendedResources.Has(string(rName)) || ignoredResourceGroups.Has(rNamePrefix) {
				continue
			}
		}

		// 对应资源在节点上不足
		if rQuant > (nodeInfo.Allocatable.ScalarResources[rName] - nodeInfo.Requested.ScalarResources[rName]) {
			insufficientResources = append(insufficientResources, InsufficientResource{
				ResourceName: rName,
				Reason:       fmt.Sprintf("Insufficient %v", rName),
				Requested:    podRequest.ScalarResources[rName],
				Used:         nodeInfo.Requested.ScalarResources[rName],
				Capacity:     nodeInfo.Allocatable.ScalarResources[rName],
			})
		}
	}

	// 返回所有的不足资源信息
	return insufficientResources
}

上面的过滤函数整体比较简单易懂,拿到 prefilter 阶段存储在 CycleState 里面的 Pod 请求资源数据,然后和节点上剩余的可分配资源进行比较,如果没有设置 Requests 资源则直接返回,但是也会检查当前节点是否还有 Pod 数量(默认110),然后就是比较 CPU、内存、临时存储、标量资源等是否还有可分配的,所谓标量资源就是我们在定义 Pod 的时候可以自己指定一种资源来进行分配,比如 GPU,我们就可以当成一种标量资源进行分配,同样也需要判断节点上是否有可分配的标量资源。