简介
Kubernetes 是一个强大的编排工具,可以用来很方便的管理许多台机器,为了使机器的资源利用率提高,同时也尽可能的把压力分摊到各个机器上,这个职责就是由 scheduler 来完成的。
Kubernetes scheduler 是一个策略丰富、拓扑感知、工作负载特定的功能,显著影响可用性、性能和容量。
为了能更好的使用它,所以从源码的角度,对它进行一个全方位的分析与学习。
scheduler 的功能不多,但逻辑比较复杂,里面有很多考虑的因素,总结下来大致有如下几点:
Leader 选主,确保集群中只有一个 scheduler 在工作,其它只是高可用备份实例。通过 endpoint:kube-scheduler 作为仲裁资源。
Node 筛选,根据设置的条件、资源要求等,匹配出所有满足分配的 Node 结点。
最优 Node 选择。在所有满足条件的 Node 中,根据定义好的规则来打分,取分数最高的。如果有相同分数的,则采用轮询方式。
为了响应高优先级的资源分配,增加了抢占功能。scheduler 有权删除一些低优先级的 Pod,以释放资源给高优先级的 Pod 来使用。
功能说明
代码看下来比较困难,下面将分几个场景来描述 scheduler 工作的过程:
1、环境说明(假设 3 台机器,分别是 8C16G)
场景一:资源分配——最基本的功能
2、先分配一个请求 2C4G 的 Pod:A
场景二:机器负载均衡——评分机制
3、再分配一个请求 2C4G 的 Pod:B(尽管 node1 上还有空闲资源可分配 B,但 node2 和 node3 空闲资源更多,打分更高,所以分配到了 node2<选择 node2 还是 node3,是由 schedule 轮询选择的>。)
4、同理,如果再分配一个 C,scheduler 会优先分配到 node3 上
场景三:资源抢占——特权机制
5、现在 3 个 Node 上都分配了 2C4G,就是都剩余 6C12G,如果我这个时候分配一个 8C12G 的 Pod:D,在同优先级的情况下,D 将不会分配,处于 Pending 状态,因为三台机器都资源不足。
6、如果这个时候,我给 D 设置一个高的优先级,schedule 会删除一台机器上的 Pod,比如 A,然后资源足够了,将 D 分配到 node1 上,再将 A 分配到 node2 或 node3 上(这里分配是一个类似,因为三台都是一样的)
7、下面实战一把,详细试验下 scheduler 的抢占过程:
我有一个 Deployment,有 3 个复本,分别分配到两台机器上。(为什么用这个例子,是为了说明,抢占一定会发生在 10-10-40-89 上,因为要删除的 Pod 最少)
这个时候,我创建一个高优先级的 Deployment:
快速查询,能看到下面的阶段:
第一步,将要分配的 testpc-745cc7867-fqbp2 设置为“提名 Pod”,这个名字后面会再出现,同时删除原 10-10-40-89 上的 testpod,由于截的比较慢,下图中新的 testpod 已经在 10-10-88-99 上创建了。
第二步,提名 Pod 将会分配到对应的结点上(等待 Terminating 状态的 Pod 释放完资源后)。
第三步,资源足够,Pod 正常 Running。
最后展示下 watch 情况下的事件:
测试我共有两个 yaml 文件,如下:
testpod.yaml:
testpc.yaml:
场景四:关系户——亲和与反亲和
scheduler 在分配 Pod 时,考虑的要素很多,亲和性和反亲和,是一个比较常用的,在这里做一个典型来讲讲。
比如在上图中,我新的 Pod:D,要求不能和 A 在一台机器上,和 B 的互斥打分是 100,和 C 的互斥打分是 10。表示说,D 一定不能和 A 在一台机器,尽可能不和 B、C 在同一台机器,实在没办法时(资源不足),D 更倾向于和 C 在一起。
样例:
通过对这四个应用场景的分析,对它的功能有了一个初步的了解。要想更全面、深入的了解它的功能,需要从它的源码来着手。下面将从源码层面来做深入分析。
代码分析
scheduler 总体结构
scheduler 的配置,基本都是采用默认配置,图中列出了它的配置加载流程,基本都是加载它自身的默认配置。
server.Run 为它的主体逻辑,之后会详细讲解。
重要配置讲解
图中,单独列出了两个 config 配置:
1、disablePreemption:
scheduler 有个抢占功能。当 Pod 调度发现无可用资源时,它会将比该 Pod 优先级低的 Pod 删除,以释放资源给它来调度。disablePreemption 默认为 false,表示开启抢占,如果需要关闭,则设置为 true。
2、既然说到优先级,所以我还列出来了优先级的设置方法
Kubernetes 中有个单独的优先级的资源,叫:PriorityClass,通过下面这个 yaml,能创建一个 PriorityClass。
然后可将这个 PriorityClass 关联到 Pod 上:
这样就完成的 Pod 优先级的设置。如果不设置,Pod 默认是同一优先级(为 0)。
特别注意:
static Pod 比较特殊,需要直接设置 priority,因为 kubelet 是根据 priority 来判断。
scheduler 启动流程
通过深入分析 server.Run,可看到如下流程:
server.Run 还是有一部分的配置处理流程。
schedulerConfig 中,根据默认的参数,加载了两大块内容:predicate、priority 函数。
predicate 函数用于做 Pod 是否可分配到 Node 上的检查函数
priority 函数,则用于选优。当可分配的 Node 有多个时,这个时候就会根据 priority 函数来给 node 打分,最终调度到分数最高的 Node 上。
Kubernetes 提供了这些默认的判断函数:
predicate:
1、CheckNodeConditionPredicate
we really don’t want to check predicates against unschedulable nodes.
检查 Node 状态:是否处于可调度状态等。
-----> 遍历 nodeInfo 中 Node 的所有状况:
如果 Node 类型为 ready,并且状态不是 True,则认为结点为 notReady
如果 Node 类型为 OutOfDisk,并且状态不是 False,则认为结点 OutOfDisk
如果 Node 类型为 NetworkUnavailable,并且状态不是 False,则认为结点状态为:NetworkUnavailable
检查 Node 的 spec,如果是 UnSchedulable,则认为结点为 UnSchedulable。
以上检查都通过,则返回匹配成功
2、PodFitsHost
we check the pod.spec.nodeName.
检查 pod.spec.nodeName 是否匹配。
----> 如果 Pod 未指定 NodeName,则返回匹配成功。
检查 Node 的名字,如果与 Pod 指定的同名,则匹配成功,否则返回:nodeName 不匹配。
3、PodFitsHostPorts
we check ports asked on the spec.
检查服务端口是否被占用。
-----> 如果元数据 metadata 中有定义需要的 podPorts,则直接从元数据中取,否则从 Pod 的所有容器中获取需要的 port。
如果需要的 port 为空,则返回匹配成功。
从 nodeInfo 中获取当前已经使用的 port,如果有冲突,则返回:端口不匹配,否则返回匹配成功。
4、PodMatchNodeSelector
check node label after narrowing search.
检查 label 是否匹配。
------> 如果 Pod 中定义了 NodeSelector,则根据选择来匹配 Node 的 labels,如果不匹配,则返回 NodeSelectorNotMatch。
如果 Pod 的 Affinity 中定义了 NodeAffinity,则检查结点亲和关系:
如果未定义 requiredDuringSchedulingIgnoredDuringExecution,则直接返回匹配。
如果定义了 requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms,则里面有一个匹配,则匹配。否则认为不匹配。
特别的:如果 nodeSelectorTerms 为 nil,则全不匹配;如果 nodeSelectorTerms 不为 nil,但是空的切片,则全不匹配;同样,nodeSelectorTerms 中的 MatchExpressions,如果为 nil 或者是空切片,也不匹配。
5、PodFitsResources
this one comes here since it’s not restrictive enough as we do not try to match values but ranges.
-----> 检查 Node 的 allowedPodNumber 是否超过,如果超过,增加超限错误(此处未直接返回,会把所有错误检查完一次性返回)。
检查元数据中是否有定义 podRequest、ignoredExtendedResources,如果定义了,则从元数据中取。否则从 Pod 中每个容器中取:先检查所有 container 中所有需要的资源总合,再检查 initContainer 中,如果有资源比总合还大,则取较大的为所需要的资源。
如果需要的资源都为 0,则返回检查结果。
获取 Node 的可用资源,检查需要新申请的资源+已申请的资源是否超过可用资源,如果超过,则记录资源不足。
检查所有 Pod 扩展资源,并判断扩展资源是否需要检查(ignoredExtendedResources),如果需要检查,则判断资源是否足够,不足够则记录失败。
返回检查结果(如果无失败,是检查成功)。
6、NoDiskConflict
Following the resource predicate, we check disk.
----> 遍历 Pod 所有存储、Node 下的所有 Pod,检查是否有存储冲突:
如果 Pod 无存储(无 GCE、AWS、RBD、ISCSI),则检查通过。
7、PodToleratesNodeTaints
check toleration here, as node might have toleration.
-----> 检查结点是否容忍 taint 环境:
参数:Pod 中定义的容忍规则:tolerations,Node 中的环境状态:taints,筛选规则:取 effect 为 NoSchedule、NoExecute 的。
如果 Node 无 taints,返回匹配成功。
遍历所有 taints,如果 taint 不满足筛选规则,则跳过检查。
遍历所有的容忍规则,检查是否有规则是允许结点的 taint 状态。检查步骤:
如果 effect 为空,则检查通过。否则要相同
如果 key 为空,则检查通过,否则要相同
如果 operator 为 Exists,则检查通过,如果为空或者是 Equal,则要相同,否则不通过
8、PodToleratesNodeNoExecuteTaints
check toleration here, as node might have toleration.
-----> 检查规则同上相似,只是筛选规则变了:取 effect 为 NoExecute 的。
9、CheckNodeLabelPresence
labels are easy to check, so this one goes before.
------> 检查 label 是否存在,不关心值。可设置 label 存在与不存在。
只有在 scheduler.CreateFromConfig(policy)才会初始化该检查,在 RegisterCustomFitPredicate 中注册,默认无该检查。
10、checkServiceAffinity
-----> 检查服务类同关系。
如果一个 Pod 的服务调度到有 label:"region=foo"的 Node,之后有相同服务的 Pod 都会调度到该 Node。
11、MaxPDVolumeCountPredicate
-----> 检查挂载的卷个数是不是超标,只支持:ESB:39,GCE:16,AzureDisk:16。
12、VolumeNodePredicate
-----> 无
13、VolumeZonePredicate
-----> 检查存储区域划分:
检查 Node 中是否有 label:failure-domain.beta.kubernetes.io/zone 或者 failure-domain.beta.kubernetes.io/region,如果有,则检查 Pod 存储情况。
遍历 Pod 需要的存储信息:
根据 PVC 名字获取 PVC 信息,取出 PVC 对应的 PV 名字,如果没有名字(表示还未绑定 PV),获取 PVC 的 StorageClassName,如果处理正在绑定中,则跳过不检查,否则返回匹配失败(因为 PVC 绑定失败)。
绑定成功的,根据 pvName 获取对应的 PV 信息,检查 PV 的标签,如果 PV 有上面两个标签(zone、region),检查 PV 的值中(值可能有多个,用__分隔),是否包含 Node 对应标签的值,如果没有包含,则返回匹配失败。
14、CheckNodeMemoryPressurePredicate
doesn’t happen often.
-----> 检查 Node 内存压力。
15、CheckNodeDiskPressurePredicate
doesn’t happen often.
16、InterPodAffinityMatches
Most expensive predicate to compute.
默认有这些打分函数(priority):
SelectorSpreadPriority:根据相同的 RC 和服务拆分,使每个 Node 具有相同服务或 RC 的 Pod 尽量少,spreads pods by minimizing the number of pods (belonging to the same service or replication controller) on the same node.
InterPodAffinityPriority:根据 Pod 共性来分配,pods should be placed in the same topological domain (e.g. same node, same rack, same zone, same power domain, etc.).
LeastRequestedPriority:选择比较闲的 node,Prioritize nodes by least requested utilization.
BalancedResourceAllocation:从资源分配平衡性来考虑分配,Prioritizes nodes to help achieve balanced resource usage.
NodePreferAvoidPodsPriority:用于用户自定义分配,权重 10000 起,方便用户来指定。0 的时候不起作用。用户通过这个来指定:scheduler.alpha.kubernetes.io/preferAvoidPods Set this weight large enough to override all other priority functions.
NodeAffinityPriority:根据结点关系来分配,Prioritizes nodes that have labels matching NodeAffinity.
TaintTolerationPriority:根据 pod 设置的容忍项来分配,Prioritizes nodes that marked with taint which pod can tolerate.
最终,死循环进入:scheduleOne,真正开始 schedule 的调度流程。
Schedule 调度流程
先讲主流程:
主流程分为以下 8 步:
从 Pod 队列中取出一个需要调度的 Pod
尝试调度该 Pod
调度失败,则尝试抢占 Pod
调度成功后,尝试做 volumes 绑定
由于 reserve 插件暂时未启用,暂未分析
尝试将 Pod 分配到 Node 上
真正实现绑定。第 4 步和第 6 步中,都只是对 schedule 的 cache 的操作,先确保对 cache 的操作能完成,最终在第 7 步,异常实现将 cache 中的修改应用到 apiserver 中。如果应用失败,会将 pod 的分配信息从 cache 中清除,重新进行 scheduler。
最复杂也最核心的,就是第 2 步和第 3 步,下面分别进行分析。
调度 Pod 流程
调度 Pod,就是尝试将 Pod 分配到 Node 上,流程如下:
共有 7 点,将逐步分析:
Pod 基本检查,检查 Pod 是否有了对应的 PVC,这里只是检查 PVC 是否存在,不关心绑定过程
取出所有 Node 列表
将 nodeInfo 应用到缓存中。全局 nodeInfo 中保存了当前 Node 的真实数据信息,而 cache 中会有调度过程的假设分析的信息。
检查 Pod 是否可调度到 Node 上,返回可调度的 Node 列表
a) 这里的检查,是针对前面初始化时,注册的 predicate 函数,如果有不符合,则认为不可调度。
b) 这里会尝试两次,之所以两次,是因为有“提名 Pod”的存在。暂时先不管“提名 Pod”哪来的,后面会讲到。提名 Pod,就是说,这个 Pod 已经分配到 Node 上,但它还未应用到 Kubernetes 环境,目前只是占着这个坑位,要调度的 Pod,在调度的过程中,也需要考虑它所占的资源。
c) 第一次时,会先把优先级大于当前 Pod 的提名 Pod 分配到 Node 中(添加到一个临时的 nodeInfo 中),然后检查所有的 predicat 函数是否通过。
d) 第二次时,不添加提名 Pod,再检查所有的 predicate 函数。之所以有第二次,是因为提名 Pod 实际还并不存在,有些 Pod 亲和性可能会判断有误。
e) 当然,如果没有提名 Pod,则不需要第二次判断。
如果找不到,则返回失败。如果只找到一个,则返回该 Node。
当找到多个 Node 时,会去给 Node 打分,打分规则如下:
a) 如果没有定义打分规则,则返回所有分数都为 1。schedule 默认是有打分函数的,前面初始化中有讲。
b) 运行早期老版本的打分函数。早期就是单纯的一个 function,运行后得到打分结果。
c) 新版本,将打分函数拆分成两步,map 和 reduce,先按 16 个并发运行 map,之后运行 reduce 统计执行结果。
d) 这里还预留了扩展支持。
e) 最终返回打分结果。
根据打分结果,选择 Node。
a) 先取出得分最高的 Node 列表
b) 然后按 round-robin 的方式选择 Node
由于相同最高分的 Node 可能有多个,genericScheduler 采用 round-robin 的方式:它自己记录一个全局的 lastNodeIndex,如何 num 为当前有相同最高分的节点数,则用 lastNodeIndex % num 来选取本次节点的下标,之后 lastNodeIndex 加 1,实现轮询调度。
到此,Pod 的调度流程分析完成。当中有个特别的东西:提名 Pod(NominatedPod),它的出现和下面讲的抢占流程有关。
Pod 抢占流程
抢占的流程,比调度复杂一些,主要分两大步:抢占分析和抢占。第一步是检查是不是能完成抢占,第二步是执行抢占(删除 Pod)。
抢占检查
检查 Pod 是否可以发起抢占:如果 Pod 是提名 Pod(已经预分配到 Node),并且该 Node 上有处于 terminating 的 Pod p,并且 p 的优先级小于当前 Pod,则不允许发起抢占。
获取所有 Node 清单。
获取可能的 Node。检查调度失败原因,如果是 nodeNotReady 这种原因,则 Node 不参与抢占。
这些都是不参与抢占的:
predicates.ErrNodeSelectorNotMatch,
predicates.ErrPodAffinityRulesNotMatch,
predicates.ErrPodNotMatchHostName,
predicates.ErrTaintsTolerationsNotMatch,
predicates.ErrNodeLabelPresenceViolated,
predicates.ErrNodeNotReady,
predicates.ErrNodeNetworkUnavailable,
predicates.ErrNodeUnderDiskPressure,
predicates.ErrNodeUnderPIDPressure,
predicates.ErrNodeUnderMemoryPressure,
predicates.ErrNodeUnschedulable,
predicates.ErrNodeUnknownCondition,
predicates.ErrVolumeZoneConflict,
predicates.ErrVolumeNodeConflict,
predicates.ErrVolumeBindConflict
如果可抢占的 Node 没有,则结束。
获取 pdb 列表:pdb is PodDisruptionBudget. 这个是预算的定义,比如 statefulset 定义了 3 个复本,而我们定义了,允许其中 1 个 Pod 可以挂掉
获取通过抢占(删除一些 Pod),能完成调度的 Node 列表
a) 将比当前 Pod 优先级低的 Pod 全部从 nodeInfoCopy 中删除,然后尝试去调度
b) 如果调度失败,则表示无法抢占。(因为不能删除比它优先级高的)
c) 将要删除的 Pod,根据 pdb 进行拆分:nonViolatingVictim 和 violatingVictim。说明见图中
d) 然后尝试将 violatingVictim 中的 Pod 一个个加进去,尝试能不能调度。numViolatingVictim 中记录不通过数
e) 然后尝试将 nonViolatingVictim 中的 Pod 一个个加进去,尝试能不能调度。victims 记录不通过的 Pod 信息
f) 返回 victims 和 numViolatingVictim
extenders 扩展保留
从可抢占的 Node 列表中,选择最合适的一个 Node。按如下规则进行选择:
a) node pdb violations 最小。就是上面返回的 numViolatingVictim
b) 如果只有一个 Node 满足,则返回该 Node
c) 比较 Node 中 victims 中优先级的最高值,取最小的那个。最高:取的是单个 Node 中,优先级的最高值。最小:取的是所有 Node 中的最小值
d) 如果只有一个,则返回该 Node
e) 取 Node 中 victims 优先级总和最小的
f) 如果只有一个,则返回该 Node
g) 取 Node 中 victims 的 Pod 数最小的
h) 返回第一个
如果无合适的,则结束
获取比当前优先级小的提名 Pod
返回 Node 信息,需要删除的 Pod 列表,优先级小的提名 Pod
到此,抢占检查结束。得到期望调度的 Node、要调度到这个 Node 上,需要删除的 Pod 列表、以及比当前 Pod 优先级小的提名 Pod。
抢占执行流程(找到了期望的 Node 才会进入)
将当前 Pod,变更为提名 Pod,对应的 Node 为期望的 Node。这里就是提名 Pod 出现的原因。
将提名 Pod 信息更新到 apiServer
遍历 victims(抢占流程返回的需要删除的 Pod 列表),删除 Pod,并记录 event
遍历 nominatedPodsToClear(抢占返回的比当前 Pod 优先级小的提名 Pod),清空提名 Pod 配置,并更新 apiServer
到此,调度流程分析完成。
评论