10.深入k8s:排程的優先順序及搶佔機制原始碼分析
阿新 • • 發佈:2020-09-13
> 轉載請宣告出處哦~,本篇文章釋出於luozhiyun的部落格:https://www.luozhiyun.com
>
> 原始碼版本是[1.19](https://github.com/kubernetes/kubernetes/tree/release-1.19)
![84253409_p0](https://img.luozhiyun.com/20200912223315.png)
上一篇我們將了獲取node成功的情況,如果是一個優先pod獲取node失敗,那麼就會進入到搶佔環節中,那麼搶佔環節k8s會做什麼呢,搶佔是如何發生的,哪些資源會被搶佔這些都是我們這篇要研究的內容。
## 排程的優先順序與搶佔機制
正常情況下,當一個 Pod 排程失敗後,它就會被暫時“擱置”起來,直到 Pod 被更新,或者叢集狀態發生變化,排程器才會對這個 Pod 進行重新排程。但是我們可以通過PriorityClass優先順序來避免這種情況。通過設定優先順序一些優先順序比較高的pod,如果pod 排程失敗,那麼並不會被”擱置”,而是會”擠走”某個 node 上的一些低優先順序的 pod,這樣就可以保證高優先順序的 pod 排程成功。
要使用PriorityClass,首先我們要定義一個PriorityClass物件,例如:
```yaml
apiVersion: v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."
```
value越高則優先順序越高;globalDefault 被設定為 true 的話,那就意味著這個 PriorityClass 的值會成為系統的預設值,如果是false則表示我們只希望宣告使用該 PriorityClass 的 Pod 擁有值為 1000000 的優先順序,而對於沒有宣告 PriorityClass 的 Pod 來說,它們的優先順序就是 0。
Pod 就可以宣告使用它了:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
```
高優先順序的 Pod 排程失敗的時候,排程器的搶佔能力就會被觸發。排程器就會試圖從當前叢集裡尋找一個節點,使得當這個節點上的一個或者多個低優先順序 Pod 被刪除後,待排程的高優先順序 Pod 就可以被排程到這個節點上。
高優先順序Pod進行搶佔的時候會將pod的 nominatedNodeName 欄位,設定為被搶佔的 Node 的名字。然後,在下一週期中決定是不是要執行在被搶佔的節點上,當這個Pod在等待的時候,如果有其他更高優先順序的 Pod 也要搶佔同一個節點,那麼排程器就會清空原搶佔者的 spec.nominatedNodeName 欄位,從而允許更高優先順序的搶佔者執行搶佔。
## 原始碼解析
這裡我依舊拿出這張圖來進行講解,上一篇我們將了獲取node成功的情況,如果是一個優先pod獲取node失敗,那麼就會進入到搶佔環節中。
![排程流程](https://img.luozhiyun.com/20200905190824.png)
通過上一篇的分析,我們知道,在scheduleOne方法中執行sched.Algorithm.Schedule會選擇一個合適的node節點,如果獲取node失敗,那麼就會進入到一個if邏輯中執行搶佔。
程式碼路徑:pkg/scheduler/scheduler.go
```go
func (sched *Scheduler) scheduleOne(ctx context.Context) {
...
//為pod資源物件選擇一個合適的節點
scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, prof, state, pod)
//獲取node失敗,搶佔邏輯
if err != nil {
//上面呼叫失敗之後,下面會根據pod執行搶佔
nominatedNode := ""
if fitError, ok := err.(*core.FitError); ok {
if !prof.HasPostFilterPlugins() {
klog.V(3).Infof("No PostFilter plugins are registered, so no preemption will be performed.")
} else {
result, status := prof.RunPostFilterPlugins(ctx, state, pod, fitError.FilteredNodesStatuses)
if status.Code() == framework.Error {
klog.Errorf("Status after running PostFilter plugins for pod %v/%v: %v", pod.Namespace, pod.Name, status)
} else {
klog.V(5).Infof("Status after running PostFilter plugins for pod %v/%v: %v", pod.Namespace, pod.Name, status)
}
//搶佔成功後,將nominatedNodeName設定為被搶佔的 Node 的名字,然後重新進入下一個排程週期
if status.IsSuccess() && result != nil {
nominatedNode = result.NominatedNodeName
}
}
metrics.PodUnschedulable(prof.Name, metrics.SinceInSeconds(start))
} else if err == core.ErrNoNodesAvailable {
metrics.PodUnschedulable(prof.Name, metrics.SinceInSeconds(start))
} else {
klog.ErrorS(err, "Error selecting node for pod", "pod", klog.KObj(pod))
metrics.PodScheduleError(prof.Name, metrics.SinceInSeconds(start))
}
sched.recordSchedulingFailure(prof, podInfo, err, v1.PodReasonUnschedulable, nominatedNode)
return
}
...
}
```
在這個方法裡面RunPostFilterPlugins會執行具體的搶佔邏輯,然後返回被搶佔的node節點。搶佔者並不會立刻被排程到被搶佔的 node 上,排程器只會將搶佔者的 status.nominatedNodeName 欄位設定為被搶佔的 node 的名字。然後,搶佔者會重新進入下一個排程週期,在新的排程週期裡來決定是不是要執行在被搶佔的節點上,當然,即使在下一個排程週期,排程器也不會保證搶佔者一定會執行在被搶佔的節點上。
這樣設計的一個重要原因是排程器只會通過標準的 DELETE API 來刪除被搶佔的 pod,所以,這些 pod 必然是有一定的“優雅退出”時間(預設是 30s)的。而在這段時間裡,其他的節點也是有可能變成可排程的,或者直接有新的節點被新增到這個叢集中來。
而在搶佔者等待被排程的過程中,如果有其他更高優先順序的 pod 也要搶佔同一個節點,那麼排程器就會清空原搶佔者的 status.nominatedNodeName 欄位,從而允許更高優先順序的搶佔者執行搶佔,並且,這也使得原搶佔者本身也有機會去重新搶佔其他節點。
接著我們繼續看,RunPostFilterPlugins會遍歷所有的postFilterPlugins,然後執行runPostFilterPlugin方法:
```go
func (f *frameworkImpl) RunPostFilterPlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod, filteredNodeStatusMap framework.NodeToStatusMap) (_ *framework.PostFilterResult, status *framework.Status) {
startTime := time.Now()
defer func() {
metrics.FrameworkExtensionPointDuration.WithLabelValues(postFilter, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
}()
statuses := make(framework.PluginToStatus)
//postFilterPlugins裡面只有一個defaultpreemption
for _, pl := range f.postFilterPlugins {
r, s := f.runPostFilterPlugin(ctx, pl, state, pod, filteredNodeStatusMap)
if s.IsSuccess() {
return r, s
} else if !s.IsUnschedulable() {
// Any status other than Success or Unschedulable is Error.
return nil, framework.NewStatus(framework.Error, s.Message())
}
statuses[pl.Name()] = s
}
return nil, statuses.Merge()
}
```
根據我們上一節看的scheduler的初始化可以知道設定的PostFilter如下:
程式碼路徑:pkg/scheduler/algorithmprovider/registry.go
```
PostFilter: &schedulerapi.PluginSet{
Enabled: []schedulerapi.Plugin{
{Name: defaultpreemption.Name},
},
},
```
可見,目前只有一個defaultpreemption來執行搶佔邏輯,在postFilterPlugins迴圈裡面會呼叫到runPostFilterPlugin然後執行defaultpreemption的PostFilter方法,最後執行到preempt執行具體搶佔邏輯。
程式碼路徑:pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go
```go
func (pl *DefaultPreemption) PostFilter(...) (*framework.PostFilterResult, *framework.Status) {
...
//執行搶佔
nnn, err := pl.preempt(ctx, state, pod, m)
...
return &framework.PostFilterResult{NominatedNodeName: nnn}, framework.NewStatus(framework.Success)
}
```
搶佔的執行流程圖如下:
![搶佔](https://img.luozhiyun.com/20200912223345.png)
程式碼路徑:pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go
```go
func (pl *DefaultPreemption) preempt(ctx context.Context, state *framework.CycleState, pod *v1.Pod, m framework.NodeToStatusMap) (string, error) {
cs := pl.fh.ClientSet()
ph := pl.fh.PreemptHandle()
//返回node列表
nodeLister := pl.fh.SnapshotSharedLister().NodeInfos()
pod, err := util.GetUpdatedPod(cs, pod)
if err != nil {
klog.Errorf("Error getting the updated preemptor pod object: %v", err)
return "", err
}
//確認搶佔者是否能夠進行搶佔,如果對應的node節點上的pod正在優雅退出(Graceful Termination ),那麼就不應該進行搶佔
if !PodEligibleToPreemptOthers(pod, nodeLister, m[pod.Status.NominatedNodeName]) {
klog.V(5).Infof("Pod %v/%v is not eligible for more preemption.", pod.Namespace, pod.Name)
return "", nil
}
// 查詢所有搶佔候選者
candidates, err := FindCandidates(ctx, cs, state, pod, m, ph, nodeLister, pl.pdbLister)
if err != nil || len(candidates) == 0 {
return "", err
}
//若有 extender 則執行
candidates, err = CallExtenders(ph.Extenders(), pod, nodeLister, candidates)
if err != nil {
return "", err
}
// 查詢最佳搶佔候選者
bestCandidate := SelectCandidate(candidates)
if bestCandidate == nil || len(bestCandidate.Name()) == 0 {
return "", nil
}
// 在搶佔一個node之前做一些準備工作
if err := PrepareCandidate(bestCandidate, pl.fh, cs, pod); err != nil {
return "", err
}
return bestCandidate.Name(), nil
}
```
preempt方法首先會去獲取node列表,然後獲取最新的要執行搶佔的pod資訊,接著分下面幾步執行搶佔:
1. 呼叫PodEligibleToPreemptOthers方法,檢查搶佔者是否能夠進行搶佔,如果當前的pod已經搶佔了一個node節點或者在被搶佔node節點中有pod正在執行優雅退出,那麼不應該執行搶佔;
2. 呼叫FindCandidates找到所有node中能被搶佔的node節點,並返回候選列表以及node節點中需要被刪除的pod;
3. 若有 extender 則執行CallExtenders;
4. 呼叫SelectCandidate方法在所有候選列表中找出最合適的node節點執行搶佔;
5. 呼叫PrepareCandidate方法刪除被搶佔的node節點中victim(犧牲者),以及清除NominatedNodeName欄位資訊;
**PodEligibleToPreemptOthers**
```go
func PodEligibleToPreemptOthers(pod *v1.Pod, nodeInfos framework.NodeInfoLister, nominatedNodeStatus *framework.Status) bool {
if pod.Spec.PreemptionPolicy != nil && *pod.Spec.PreemptionPolicy == v1.PreemptNever {
klog.V(5).Infof("Pod %v/%v is not eligible for preemption because it has a preemptionPolicy of %v", pod.Namespace, pod.Name, v1.PreemptNever)
return false
}
//檢視搶佔者是否已經搶佔過
nomNodeName := pod.Status.NominatedNodeName
if len(nomNodeName) > 0 {
if nominatedNodeStatus.Code() == framework.UnschedulableAndUnresolvable {
return true
}
//獲取被搶佔的node節點
if nodeInfo, _ := nodeInfos.Get(nomNodeName); nodeInfo != nil {
//檢視是否存在正在被刪除並且優先順序比搶佔者pod低的pod
podPriority := podutil.GetPodPriority(pod)
for _, p := range nodeInfo.Pods {
if p.Pod.DeletionTimestamp != nil && podutil.GetPodPriority(p.Pod) < podPriority {
// There is a terminating pod on the nominated node.
return false
}
}
}
}
return true
}
```
這個方法會檢查該pod是否已經搶佔過其他node節點,如果是的話就遍歷節點上的所有pod物件,如果發現節點上有pod資源物件的優先順序小於待排程pod資源物件並處於終止狀態,則返回false,不會發生搶佔。
接下來看FindCandidates方法:
**FindCandidates**
```go
func FindCandidates(ctx context.Context, cs kubernetes.Interface, state *framework.CycleState, pod *v1.Pod,
m framework.NodeToStatusMap, ph framework.PreemptHandle, nodeLister framework.NodeInfoLister,
pdbLister policylisters.PodDisruptionBudgetLister) ([]Candidate, error) {
allNodes, err := nodeLister.List()
if err != nil {
return nil, err
}
if len(allNodes) == 0 {
return nil, core.ErrNoNodesAvailable
}
//找 predicates 階段失敗但是通過搶佔也許能夠排程成功的 nodes
potentialNodes := nodesWherePreemptionMightHelp(allNodes, m)
if len(potentialNodes) == 0 {
klog.V(3).Infof("Preemption will not help schedule pod %v/%v on any node.", pod.Namespace, pod.Name)
if err := util.ClearNominatedNodeName(cs, pod); err != nil {
klog.Errorf("Cannot clear 'NominatedNodeName' field of pod %v/%v: %v", pod.Namespace, pod.Name, err)
}
return nil, nil
}
if klog.V(5).Enabled() {
var sample []string
for i := 0; i < 10 && i < len(potentialNodes); i++ {
sample = append(sample, potentialNodes[i].Node().Name)
}
klog.Infof("%v potential nodes for preemption, first %v are: %v", len(potentialNodes), len(sample), sample)
}
//獲取PDB物件,PDB能夠限制同時終端的pod資源物件的數量,以保證叢集的高可用性
pdbs, err := getPodDisruptionBudgets(pdbLister)
if err != nil {
return nil, err
}
//尋找符合條件的node,並封裝成candidate陣列返回
return dryRunPreemption(ctx, ph, state, pod, potentialNodes, pdbs), nil
}
```
FindCandidates方法首先會獲取node列表,然後呼叫nodesWherePreemptionMightHelp方法來找出predicates 階段失敗但是通過搶佔也許能夠排程成功的nodes,因為並不是所有的node都可以通過搶佔來排程成功。最後呼叫dryRunPreemption方法來獲取符合條件的node節點。
**dryRunPreemption**
```go
func dryRunPreemption(ctx context.Context, fh framework.PreemptHandle, state *framework.CycleState,
pod *v1.Pod, potentialNodes []*framework.NodeInfo, pdbs []*policy.PodDisruptionBudget) []Candidate {
var resultLock sync.Mutex
var candidates []Candidate
checkNode := func(i int) {
nodeInfoCopy := potentialNodes[i].Clone()
stateCopy := state.Clone()
//找到node上被搶佔的pod,也就是victims
pods, numPDBViolations, fits := selectVictimsOnNode(ctx, fh, stateCopy, pod, nodeInfoCopy, pdbs)
if fits {
resultLock.Lock()
victims := extenderv1.Victims{
Pods: pods,
NumPDBViolations: int64(numPDBViolations),
}
c := candidate{
victims: &victims,
name: nodeInfoCopy.Node().Name,
}
candidates = append(candidates, &c)
resultLock.Unlock()
}
}
parallelize.Until(ctx, len(potentialNodes), checkNode)
return candidates
}
```
這裡會開啟16個執行緒呼叫checkNode方法,checkNode方法裡面會呼叫selectVictimsOnNode方法來檢查這個node是不是能被執行搶佔,如果能執行搶佔返回的pods表示需要刪除的節點,然後封裝成candidate新增到candidates列表中返回。
**selectVictimsOnNode**
```go
func selectVictimsOnNode(
ctx context.Context,
ph framework.PreemptHandle,
state *framework.CycleState,
pod *v1.Pod,
nodeInfo *framework.NodeInfo,
pdbs []*policy.PodDisruptionBudget,
) ([]*v1.Pod, int, bool) {
var potentialVictims []*v1.Pod
//移除node節點的pod
removePod := func(rp *v1.Pod) error {
if err := nodeInfo.RemovePod(rp); err != nil {
return err
}
status := ph.RunPreFilterExtensionRemovePod(ctx, state, pod, rp, nodeInfo)
if !status.IsSuccess() {
return status.AsError()
}
return nil
}
//將node節點新增pod
addPod := func(ap *v1.Pod) error {
nodeInfo.AddPod(ap)
status := ph.RunPreFilterExtensionAddPod(ctx, state, pod, ap, nodeInfo)
if !status.IsSuccess() {
return status.AsError()
}
return nil
}
// 獲取pod的優先順序,並將node中所有優先順序低於該pod的呼叫removePod方法pod移除
podPriority := podutil.GetPodPriority(pod)
for _, p := range nodeInfo.Pods {
if podutil.GetPodPriority(p.Pod) < podPriority {
potentialVictims = append(potentialVictims, p.Pod)
if err := removePod(p.Pod); err != nil {
return nil, 0, false
}
}
}
//沒有優先順序低的node,直接返回
if len(potentialVictims) == 0 {
return nil, 0, false
}
if fits, _, err := core.PodPassesFiltersOnNode(ctx, ph, state, pod, nodeInfo); !fits {
if err != nil {
klog.Warningf("Encountered error while selecting victims on node %v: %v", nodeInfo.Node().Name, err)
}
return nil, 0, false
}
var victims []*v1.Pod
numViolatingVictim := 0
//將potentialVictims集合裡的pod按照優先順序進行排序
sort.Slice(potentialVictims, func(i, j int) bool { return util.MoreImportantPod(potentialVictims[i], potentialVictims[j]) })
//將pdb的pod分離出來
//基於 pod 是否有 PDB 被分為兩組 violatingVictims 和 nonViolatingVictims
//PDB:https://kubernetes.io/docs/concepts/workloads/pods/disruptions/
violatingVictims, nonViolatingVictims := filterPodsWithPDBViolation(potentialVictims, pdbs)
reprievePod := func(p *v1.Pod) (bool, error) {
if err := addPod(p); err != nil {
return false, err
}
fits, _, _ := core.PodPassesFiltersOnNode(ctx, ph, state, pod, nodeInfo)
if !fits {
if err := removePod(p); err != nil {
return false, err
}
// 加入到 victims 中
victims = append(victims, p)
klog.V(5).Infof("Pod %v/%v is a potential preemption victim on node %v.", p.Namespace, p.Name, nodeInfo.Node().Name)
}
return fits, nil
}
//刪除pod,並記錄刪除個數
for _, p := range violatingVictims {
if fits, err := reprievePod(p); err != nil {
klog.Warningf("Failed to reprieve pod %q: %v", p.Name, err)
return nil, 0, false
} else if !fits {
numViolatingVictim++
}
}
//刪除pod
for _, p := range nonViolatingVictims {
if _, err := reprievePod(p); err != nil {
klog.Warningf("Failed to reprieve pod %q: %v", p.Name, err)
return nil, 0, false
}
}
return victims, numViolatingVictim, true
}
```
這個方法首先定義了兩個方法,一個是removePod,另一個是addPod,這兩個方法都差不多,如果是removePod就會將pod從node中移除,然後修改node一些屬性,如將Requested.MilliCPU、Requested.Memory中減去,表示已用資源大小,將該pod從node節點的Pods列表中移除等等。
回到selectVictimsOnNode繼續往下,會遍歷node裡面的pod列表,如果找到優先順序小於搶佔pod的就加入到potentialVictims集合中,並呼叫removePod方法,將當前被遍歷的pod從node中移除。
接著會呼叫PodPassesFiltersOnNode方法,這個方法會執行兩次。第一次會呼叫addNominatedPods方法將排程佇列中找到節點上優先順序大於或等於當前pod資源物件的nominatedPods加入到nodeInfo物件中,然後執行FilterPlugin列表;第二次則直接執行FilterPlugins列表。之所以要這麼做,是由於親和性的關係,k8s需要判斷當前排程的pod親和性是否依賴了nominatedPods。
繼續往下會對potentialVictims按照優先順序進行排序,優先順序高的在前面。
接著會呼叫filterPodsWithPDBViolation方法,將 PDB 約束的 Pod和未約束的Pod分離成兩個組,然後會分別遍歷violatingVictims和nonViolatingVictims呼叫reprievePod方法對pod進行移除。這裡我們在[官方文件](https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#poddisruptionbudget-%E6%98%AF%E8%A2%AB%E6%94%AF%E6%8C%81%E7%9A%84-%E4%BD%86%E4%B8%8D%E6%8F%90%E4%BE%9B%E4%BF%9D%E8%AF%81)也可以看其設計理念,PodDisruptionBudget 是在搶佔中被支援的,但不提供保證,然後將被移除的pod新增到victims列表中,並記錄好被刪除的刪除pod個數,最後返回。
到這裡整個FindCandidates方法就探索完畢了,還是比較長的,我們繼續回到preempt方法中往下看,SelectCandidate方法會查詢最佳搶佔候選者。
**SelectCandidate**
```go
func SelectCandidate(candidates []Candidate) Candidate {
if len(candidates) == 0 {
return nil
}
if len(candidates) == 1 {
return candidates[0]
}
victimsMap := candidatesToVictimsMap(candidates)
// 選擇1個 node 用於 schedule
candidateNode := pickOneNodeForPreemption(victimsMap)
for _, candidate := range candidates {
if candidateNode == candidate.Name() {
return candidate
}
}
klog.Errorf("None candidate can be picked from %v.", candidates)
return candidates[0]
}
```
這個方法裡面會呼叫candidatesToVictimsMap方法做一個name和victims對映map,然後呼叫pickOneNodeForPreemption執行主要過濾邏輯。
**pickOneNodeForPreemption**
```go
func pickOneNodeForPreemption(nodesToVictims map[string]*extenderv1.Victims) string {
//若該 node 沒有 victims 則返回
if len(nodesToVictims) == 0 {
return ""
}
minNumPDBViolatingPods := int64(math.MaxInt32)
var minNodes1 []string
lenNodes1 := 0
//尋找 PDB violations 數量最小的 node
for node, victims := range nodesToVictims {
numPDBViolatingPods := victims.NumPDBViolations
if numPDBViolatingPods < minNumPDBViolatingPods {
minNumPDBViolatingPods = numPDBViolatingPods
minNodes1 = nil
lenNodes1 = 0
}
if numPDBViolatingPods == minNumPDBViolatingPods {
minNodes1 = append(minNodes1, node)
lenNodes1++
}
}
//如果最小的node只有一個,直接返回
if lenNodes1 == 1 {
return minNodes1[0]
}
minHighestPriority := int32(math.MaxInt32)
var minNodes2 = make([]string, lenNodes1)
lenNodes2 := 0
// 找到node裡面pods 最高優先順序最小的
for i := 0; i < lenNodes1; i++ {
node := minNodes1[i]
victims := nodesToVictims[node]
highestPodPriority := podutil.GetPodPriority(victims.Pods[0])
if highestPodPriority < minHighestPriority {
minHighestPriority = highestPodPriority
lenNodes2 = 0
}
if highestPodPriority == minHighestPriority {
minNodes2[lenNodes2] = node
lenNodes2++
}
}
if lenNodes2 == 1 {
return minNodes2[0]
}
// 找出node裡面Victims列表優先順序加和最小的
minSumPriorities := int64(math.MaxInt64)
lenNodes1 = 0
for i := 0; i < lenNodes2; i++ {
var sumPriorities int64
node := minNodes2[i]
for _, pod := range nodesToVictims[node].Pods {
sumPriorities += int64(podutil.GetPodPriority(pod)) + int64(math.MaxInt32+1)
}
if sumPriorities < minSumPriorities {
minSumPriorities = sumPriorities
lenNodes1 = 0
}
if sumPriorities == minSumPriorities {
minNodes1[lenNodes1] = node
lenNodes1++
}
}
if lenNodes1 == 1 {
return minNodes1[0]
}
// 找到node列表中需要犧牲的pod數量最小的
minNumPods := math.MaxInt32
lenNodes2 = 0
for i := 0; i < lenNodes1; i++ {
node := minNodes1[i]
numPods := len(nodesToVictims[node].Pods)
if numPods < minNumPods {
minNumPods = numPods
lenNodes2 = 0
}
if numPods == minNumPods {
minNodes2[lenNodes2] = node
lenNodes2++
}
}
if lenNodes2 == 1 {
return minNodes2[0]
}
//若多個 node 的 pod 數量相等,則選出高優先順序 pod 啟動時間最短的
latestStartTime := util.GetEarliestPodStartTime(nodesToVictims[minNodes2[0]])
if latestStartTime == nil {
klog.Errorf("earliestStartTime is nil for node %s. Should not reach here.", minNodes2[0])
return minNodes2[0]
}
nodeToReturn := minNodes2[0]
for i := 1; i < lenNodes2; i++ {
node := minNodes2[i]
earliestStartTimeOnNode := util.GetEarliestPodStartTime(nodesToVictims[node])
if earliestStartTimeOnNode == nil {
klog.Errorf("earliestStartTime is nil for node %s. Should not reach here.", node)
continue
}
if earliestStartTimeOnNode.After(latestStartTime.Time) {
latestStartTime = earliestStartTimeOnNode
nodeToReturn = node
}
}
return nodeToReturn
}
```
這個方法看起來很長,其實邏輯十分的清晰:
1. 找出最少的的PDB violations的node節點,如果找出的node集合大於1則往下走;
2. 找出找到node裡面pods 最高優先順序最小的node,如果還是找出的node集合大於1則往下走;
3. 找出node裡面Victims列表優先順序加和最小的,如果還是找出的node集合大於1則往下走;
4. 找到node列表中需要犧牲的pod數量最小的,如果還是找出的node集合大於1則往下走;
5. 若多個 node 的 pod 數量相等,則選出高優先順序 pod 啟動時間最短的,然後返回。
然後preempt方法往下走到呼叫PrepareCandidate方法:
**PrepareCandidate**
```go
func PrepareCandidate(c Candidate, fh framework.FrameworkHandle, cs kubernetes.Interface, pod *v1.Pod) error {
for _, victim := range c.Victims().Pods {
if err := util.DeletePod(cs, victim); err != nil {
klog.Errorf("Error preempting pod %v/%v: %v", victim.Namespace, victim.Name, err)
return err
}
if waitingPod := fh.GetWaitingPod(victim.UID); waitingPod != nil {
waitingPod.Reject("preempted")
}
fh.EventRecorder().Eventf(victim, pod, v1.EventTypeNormal, "Preempted", "Preempting", "Preempted by %v/%v on node %v",
pod.Namespace, pod.Name, c.Name())
}
metrics.PreemptionVictims.Observe(float64(len(c.Victims().Pods)))
//移除低優先順序 pod 的 Nominated,更新這些 pod,移動到 activeQ 佇列中,讓排程器為這些 pod 重新 bind node
nominatedPods := getLowerPriorityNominatedPods(fh.PreemptHandle(), pod, c.Name())
if err := util.ClearNominatedNodeName(cs, nominatedPods...); err != nil {
klog.Errorf("Cannot clear 'NominatedNodeName' field: %v", err)
// We do not return as this error is not critical.
}
return nil
}
```
這個方法會呼叫DeletePod刪除Victims列表裡面的pod,然後將這些pod中的Status.NominatedNodeName屬性置空。
到這裡整個搶佔過程就講解完畢了~
### 總結
看完這一篇我們對k8s的搶佔可以說有一個全域性的瞭解,心裡應該非常清楚k8s在搶佔的時候會發生什麼,例如什麼時候時候哪些pod會執行搶佔,以及為什麼執行搶佔,以及搶佔了哪些pod的資源,對於被搶佔的pod會不會重新被排程等等。
## Reference
https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/
https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/
https://kubernetes.io/docs/concepts/configuration/pod-overhead/
https://kubernetes.io/docs/concepts/workloads/pods/disruptions/
https://kubernetes.io/docs/tasks/run-application/configure-pdb/
https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-a