【Kubernetes in Action读书笔记】通过测试用例探索调度器Scheduler调度Pod的过程

30 阅读4分钟

【Kubernetes in Action读书笔记】通过测试用例探索调度器Scheduler调度Pod的过程

知识点

  • (11.1.5)调度器Scheduler利用API服务器的监听机制,等待新建Pod的事件,然后为每个新的还没有节点的Pod分配节点
  • (11.1.5)调度器不会命令选中的节点(或者节点上运行的Kubelet)去创建Pod,而是通过API服务器更新Pod的定义,然后由API服务器再去通知Kubelet(同样通过之前描述的监听机制〕该Pod己经被调度过。当目标节点上的Kubelet发现该Pod将被调度到这里,它就会创建并且运行Pod的容器
  • (14.1)通过指定**容器(注意⚠️不是Pod)**对CPU资源和内存资源的请求量(即requests、最小需求量?)以及限制量(即limits)来确保Pod公平地使用Kubernetes集群资源
  • (14.1)requests不会限制容器(最多、最大)可以使用的CPU数量
  • (14.1)调度器Scheduler在将Pod调度到节点的过程中只?会用到requests中的信息。Scheduler在调度时只考虑那些未分配资源量满足Pod需求量的节点
  • (14.1)Scheduler在调度时并不关注各类资源在当前时刻的实际使用量,而只关心节点上部署的所有Pod的资源申请量(requests)之和如果使用基于实际资源消耗量的调度算法,将打破系统为那些已部署成功的Pods提供足够资源的保证
  • Kubernetes将Pod划分为3种QoS等级:BestEffort(优先级最低);Burstable;Guaranteed (优先级最高)

通过测试用例探索3种QoS等级的Pod如何调度

先来看一下测试用例的框架。

【待确认:选择节点的过程是否只涉及Fit这个Plugin】

// kubernetes/pkg/scheduler/framework/plugins/noderesources/fit_test.go

func TestEnoughRequests(t *testing.T) {
    enoughPodsTests := []struct {
        pod                       *v1.Pod
        nodeInfo                  *framework.NodeInfo
        name                      string
        args                      config.NodeResourcesFitArgs
        wantInsufficientResources []InsufficientResource
        wantStatus                *framework.Status
    }{
        // 每个testcase一个struct literal
    }
    
    for _, test := range enoughPodsTests {
        t.Run(test.name, func(t *testing.T) {
            node := v1.Node{
                Status: v1.NodeStatus{
                    Capacity: makeResources(10, 20, 32, 5, 20, 5).Capacity, 
                    Allocatable: makeAllocatableResources(10, 20, 32, 5, 20, 5)
                }
            }
            test.nodeInfo.SetNode(&node)    // ①

            // ...
            p, err := NewFit(&test.args, nil, plfeature.Features{})    // ②
            // ...

            gotStatus := p.(framework.FilterPlugin).Filter(context.Background(), cycleState, test.pod, test.nodeInfo)        // ③
            // 对比 gotStatus 和 test.wantStatus

            gotInsufficientResources := fitsRequest(computePodResourceRequest(test.pod), test.nodeInfo, p.(*Fit).ignoredResources, p.(*Fit).ignoredResourceGroups)    // ④
            // 对比 gotInsufficientResources 和 test.wantInsufficientResources
            }
        })
    }
}

首先①创建了一个节点,并给出了该节点拥有的CPU核数和内存大小等资源。

②新建了一个名为Fitinterface framework.PluginPluginFilterPlugin等接口的父接口,所有下面才有p.(framework.FilterPlugin)这样的类型转换。

Filter()的核心逻辑是检查给定的Node是否能提供足够的资源。函数fitsRequest()的实现很直观,就是简单的数值比较。如果insufficientResources为空,表示Node能提供Pod所需的资源;不为空时,Filter()会将缺乏的资源(类型为InsufficientResource)逐一转换成调度失败的原因insufficientResources[i].Reason

// kubernetes/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.
func (f *Fit) Filter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
    // ...

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

    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
}

为了对比InsufficientResource类型的值,④这里又调用了一遍函数fitsRequest()

了解了框架以后,就可以来看一些测试用例了。

BestEffort等级——没有设置任何requestslimits的Pod如何调度

BestEffort等级意味着:

  • 容器没有任何资源保证,在最坏情况下,它们分不到任何CPU时间
  • 在需要为其他Pod释放内存时,这些容器会第一批被杀死
  • 不过因为BestEffort级别的Pod没有配置内存limits,当有充足的可用内存时,这些容器可以使用任意多的内存
// kubernetes/pkg/scheduler/framework/plugins/noderesources/fit_test.go
{
    pod: &v1.Pod{},
    nodeInfo: framework.NewNodeInfo(
        newResourcePod(framework.Resource{MilliCPU: 10, Memory: 20})),
    name:                      "no resources requested always fits",
    wantInsufficientResources: []InsufficientResource{},
},

framework.NewNodeInfo(newResourcePod(framework.Resource{MilliCPU: 10, Memory: 20}))表示节点已经使用了10毫核的CPU和20(KB?)的资源。

从这个测试用例可以看出,BestEffort等级的Pod总是能够调度成功

Burstable等级和Guaranteed等级的Pod如何调度

由于调度时Scheduler只参考Pod的资源申请量(requests),所以Burstable等级(既非BestEffort等级、也非Guaranteed等级)和Guaranteed等级(所有资源的requestlimits都相等) 的Pod此时没有差异。

成功调度的用例。

{
  pod: newResourcePod(framework.Resource{MilliCPU: 1, Memory: 1}),
  nodeInfo: framework.NewNodeInfo(
    newResourcePod(framework.Resource{MilliCPU: 5, Memory: 5})),
  name:                      "both resources fit",
  wantInsufficientResources: []InsufficientResource{},
},

失败调度的用例。

{
  pod: newResourcePod(framework.Resource{MilliCPU: 1, Memory: 1}),
  nodeInfo: framework.NewNodeInfo(
    newResourcePod(framework.Resource{MilliCPU: 10, Memory: 20})),
  name:       "too many resources fails",
  wantStatus: framework.NewStatus(framework.Unschedulable, getErrReason(v1.ResourceCPU), getErrReason(v1.ResourceMemory)),
  wantInsufficientResources: []InsufficientResource{
    {ResourceName: v1.ResourceCPU, Reason: getErrReason(v1.ResourceCPU), Requested: 1, Used: 10, Capacity: 10},
    {ResourceName: v1.ResourceMemory, Reason: getErrReason(v1.ResourceMemory), Requested: 1, Used: 20, Capacity: 20},
  },
},

apiVersion: v1
kind: Pod
metadata:
  name: requests-pod
spec:
  containers:
    - image: busybox
      command: ["dd", "if=/dev/zero", "of=/dev/null"]
      name: main
      resources:
        requests:
          cpu: 200m
          memory: 10Mi

在minikube中,修改Scheduler的日志级别

minikube.sigs.k8s.io/docs/handbo…

$ minikube start --extra-config=scheduler.v=8