1. 為什么需要自定義調度邏輯
什么是所謂的調度?
-
所謂調度就是指給 Pod 對象的 spec.nodeName 賦值
-
待調度對象則是所有 spec.nodeName 為空的 Pod
-
調度過程則是從集群現有的 Node 中為當前 Pod 選擇一個最合適的
實際上 Pod 上還有一個平時比較少關注的屬性:?spec.schedulerName
,用于指定該 Pod 要交給哪個調度器進行調度。
那么問題來了,平時用的時候也沒給 spec.schedulerName 賦值過,怎么也能調度呢?
因為默認的 kube-scheduler 可以兼容 spec.schedulerName 為空或者為?default
?的 Pod。
為什么需要自定義調度邏輯
自定義調度邏輯可以解決特定應用場景和需求,使集群資源使用更高效,適應特殊的調度策略。
比如:
- 不同的工作負載可能有特定的資源需求,比如 GPU 或 NPU,需要確保 Pod 只能調度到滿足這些資源條件的節點上。
- 某些集群可能需要均衡資源消耗,避免將多個負載集中到某些節點上。
- 為了降低延遲,可能需要將Pod調度到特定地理位置的節點上。自定義調度器可以根據節點的地理位置標簽進行調度決策。
- 某些應用需要與其他應用隔離運行,以避免資源爭搶。通過自定義調度器,可以將特定類型的任務或工作負載隔離到專用的節點上。
- ...
總之就是業務上有各種特殊的調度需求,因此我們需要通過實現自定義調度器來滿足這些需求。
通過實現自定義調度器,可以根據具體的業務需求和集群環境,實現更靈活、更高效的資源管理和調度策略。
2.如何增加自定義調度邏輯
自定義調度器的幾種方法
要增加自定義調度邏輯也并不復雜,K8s 整個調度流程都已經插件化了,我們并不需要重頭開始實現一個調度器,而只需要實現一個調度插件,通過在調度過程中各個階段加入我們的自定義邏輯,來控制最終的調度結果。
總體來說可以分為以下幾個方向:
1)新增一個調度器
直接修改 kube-scheduler 源碼,編譯替換- 使用 調度框架(Scheduling Framework),我們可以使用?scheduler-plugins?作為模板,簡化自定義調度器的開發流程。
- Kubernetes v1.15 版本中引入了可插拔架構的調度框架,使得定制調度器這個任務變得更加的容易。調庫框架向現有的調度器中添加了一組插件化的 API,該 API 在保持調度程序“核心”簡單且易于維護的同時,使得大部分的調度功能以插件的形式存在。
2)擴展原有調度器
- 通過 Scheduler Extender 可以實現對已有調度器進行擴展。單獨創建一個 HTTP 服務并實現對應接口,后續就可以將該服務作為外置調度器使用。通過配置?
KubeSchedulerConfiguration
原 Scheduler 會以 HTTP 調用方式和外置調度器交互,實現在不改動原有調度器基礎上增加自定義邏輯。
3)其他非主流方案
- 自定義 Webhook 直接修改未調度 Pod 的 spec.nodeName 字段,有點離譜但理論可行哈哈
二者都有自己的優缺點
優點 | 缺點 | |
---|---|---|
新增調度器 | 性能好:由于不依賴外部插件或 HTTP 調用,調度流程的延遲相對較低,適合對性能要求較高的場景復用性高:可復用現有的調度插件,如?scheduler-plugins ,大大降低了開發難度,提升了開發效率。 | 多個調度器可能會沖突:比如多個調度器同時調度了一個 Pod 到節點上,先啟動的 Pod 把資源占用了,后續的 Pod 無法啟動。 |
擴展調度器 | 實現簡單:無需重新編譯調度器,通過配置?KubeSchedulerConfiguration ?創建一個外部 HTTP 服務來實現自定義邏輯。零侵入性:不需要修改或重構調度器的核心代碼,可快速上線新的調度邏輯。靈活性較高:原有調度器和自定義邏輯相對獨立,方便維護與測試。 | 性能差:調度請求需要經過 HTTP 調用,增加了調用延遲,對性能可能有影響。 |
一般在我們要改動的邏輯不多時,直接使用 Scheduler Extender 是比較簡單的。
Scheduler 配置
調度器的配置有一個單獨的對象:KubeSchedulerConfiguration,不過并沒有以 CRD 形式存在,而是存放到 Configmap 中的。
以下是一個完整 KubeSchedulerConfiguration 的 yaml:
apiVersion: v1
data:config.yaml: |apiVersion: kubescheduler.config.k8s.io/v1beta2kind: KubeSchedulerConfigurationleaderElection:leaderElect: falseprofiles:- schedulerName: hami-schedulerextenders:- urlPrefix: "https://127.0.0.1:443"filterVerb: filterbindVerb: bindnodeCacheCapable: trueweight: 1httpTimeout: 30senableHTTPS: truetlsConfig:insecure: truemanagedResources:- name: nvidia.com/gpuignoredByScheduler: true- name: nvidia.com/gpumemignoredByScheduler: true- name: nvidia.com/gpucoresignoredByScheduler: true
一般分為基礎配置和 extenders 配置兩部分。
基礎配置
基礎配置一般就是配置調度器的名稱,Demo 如下:
apiVersion: v1
kind: ConfigMap
metadata:name: my-scheduler-confignamespace: kube-system
data:my-scheduler-config.yaml: |apiVersion: kubescheduler.config.k8s.io/v1beta2kind: KubeSchedulerConfigurationprofiles:- schedulerName: my-schedulerleaderElection:leaderElect: false
通過 schedulerName 來指定該調度器的名稱,比如這里就是?my-scheduler
?。
創建 Pod 時除非手動指定 spec.schedulerName 為 my-scheduler,否則不會由該調度器進行調度。
擴展調度器:extenders 配置
extenders 部分通過額外指定一個 http 服務器來實現外置的自定義的調度邏輯。
一個簡單的 Scheduler Extender 配置如下:
apiVersion: v1
kind: ConfigMap
metadata:name: i-scheduler-extendernamespace: kube-system
data:i-scheduler-extender.yaml: |apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationprofiles:- schedulerName: i-scheduler-extenderleaderElection:leaderElect: falseextenders:- urlPrefix: "http://localhost:8080"enableHTTPS: falsefilterVerb: "filter"prioritizeVerb: "prioritize"bindVerb: "bind"weight: 1nodeCacheCapable: true
核心部分為
extenders:- urlPrefix: "http://localhost:8080"enableHTTPS: falsefilterVerb: "filter"prioritizeVerb: "prioritize"bindVerb: "bind"weight: 1nodeCacheCapable: true
幾個核心參數含義如下:
-
urlPrefix: http://127.0.0.1:8080
?用于指定外置的調度服務訪問地址 -
filterVerb: "filter"
:表示 Filter 接口在外置服務中的訪問地址為 filter,即完整地址為?http://127.0.0.1:8080/filter
-
prioritizeVerb: "prioritize"
:同上,Prioritize(Score) 接口的地址為 prioritize -
bindVerb: "bind"
:同上,Bind 接口的地址為 bind
這樣該調度器在執行 Filter 接口邏輯時,除了內置的調度器插件之外,還會通過 HTTP 方式調用外置的調度器。
這樣我們只需要創建一個 HTTP 服務,實現對應接口即可實現自定義的調度邏輯,而不需要重頭實現一個調度器。
ManagedResources 配置
在之前的配置中是所有 Pod 都會走 Extender 的調度邏輯,實際上 Extender 還有一個 ManagedResources 配置,用于限制只有申請使用指定資源的 Pod 才會走 Extender 調度邏輯,這樣可以減少無意義的調度。
一個帶 managedResources 的 KubeSchedulerConfiguration 內容如下
apiVersion: v1
data:config.yaml: |apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationleaderElection:leaderElect: falseprofiles:- schedulerName: hami-schedulerextenders:- urlPrefix: "https://127.0.0.1:443"filterVerb: filterbindVerb: bindnodeCacheCapable: falseenableHTTPS: falsemanagedResources:- name: nvidia.com/gpuignoredByScheduler: true- name: nvidia.com/gpumemignoredByScheduler: true- name: nvidia.com/gpucoresignoredByScheduler: true- name: nvidia.com/gpumem-percentageignoredByScheduler: true- name: nvidia.com/priorityignoredByScheduler: true
在原來的基礎上增加了 managedResources 部分的配置
managedResources:- name: nvidia.com/gpuignoredByScheduler: true- name: nvidia.com/gpumemignoredByScheduler: true- name: nvidia.com/gpucoresignoredByScheduler: true- name: nvidia.com/gpumem-percentageignoredByScheduler: true
只有 Pod 申請這些特殊資源時才走 Extender 調度邏輯,否則使用原生的 Scheduler 調度即可。
ignoredByScheduler: true
?的作用是告訴調度器忽略指定資源,避免將它們作為調度決策的依據。也就是說,雖然這些資源(如 GPU 或其他加速硬件)會被 Pod 請求,但調度器不會在選擇節點時基于這些資源的可用性做出決定。
ps:因為這些資源可能是虛擬的,并不會真正的出現在 Node 上,因此調度時需要忽略掉,否則就沒有任何節點滿足條件了,但是這些虛擬資源則是我們的自定義調度邏輯需要考慮的事情。
Scheduler 中的判斷邏輯如下,和之前說的一樣,只有當 Pod 申請了這些指定的資源時,Scheduler 才會調用 Extender。
// IsInterested returns true if at least one extended resource requested by
// this pod is managed by this extender.
func (h *HTTPExtender) IsInterested(pod *v1.Pod) bool {if h.managedResources.Len() == 0 {return true}if h.hasManagedResources(pod.Spec.Containers) {return true}if h.hasManagedResources(pod.Spec.InitContainers) {return true}return false
}func (h *HTTPExtender) hasManagedResources(containers []v1.Container) bool {for i := range containers {container := &containers[i]for resourceName := range container.Resources.Requests {if h.managedResources.Has(string(resourceName)) {return true}}for resourceName := range container.Resources.Limits {if h.managedResources.Has(string(resourceName)) {return true}}}return false
}
3. Scheduler Extender 規范
Scheduler Extender 通過 HTTP 請求的方式,將調度框架階段中的調度決策委托給外部的調度器,然后將調度結果返回給調度框架。
我們只需要實現一個 HTTP 服務,然后通過配置文件將其注冊到調度器中,就可以實現自定義調度器。
通過 Scheduler Extender 擴展原有調度器一般分為以下兩步:
- 1)創建一個 HTTP 服務,實現對應接口
- 2)修改調度器配置 KubeSchedulerConfiguration,增加 extenders 相關配置
外置調度器可以影響到三個階段:
-
Filter:調度框架將調用 Filter 函數,過濾掉不適合被調度的節點。
-
Priority:調度框架將調用 Priority 函數,為每個節點計算一個優先級,優先級越高,節點越適合被調度。
-
Bind:調度框架將調用 Bind 函數,將 Pod 綁定到一個節點上。
Filter、Priority、Bind 三個階段分別對應三個 HTTP 接口,三個接口都接收 POST 請求,各自的請求、響應結構定義在這里:#kubernetes/kube-scheduler/extender/v1/types.go
在這個 HTTP 服務中,我們可以實現上述階段中的任意一個或多個階段的接口,來定制我們的調度需求。
每個接口的請求和響應值請求如下。
Filter
請求參數
// ExtenderArgs represents the arguments needed by the extender to filter/prioritize
// nodes for a pod.
type ExtenderArgs struct {// Pod being scheduledPod *v1.Pod// List of candidate nodes where the pod can be scheduled; to be populated// only if Extender.NodeCacheCapable == falseNodes *v1.NodeList// List of candidate node names where the pod can be scheduled; to be// populated only if Extender.NodeCacheCapable == trueNodeNames *[]string
}
響應結果
// ExtenderFilterResult represents the results of a filter call to an extender
type ExtenderFilterResult struct {// Filtered set of nodes where the pod can be scheduled; to be populated// only if Extender.NodeCacheCapable == falseNodes *v1.NodeList// Filtered set of nodes where the pod can be scheduled; to be populated// only if Extender.NodeCacheCapable == trueNodeNames *[]string// Filtered out nodes where the pod can't be scheduled and the failure messagesFailedNodes FailedNodesMap// Filtered out nodes where the pod can't be scheduled and preemption would// not change anything. The value is the failure message same as FailedNodes.// Nodes specified here takes precedence over FailedNodes.FailedAndUnresolvableNodes FailedNodesMap// Error message indicating failureError string
}
Prioritize
請求參數
// ExtenderArgs represents the arguments needed by the extender to filter/prioritize
// nodes for a pod.
type ExtenderArgs struct {// Pod being scheduledPod *v1.Pod// List of candidate nodes where the pod can be scheduled; to be populated// only if Extender.NodeCacheCapable == falseNodes *v1.NodeList// List of candidate node names where the pod can be scheduled; to be// populated only if Extender.NodeCacheCapable == trueNodeNames *[]string
}
響應結果
// HostPriority represents the priority of scheduling to a particular host, higher priority is better.
type HostPriority struct {// Name of the hostHost string// Score associated with the hostScore int64
}// HostPriorityList declares a []HostPriority type.
type HostPriorityList []HostPriority
Bind
請求參數
// ExtenderBindingArgs represents the arguments to an extender for binding a pod to a node.
type ExtenderBindingArgs struct {// PodName is the name of the pod being boundPodName string// PodNamespace is the namespace of the pod being boundPodNamespace string// PodUID is the UID of the pod being boundPodUID types.UID// Node selected by the schedulerNode string
}
響應結果
// ExtenderBindingResult represents the result of binding of a pod to a node from an extender.
type ExtenderBindingResult struct {// Error message indicating failureError string
}
4. Demo
這部分則是手把手實現一個簡單的擴展調度器,完整代碼見:lixd/i-scheduler-extender
功能如下:
- 1)過濾階段:僅調度到帶有 Label?
priority.lixueduan.com
?的節點上 - 2)打分階段:直接將節點上 Label?
priority.lixueduan.com
?的值作為得分- 比如某節點攜帶 Label?
priority.lixueduan.com=50
?則打分階段該節點則是 50 分
- 比如某節點攜帶 Label?
代碼實現
main.go
比較簡單,直接通過內置的 net.http 包啟動一個 http 服務即可。
var h *server.Handlerfunc init() {h = server.NewHandler(extender.NewExtender())
}func main() {http.HandleFunc("/filter", h.Filter)http.HandleFunc("/filter_onlyone", h.FilterOnlyOne) // Filter 接口的一個額外實現http.HandleFunc("/priority", h.Prioritize)http.HandleFunc("/bind", h.Bind)http.ListenAndServe(":8080", nil)
}
由于 Priority 階段返回的得分 kube-Scheduler 會自行匯總后重新計算,因此擴展調度器的 priority 接口并不能安全控制最終調度結果,因此額外實現了一個 filter_onlyone 接口。
Filter 實現
filter 接口用于過濾掉不滿足條件的接口,這里直接過濾掉沒有指定 label 的節點即可。
// Filter 過濾掉不滿足條件的節點
func (ex *Extender) Filter(args extenderv1.ExtenderArgs) *extenderv1.ExtenderFilterResult {nodes := make([]v1.Node, 0)nodeNames := make([]string, 0)for _, node := range args.Nodes.Items {_, ok := node.Labels[Label]if !ok { // 排除掉不帶指定標簽的節點continue}nodes = append(nodes, node)nodeNames = append(nodeNames, node.Name)}// 沒有滿足條件的節點就報錯if len(nodes) == 0 {return &extenderv1.ExtenderFilterResult{Error: fmt.Errorf("all node do not have label %s", Label).Error()}}args.Nodes.Items = nodesreturn &extenderv1.ExtenderFilterResult{Nodes: args.Nodes, // 當 NodeCacheCapable 設置為 false 時會使用這個值NodeNames: &nodeNames, // 當 NodeCacheCapable 設置為 true 時會使用這個值}
}
具體返回 Nodes 還是 NodeNames 決定了后續 Scheduler 部署的 NodeCacheCapable 參數的配置,二者對于即可。
Prioritize 實現
Prioritize 對應的就是 Score 階段,給 Filter 之后留下來的節點打分,選擇一個最適合的節點進行調度。
這里的邏輯就是之前說的:解析 Node 上的 label ,直接將其 value 作為節點分數。
// Prioritize 給 Pod 打分
func (ex *Extender) Prioritize(args extenderv1.ExtenderArgs) *extenderv1.HostPriorityList {var result extenderv1.HostPriorityListfor _, node := range args.Nodes.Items {// 獲取 Node 上的 Label 作為分數priorityStr, ok := node.Labels[Label]if !ok {klog.Errorf("node %q does not have label %s", node.Name, Label)continue}priority, err := strconv.Atoi(priorityStr)if err != nil {klog.Errorf("node %q has priority %s are invalid", node.Name, priorityStr)continue}result = append(result, extenderv1.HostPriority{Host: node.Name,Score: int64(priority),})}return &result
}
Bind 實現
就是通過 clientset 創建一個 Binding 對象,指定 Pod 和 Node 信息。
// Bind 將 Pod 綁定到指定節點
func (ex *Extender) Bind(args extenderv1.ExtenderBindingArgs) *extenderv1.ExtenderBindingResult {log.Printf("bind pod: %s/%s to node:%s", args.PodNamespace, args.PodName, args.Node)// 創建綁定關系binding := &corev1.Binding{ObjectMeta: metav1.ObjectMeta{Name: args.PodName, Namespace: args.PodNamespace, UID: args.PodUID},Target: corev1.ObjectReference{Kind: "Node", APIVersion: "v1", Name: args.Node},}result := new(extenderv1.ExtenderBindingResult)err := ex.ClientSet.CoreV1().Pods(args.PodNamespace).Bind(context.Background(), binding, metav1.CreateOptions{})if err != nil {klog.ErrorS(err, "Failed to bind pod", "pod", args.PodName, "namespace", args.PodNamespace, "podUID", args.PodUID, "node", args.Node)result.Error = err.Error()}return result
}
Filter OnlyOne 實現
Extender 僅作為一個額外的調度插件接入, 接口返回得分最終 Scheduler 會將其和其他插件打分合并之后再選出最終節點,因此 extender 中無法通過 prioritie 接口的分數完全控制調度結果。
不過也不是沒有辦法!
想要完全控制調度結果,我們可以在 Filter 接口中特殊處理。
Filter 接口先過濾掉不滿足條件的節點,然后對剩余節點進行打分,最終只返回得分最高的那個節點,這樣就一定會調度到該接口,從而實現完全控制調度結果。
具體實現如下:
// FilterOnlyOne 過濾掉不滿足條件的節點,并將其余節點打分排序,最終只返回得分最高的節點以實現完全控制調度結果
func (ex *Extender) FilterOnlyOne(args extenderv1.ExtenderArgs) *extenderv1.ExtenderFilterResult {// 過濾掉不滿足條件的節點nodeScores := &NodeScoreList{NodeList: make([]*NodeScore, 0)}for _, node := range args.Nodes.Items {_, ok := node.Labels[Label]if !ok { // 排除掉不帶指定標簽的節點continue}// 對剩余節點打分score := ComputeScore(node)nodeScores.NodeList = append(nodeScores.NodeList, &NodeScore{Node: node, Score: score})}// 沒有滿足條件的節點就報錯if len(nodeScores.NodeList) == 0 {return &extenderv1.ExtenderFilterResult{Error: fmt.Errorf("all node do not have label %s", Label).Error()}}// 排序sort.Sort(nodeScores)// 然后取最后一個,即得分最高的節點,這樣由于 Filter 只返回了一個節點,因此最終肯定會調度到該節點上m := (*nodeScores).NodeList[len((*nodeScores).NodeList)-1]// 組裝一下返回結果args.Nodes.Items = []v1.Node{m.Node}return &extenderv1.ExtenderFilterResult{Nodes: args.Nodes,NodeNames: &[]string{m.Node.Name},}
}
部署
構建鏡像
Dockerfile 如下:
# syntax=docker/dockerfile:1# Build the manager binary
FROM golang:1.22.5 as builder
ARG TARGETOS
ARG TARGETARCHENV GOPROXY=https://goproxy.cnWORKDIR /workspace
# Copy the go source
COPY . /workspace
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o extender main.go# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
#FROM gcr.io/distroless/static:nonroot
FROM busybox:1.36
WORKDIR /
COPY --from=builder /workspace/extender .
USER 65532:65532ENTRYPOINT ["/extender"]
部署到集群
部分也是分為兩步:
-
1)部署 Extender:由于 Extender 只是一個 HTTP 服務器,只需要使用 Deployment 將其部署到集群即可。
-
2)配置 Extender:修改調度器的 KubeSchedulerConfiguration 配置,在其中 extender 部分指定 url 以及對應的 path,進行接入。
這里為了不影響到默認的 kube-scheduler,我們使用 kube-scheduler 鏡像單獨啟動一個 Scheduler,然后為該調度器配置上 Extender,同時為了降低網絡請求的影響,直接將 kube-scheduler 和 Extender 直接運行在同一個 Pod 里,通過 localhost 進行訪問。
完整 yaml 如下:
apiVersion: v1
kind: ServiceAccount
metadata:name: i-scheduler-extendernamespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:name: i-scheduler-extender
subjects:- kind: ServiceAccountname: i-scheduler-extendernamespace: kube-system
roleRef:kind: ClusterRolename: cluster-adminapiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ConfigMap
metadata:name: i-scheduler-extendernamespace: kube-system
data:i-scheduler-extender.yaml: |apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationprofiles:- schedulerName: i-scheduler-extenderleaderElection:leaderElect: falseextenders:- urlPrefix: "http://localhost:8080"enableHTTPS: falsefilterVerb: "filter"prioritizeVerb: "prioritize"bindVerb: "bind"weight: 1nodeCacheCapable: true
---
apiVersion: apps/v1
kind: Deployment
metadata:labels:component: i-scheduler-extendertier: control-planename: i-scheduler-extendernamespace: kube-system
spec:replicas: 1selector:matchLabels:component: i-scheduler-extendertier: control-planetemplate:metadata:labels:component: i-scheduler-extendertier: control-planespec:serviceAccountName: i-scheduler-extendercontainers:- name: kube-schedulerimage: registry.k8s.io/kube-scheduler:v1.29.0command:- kube-scheduler- --config=/etc/kubernetes/i-scheduler-extender.yamllivenessProbe:httpGet:path: /healthzport: 10259scheme: HTTPSinitialDelaySeconds: 15readinessProbe:httpGet:path: /healthzport: 10259scheme: HTTPSresources:requests:cpu: '0.1'volumeMounts:- name: config-volumemountPath: /etc/kubernetes- name: i-scheduler-extenderimage: lixd96/i-scheduler-extender:v1ports:- containerPort: 8080volumes:- name: config-volumeconfigMap:name: i-scheduler-extender
kubectl apply -f deploy
確認服務正常運行
[root@scheduler-1 ~]# kubectl -n kube-system get po|grep i-scheduler-extender
i-scheduler-extender-f9cff954c-dkwz2 2/2 Running 0 1m
接下來就可以開始測試了。
測試
創建 Pod
創建一個 Deployment 并指定使用上一步中部署的 Scheduler,然后測試會調度到哪個節點上。
apiVersion: apps/v1
kind: Deployment
metadata:name: test
spec:replicas: 1selector:matchLabels:app: testtemplate:metadata:labels:app: testspec:schedulerName: i-scheduler-extendercontainers:- image: busybox:1.36name: nginxcommand: ["sleep"] args: ["99999"]
創建之后 Pod 會一直處于 Pending 狀態
[root@scheduler-1 lixd]# k get po
NAME READY STATUS RESTARTS AGE
test-58794bff9f-ljxbs 0/1 Pending 0 17s
查看具體情況
[root@scheduler-1]# k describe po test-58794bff9f-ljxbs
Events:Type Reason Age From Message---- ------ ---- ---- -------Warning FailedScheduling 99s i-scheduler-extender all node do not have label priority.lixueduan.comWarning FailedScheduling 95s (x2 over 97s) i-scheduler-extender all node do not have label priority.lixueduan.com
可以看到,是因為 Node 上沒有我們定義的 Label,因此都不滿足條件,最終 Pod 就一直 Pending 了。
添加 Label
由于我們實現的 Filter 邏輯是需要 Node 上有priority.lixueduan.com
?才會用來調度,否則直接會忽略。
理論上,只要給任意一個 Node 打上 Label 就可以了。
[root@scheduler-1 install]# k get node
NAME STATUS ROLES AGE VERSION
scheduler-1 Ready control-plane 4h34m v1.27.4
scheduler-2 Ready <none> 4h33m v1.27.4
[root@scheduler-1 install]# k label node scheduler-1 priority.lixueduan.com=10
node/scheduler-1 labeled
再次查看 Pod 狀態
[root@scheduler-1 lixd]# k get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-58794bff9f-ljxbs 1/1 Running 0 104s 172.25.123.201 scheduler-1 <none> <none>
已經被調度到 node1 上了,查看詳細日志
[root@scheduler-1 install]# k describe po test-7f7bb8f449-w6wvv
Events:Type Reason Age From Message---- ------ ---- ---- -------Warning FailedScheduling 116s i-scheduler-extender 0/2 nodes are available: preemption: 0/2 nodes are available: 2 No preemption victims found for incoming pod.Warning FailedScheduling 112s (x2 over 115s) i-scheduler-extender 0/2 nodes are available: preemption: 0/2 nodes are available: 2 No preemption victims found for incoming pod.Normal Scheduled 26s i-scheduler-extender Successfully assigned default/test-58794bff9f-ljxbs to scheduler-1
可以看到,確實是 i-scheduler-extender 這個調度器在處理,調度到了 node1.
多節點排序
我們實現的 Score 是根據 Node 上的?priority.lixueduan.com
?對應的 Value 作為得分的,因此調度器會優先考慮調度到 Value 比較大的一個節點。
因為 Score 階段也有很多調度插件,Scheduler 會匯總所有得分,最終才會選出結果,因此這里的分數也是僅供參考,不能完全控制調度結果。
給 node2 也打上 label,value 設置為 20
[root@scheduler-1 install]# k get node
NAME STATUS ROLES AGE VERSION
scheduler-1 Ready control-plane 4h34m v1.27.4
scheduler-2 Ready <none> 4h33m v1.27.4
[root@scheduler-1 install]# k label node scheduler-2 priority.lixueduan.com=20
node/scheduler-2 labeled
然后更新 Deployment ,觸發創建新 Pod ,測試調度邏輯。
[root@scheduler-1 lixd]# kubectl rollout restart deploy test
deployment.apps/test restarted
因為 Node2 上的 priority 為 20,node1 上為 10,那么理論上會調度到 node2 上。
[root@scheduler-1 lixd]# k get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-84fdbbd8c7-47mtr 1/1 Running 0 38s 172.25.0.162 scheduler-1 <none> <none>
結果還是調度到了 node1,為什么呢?
這就是前面提到的:因為 Extender 僅作為一個額外的調度插件接入,Prioritize 接口返回得分最終 Scheduler 會將其和其他插件打分合并之后再選出最終節點,因此 Extender 想要完全控制調度結果,只能在 Filter 接口中實現,過濾掉不滿足條件的節點,并對剩余節點進行打分,最終 Filter 接口只返回得分最高的那個節點,從而實現完全控制調度結果。
ps:即之前的 Filter OnlyOne 實現,可以在 KubeSchedulerConfiguration 中配置不同的 path 來調用不同接口進行測試。
修改 KubeSchedulerConfiguration 配置,
apiVersion: v1
kind: ConfigMap
metadata:name: i-scheduler-extendernamespace: kube-system
data:i-scheduler-extender.yaml: |apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationprofiles:- schedulerName: i-scheduler-extenderleaderElection:leaderElect: falseextenders:- urlPrefix: "http://localhost:8080"enableHTTPS: falsefilterVerb: "filter_onlyone"prioritizeVerb: "prioritize"bindVerb: "bind"weight: 1nodeCacheCapable: true
修改點:
filterVerb: "filter_onlyone"
Path 從 filter 修改成了 filter_onlyone,這里的 path 和前面注冊服務時的路徑對應:
http.HandleFunc("/filter", h.Filter)http.HandleFunc("/filter_onlyone", h.FilterOnlyOne) // Filter 接口的一個額外實現
修改后重啟一下 Scheduler
kubectl -n kube-system rollout restart deploy i-scheduler-extender
再次更新 Deployment 觸發調度
[root@scheduler-1 install]# k rollout restart deploy test
deployment.apps/test restarted
這樣應該是調度到 node2 了,確認一下
[root@scheduler-1 lixd]# k get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-849f549d5b-pbrml 1/1 Running 0 12s 172.25.0.166 scheduler-2 <none> <none>
現在我們更新 Node1 的 label,改成 30
k label node scheduler-1 priority.lixueduan.com=30 --overwrite
再次更新 Deployment 觸發調度
[root@scheduler-1 install]# k rollout restart deploy test
deployment.apps/test restarted
這樣應該是調度到 node1 了,確認一下
[root@scheduler-1 lixd]# k get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-69d9ccb877-9fb6t 1/1 Running 0 5s 172.25.123.203 scheduler-1 <none> <none>
說明修改 Filter 方法實現之后,確實可以直接控制調度結果。