├── .gitignore ├── README.md ├── apiserver └── admission-controller.md ├── controller └── taint-manager.md ├── kubectl └── builder-visitor-pattern.md ├── scheduler ├── Informer-mechanism.md ├── cache.md ├── event.md ├── framework.md ├── initialization.md ├── plugin.md ├── priority-preemption.md ├── queue.md └── start-scheduler.md └── snapshots ├── scheduler-architecture.png ├── scheduling-framework-extensions.png └── wechat.jpeg /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubernetes-design 2 | Kubernetes 源码学习笔记📰。理解仅限于当时的认知,如有错误,欢迎指正📌。(持续更新🌱) 3 | 4 | 最新更新:2022-04-04 [节点生命周期管理之 TaintManager](https://github.com/kerthcet/kubernetes-design/blob/main/controller/taint-manager.md) 5 | 6 | 7 | 8 | ## 索引: 9 | 10 | ### scheduler 11 | * [Kube-Scheduler 初始化](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/initialization.md) 12 | * [Kube-Scheduler 启动](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/start-scheduler.md) 13 | * [Kube-Scheduler 调度队列](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/queue.md) 14 | * [Kube-Scheduler 优先级与抢占](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/priority-preemption.md) 15 | * [Kube-Scheduler Framework调度框架](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/framework.md) 16 | * [Kube-Scheduler Cache机制](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/cache.md) 17 | * [Kube-Scheduler Event机制](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/event.md) 18 | * Kube-Scheduler 插件机制 19 | * Kube-Scheduler 如何手写一个插件 20 | * Kube-Scheduler 多版本控制如何实现 21 | * Kube-Scheduler Extender 22 | * Kube-Scheduler Informer机制 23 | * Kube-Scheduler Event处理机制 24 | * Kube-Scheduler Metrics机制 25 | * Kube-Scheduler 如何解决调度不均问题? 26 | * Kube-Scheduler Descheduler 机制 27 | * Kube-Scheduler PodNominator 机制 28 | * Kube-Scheduler 高可用设计 29 | 30 | ### controller 31 | * [节点生命周期管理之 TaintManager](https://github.com/kerthcet/kubernetes-design/blob/main/controller/taint-manager.md) 32 | 33 | ### kubectl 34 | * [Kubectl Builder & Visitor 设计模式解析](https://github.com/kerthcet/kubernetes-design/blob/main/kubectl/builder-visitor-pattern.md) 35 | 36 | ### apiserver 37 | * AdmissionController 源码解析 -------------------------------------------------------------------------------- /apiserver/admission-controller.md: -------------------------------------------------------------------------------- 1 | ## AdmissionController 源码解析 (InProgress) 2 | 3 | Kubernetes Version: v1.22@9a1d90165d6466 4 | Date: 2021.11.20 5 | 6 | ## 1. 开篇 7 | `Admission Controller`(下面简称 `AC`) 是一段代码,它会在请求通过认证和授权之后、对象被持久化之前拦截到达 API 服务器的请求。准入控制器可以执行 “验证(Validating)” 和/或 “变更(Mutating)” 操作。 变更(mutating)控制器可以修改被其接受的对象。今天我们就从源码入手了解它是如何工作的。 8 | 9 | ## 2. 启动 10 | `AC` 是随着 `API Server` 一起启动的,入口位于 `cmd/kube-apiserver/app/server.go`,调用链如下: 11 | 12 | NewAPIServerCommand() -> 13 | options.NewServerRunOptions() -> 14 | kubeoptions.NewAdmissionOptions() 15 | 16 | 我们一起看一下 `NewAdmissionOptions()` 方法: 17 | 18 | func NewAdmissionOptions() *AdmissionOptions { 19 | // 生成配置信息 20 | options := genericoptions.NewAdmissionOptions() 21 | 22 | // 注册所有的 plugin 23 | RegisterAllAdmissionPlugins(options.Plugins) 24 | 25 | // 对 plugins 排序 26 | options.RecommendedPluginOrder = AllOrderedPlugins 27 | 28 | // 设置默认关闭 plugin 列表 29 | options.DefaultOffPlugins = DefaultOffAdmissionPlugins() 30 | 31 | return &AdmissionOptions{ 32 | GenericAdmission: options, 33 | } 34 | } 35 | 36 | ## 3. 运行 37 | 运行的入口命令同样位于 `cmd/kube-apiserver/app/server.go` 中,调用链如下: 38 | 39 | NewAPIServerCommand() -> completedOptions.Validate() 40 | 41 | 我们一起看一下 `Validata()` 方法: 42 | 43 | func (s *ServerRunOptions) Validate() []error { 44 | var errs []error 45 | if s.MasterCount <= 0 { 46 | errs = append(errs, fmt.Errorf("--apiserver-count should be a positive number, but value '%d' provided", s.MasterCount)) 47 | } 48 | errs = append(errs, s.Etcd.Validate()...) 49 | errs = append(errs, validateClusterIPFlags(s)...) 50 | errs = append(errs, validateServiceNodePort(s)...) 51 | errs = append(errs, validateAPIPriorityAndFairness(s)...) 52 | errs = append(errs, s.SecureServing.Validate()...) 53 | errs = append(errs, s.Authentication.Validate()...) 54 | errs = append(errs, s.Authorization.Validate()...) 55 | errs = append(errs, s.Audit.Validate()...) 56 | errs = append(errs, s.Admission.Validate()...) 57 | errs = append(errs, s.APIEnablement.Validate(legacyscheme.Scheme, apiextensionsapiserver.Scheme, aggregatorscheme.Scheme)...) 58 | errs = append(errs, validateTokenRequest(s)...) 59 | errs = append(errs, s.Metrics.Validate()...) 60 | errs = append(errs, validateAPIServerIdentity(s)...) 61 | 62 | return errs 63 | } 64 | 65 | 这里包含了所有的参数验证相关逻辑,比如 `AuthZ`,`AuthN`,`Admission`,以 `Admission` 为例,我们一起看一下 `s.Admission.Validate()` 方法: 66 | 67 | func (a *AdmissionOptions) Validate() []error { 68 | 69 | var errs []error 70 | 71 | // admission-control 已经 deprecated,plugin 只能在注册到一个地方,他们是互斥的 72 | if a.PluginNames != nil && 73 | (a.GenericAdmission.EnablePlugins != nil || a.GenericAdmission.DisablePlugins != nil) { 74 | errs = append(errs, fmt.Errorf("admission-control and enable-admission-plugins/disable-admission-plugins flags are mutually exclusive")) 75 | } 76 | 77 | // registeredPlugins 是所有注册的 plugin,这里进行存在性判断 78 | registeredPlugins := sets.NewString(a.GenericAdmission.Plugins.Registered()...) 79 | for _, name := range a.PluginNames { 80 | if !registeredPlugins.Has(name) { 81 | errs = append(errs, fmt.Errorf("admission-control plugin %q is unknown", name)) 82 | } 83 | } 84 | 85 | // 对所有的 Plugin 是否存在进行验证,比较简单,就不展开了 86 | errs = append(errs, a.GenericAdmission.Validate()...) 87 | 88 | return errs 89 | } 90 | -------------------------------------------------------------------------------- /controller/taint-manager.md: -------------------------------------------------------------------------------- 1 | # 节点生命周期管理之 TaintManager 2 | 3 | Kubernetes Version: v1.23@8e0ac5b6 4 | Date: 2022.04.03 5 | 6 | ## 1. 开篇 7 | Kubernetes 污点机制大家应该都是非常熟悉的了,其中污点有3种功效,分别是 `NoSchedule`,`NoExecute`,`PreferNoSchedule`,用于禁止调度,禁止运行和优先禁止调度等场景。今天我们讲一讲 `NoExecute` 是如何工作的。 8 | 9 | ## 2. 整体设计 10 | 负责管理 `NoExecute` 污点的是一个叫 `NoExecuteTaintManager` 的管理器,它属于 `NodeLifecycleController` 中的一个组件,随着 `NodeLifecycleController` 运行而运行。运行时,监听 node 和 pod 事件更新,触发对应的驱逐逻辑。 11 | 12 | ## 3. NewNoExecuteTaintManager 组件 13 | `NoExecuteTaintManager` 数据结构如下: 14 | ```golang 15 | type NoExecuteTaintManager struct { 16 | client clientset.Interface 17 | // 负责记录事件 18 | recorder record.EventRecorder 19 | // 监听 pod 20 | getPod GetPodFunc 21 | // 监听 node 22 | getNode GetNodeFunc 23 | // 获取 node 所有的 pods 24 | getPodsAssignedToNode GetPodsByNodeNameFunc 25 | 26 | // 驱逐队列 27 | taintEvictionQueue *TimedWorkerQueue 28 | taintedNodesLock sync.Mutex 29 | taintedNodes map[string][]v1.Taint 30 | 31 | // node 更新 channel 32 | nodeUpdateChannels []chan nodeUpdateItem 33 | // pod 更新 channel 34 | podUpdateChannels []chan podUpdateItem 35 | 36 | // node 更新队列 37 | nodeUpdateQueue workqueue.Interface 38 | // pod 更新队列 39 | podUpdateQueue workqueue.Interface 40 | } 41 | ``` 42 | 43 | 实例初始化位于 `pkg/controller/nodelifecycle/scheduler/taint_manager.go:L158`,在 `NodeLifecycleController` 初始化的时候调用: 44 | 45 | ```golang 46 | func NewNoExecuteTaintManager(ctx context.Context, c clientset.Interface, getPod GetPodFunc, getNode GetNodeFunc, getPodsAssignedToNode GetPodsByNodeNameFunc) *NoExecuteTaintManager { 47 | // 初始化 event 相关组件 48 | eventBroadcaster := record.NewBroadcaster() 49 | recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "taint-controller"}) 50 | eventBroadcaster.StartStructuredLogging(0) 51 | if c != nil { 52 | klog.V(0).InfoS("Sending events to api server") 53 | eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: c.CoreV1().Events("")}) 54 | } else { 55 | klog.Fatalf("kubeClient is nil when starting NodeController") 56 | } 57 | 58 | // 构造 NoExecuteTaintManager 59 | tm := &NoExecuteTaintManager{ 60 | client: c, 61 | recorder: recorder, 62 | getPod: getPod, 63 | getNode: getNode, 64 | getPodsAssignedToNode: getPodsAssignedToNode, 65 | taintedNodes: make(map[string][]v1.Taint), 66 | 67 | // 初始化 node 队列 68 | nodeUpdateQueue: workqueue.NewNamed("noexec_taint_node"), 69 | // 初始化 pod 队列 70 | podUpdateQueue: workqueue.NewNamed("noexec_taint_pod"), 71 | } 72 | // 构造驱逐队列 73 | tm.taintEvictionQueue = CreateWorkerQueue(deletePodHandler(c, tm.emitPodDeletionEvent)) 74 | 75 | return tm 76 | } 77 | ``` 78 | 79 | 整个 New 方法中最关键的是 `CreateWorkerQueue(deletePodHandler(c, tm.emitPodDeletionEvent))` 这个方法,我们展开看一下: 80 | 81 | `CreateWorkerQueue` 方法其实就是构造了一个 `TimedWorkerQueue` 队列: 82 | ```golang 83 | func CreateWorkerQueue(f func(ctx context.Context, args *WorkArgs) error) *TimedWorkerQueue { 84 | return &TimedWorkerQueue{ 85 | workers: make(map[string]*TimedWorker), 86 | workFunc: f, 87 | clock: clock.RealClock{}, 88 | } 89 | } 90 | ``` 91 | 92 | 其中,`clock.RealClock` 是一个实现了 `WithTicker` 接口的数据结构,可以实现定时执行的功能,这样就可以实现定时驱逐 pod。 93 | 94 | 另外我们观察到 `CreateWorkerQueue` 的参数是一个闭包,我们先看 `deletePodHandler` 方法: 95 | ```golang 96 | func deletePodHandler(c clientset.Interface, emitEventFunc func(types.NamespacedName)) func(ctx context.Context, args *WorkArgs) error { 97 | // 返回实际的方法 98 | return func(ctx context.Context, args *WorkArgs) error { 99 | ns := args.NamespacedName.Namespace 100 | name := args.NamespacedName.Name 101 | // 这里的 NamespaceName.String() 返回 / 这种格式的字符串 102 | klog.V(0).InfoS("NoExecuteTaintManager is deleting pod", "pod", args.NamespacedName.String()) 103 | if emitEventFunc != nil { 104 | emitEventFunc(args.NamespacedName) 105 | } 106 | var err error 107 | // 定义了重拾机制(如果可以指数退避就更好了,后面考虑提交个PR) 108 | for i := 0; i < retries; i++ { 109 | err = c.CoreV1().Pods(ns).Delete(ctx, name, metav1.DeleteOptions{}) 110 | if err == nil { 111 | break 112 | } 113 | time.Sleep(10 * time.Millisecond) 114 | } 115 | return err 116 | } 117 | } 118 | ``` 119 | 120 | 该方法主要是负责执行删除 pod 的操作。再看 `emitPodDeletionEvent` 方法,主要负责输出事件: 121 | ```golang 122 | func (tc *NoExecuteTaintManager) emitPodDeletionEvent(nsName types.NamespacedName) { 123 | if tc.recorder == nil { 124 | return 125 | } 126 | ref := &v1.ObjectReference{ 127 | Kind: "Pod", 128 | Name: nsName.Name, 129 | Namespace: nsName.Namespace, 130 | } 131 | tc.recorder.Eventf(ref, v1.EventTypeNormal, "TaintManagerEviction", "Marking for deletion Pod %s", nsName.String()) 132 | } 133 | ``` 134 | 这样,`NewNoExecuteTaintManager` 就结束了。 135 | 136 | ## 4. Run 方法 137 | 看完了初始化方法,我们看一下 `NoExecuteTaintManager` 是如何执行的,代码位于同一个文件夹下: 138 | ```golang 139 | func (tc *NoExecuteTaintManager) Run(ctx context.Context) { 140 | klog.V(0).InfoS("Starting NoExecuteTaintManager") 141 | 142 | // UpdateWorkerSize 表示 worker 数量,目前是8个 143 | for i := 0; i < UpdateWorkerSize; i++ { 144 | tc.nodeUpdateChannels = append(tc.nodeUpdateChannels, make(chan nodeUpdateItem, NodeUpdateChannelSize)) 145 | tc.podUpdateChannels = append(tc.podUpdateChannels, make(chan podUpdateItem, podUpdateChannelSize)) 146 | } 147 | go func(stopCh <-chan struct{}) { 148 | for { 149 | // 监听 nodeUpdateQueue,如果有新的元素加入队列,则从中获取该元素,这是一个 block 队列 150 | item, shutdown := tc.nodeUpdateQueue.Get() 151 | // 如果队列关闭,则退出该 goroutine 152 | if shutdown { 153 | break 154 | } 155 | nodeUpdate := item.(nodeUpdateItem) 156 | // 计算队列的 index 157 | hash := hash(nodeUpdate.nodeName, UpdateWorkerSize) 158 | select { 159 | // 监听 stop 信号 160 | case <-stopCh: 161 | // Done 方法表示 item 处理完成 162 | tc.nodeUpdateQueue.Done(item) 163 | return 164 | // 将 nodeUpdate 放到 channel中 165 | case tc.nodeUpdateChannels[hash] <- nodeUpdate: 166 | // tc.nodeUpdateQueue.Done is called by the nodeUpdateChannels worker 167 | } 168 | } 169 | }(ctx.Done()) 170 | 171 | // 原理同 nodeUpdate 一模一样,只不过这里监听的是 podUpdate 172 | go func(stopCh <-chan struct{}) { 173 | for { 174 | item, shutdown := tc.podUpdateQueue.Get() 175 | if shutdown { 176 | break 177 | } 178 | podUpdate := item.(podUpdateItem) 179 | hash := hash(podUpdate.nodeName, UpdateWorkerSize) 180 | select { 181 | case <-stopCh: 182 | tc.podUpdateQueue.Done(item) 183 | return 184 | case tc.podUpdateChannels[hash] <- podUpdate: 185 | // tc.podUpdateQueue.Done is called by the podUpdateChannels worker 186 | } 187 | } 188 | }(ctx.Done()) 189 | 190 | wg := sync.WaitGroup{} 191 | wg.Add(UpdateWorkerSize) 192 | // 前面我们监听了 nodeUpdate 和 podUpdate 事件,放到了对应的 channel 中,后续的处理则会由 worker() 负责。这里我们同样声明了 UpdateWorkerSize 个 worker 193 | for i := 0; i < UpdateWorkerSize; i++ { 194 | go tc.worker(ctx, i, wg.Done, ctx.Done()) 195 | } 196 | wg.Wait() 197 | } 198 | ``` 199 | 200 | 我们再看一下 worker 方法: 201 | ```golang 202 | func (tc *NoExecuteTaintManager) worker(ctx context.Context, worker int, done func(), stopCh <-chan struct{}) { 203 | defer done() 204 | 205 | for { 206 | select { 207 | case <-stopCh: 208 | return 209 | // 监听 nodeUpdateChannels,并进行处理 210 | case nodeUpdate := <-tc.nodeUpdateChannels[worker]: 211 | tc.handleNodeUpdate(ctx, nodeUpdate) 212 | tc.nodeUpdateQueue.Done(nodeUpdate) 213 | case podUpdate := <-tc.podUpdateChannels[worker]: 214 | // If we found a Pod update we need to empty Node queue first. 215 | priority: 216 | for { 217 | select { 218 | // 优先处理 nodeUpdateChannel,一个是因为 node 优先级更高,另外,nodeUpdate 中也会进行 pod 的处理 219 | case nodeUpdate := <-tc.nodeUpdateChannels[worker]: 220 | tc.handleNodeUpdate(ctx, nodeUpdate) 221 | tc.nodeUpdateQueue.Done(nodeUpdate) 222 | default: 223 | break priority 224 | } 225 | } 226 | // nodeUpdateChannels 被清空后开始处理 podUpdate 227 | tc.handlePodUpdate(ctx, podUpdate) 228 | tc.podUpdateQueue.Done(podUpdate) 229 | } 230 | } 231 | } 232 | ``` 233 | 234 | ## 5. 驱逐逻辑 235 | 通过上面的 Run 方法和 worker() 方法的解读,我们知道了大体的处理逻辑,下面我们看一下具体的驱逐逻辑,代码实现位于上文提到的 `handleNodeUpdate` 和 `handlePodUpdate` 方法中。我们先看 `handleNodeUpdate` 方法: 236 | ```golang 237 | func (tc *NoExecuteTaintManager) handleNodeUpdate(ctx context.Context, nodeUpdate nodeUpdateItem) { 238 | // 获取 node,其实就是调用的 NoExecuteTaintManager 的 getNode 方法 239 | node, err := tc.getNode(nodeUpdate.nodeName) 240 | if err != nil { 241 | if apierrors.IsNotFound(err) { 242 | // 如果没有找到该 node,需要从 taintedNodes 中删除 243 | tc.taintedNodesLock.Lock() 244 | defer tc.taintedNodesLock.Unlock() 245 | delete(tc.taintedNodes, nodeUpdate.nodeName) 246 | return 247 | } 248 | // 错误处理 249 | utilruntime.HandleError(fmt.Errorf("cannot get node %s: %v", nodeUpdate.nodeName, err)) 250 | return 251 | } 252 | 253 | // Create or Update 254 | klog.V(4).InfoS("Noticed node update", "node", nodeUpdate) 255 | // 获取 node 所有的污点 256 | taints := getNoExecuteTaints(node.Spec.Taints) 257 | func() { 258 | tc.taintedNodesLock.Lock() 259 | defer tc.taintedNodesLock.Unlock() 260 | klog.V(4).InfoS("Updating known taints on node", "node", node.Name, "taints", taints) 261 | if len(taints) == 0 { 262 | // 如果没有污点,则从 taintNodes 中删除 node 263 | delete(tc.taintedNodes, node.Name) 264 | } else { 265 | // 否则,就更新 node 污点信息 266 | tc.taintedNodes[node.Name] = taints 267 | } 268 | }() 269 | 270 | // 获取 node 所有的 pod 信息 271 | pods, err := tc.getPodsAssignedToNode(node.Name) 272 | if err != nil { 273 | klog.ErrorS(err, "Failed to get pods assigned to node", "node", node.Name) 274 | return 275 | } 276 | if len(pods) == 0 { 277 | return 278 | } 279 | 280 | // 如果节点没有 taints,则取消 node 上 pod 的驱逐操作 281 | if len(taints) == 0 { 282 | klog.V(4).InfoS("All taints were removed from the node. Cancelling all evictions...", "node", node.Name) 283 | for i := range pods { 284 | tc.cancelWorkWithEvent(types.NamespacedName{Namespace: pods[i].Namespace, Name: pods[i].Name}) 285 | } 286 | return 287 | } 288 | 289 | now := time.Now() 290 | // 否则,对每一个 pod 执行 processPodOnNode 逻辑 291 | for _, pod := range pods { 292 | podNamespacedName := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name} 293 | tc.processPodOnNode(ctx, podNamespacedName, node.Name, pod.Spec.Tolerations, taints, now) 294 | } 295 | } 296 | ``` 297 | 我们再看下 `processPodOnNode` 方法: 298 | ```golang 299 | func (tc *NoExecuteTaintManager) processPodOnNode( 300 | ctx context.Context, 301 | podNamespacedName types.NamespacedName, 302 | nodeName string, 303 | tolerations []v1.Toleration, 304 | taints []v1.Taint, 305 | now time.Time, 306 | ) { 307 | // 又做了一次判断,如果污点为空,则执行取消 work 308 | if len(taints) == 0 { 309 | tc.cancelWorkWithEvent(podNamespacedName) 310 | } 311 | 312 | // 返回 pod 的 tolerations 313 | allTolerated, usedTolerations := v1helper.GetMatchingTolerations(taints, tolerations) 314 | // 如果 pod 没有完全容忍污点,则需要将 pod 立马进行驱逐 315 | if !allTolerated { 316 | klog.V(2).InfoS("Not all taints are tolerated after update for pod on node", "pod", podNamespacedName.String(), "node", klog.KRef("", nodeName)) 317 | // 先从等待队列中驱逐,再立马执行 318 | tc.cancelWorkWithEvent(podNamespacedName) 319 | // 传入两个 time.Now 会立即执行驱逐逻辑,后面讲 AddWork 逻辑时会提到 320 | tc.taintEvictionQueue.AddWork(ctx, NewWorkArgs(podNamespacedName.Name, podNamespacedName.Namespace), time.Now(), time.Now()) 321 | return 322 | } 323 | 324 | // 获取所有 tolerations 中最短的时间,驱逐时间以最短的时间为准 325 | minTolerationTime := getMinTolerationTime(usedTolerations) 326 | // 如果是负数,表示永远容忍,则不驱逐 327 | if minTolerationTime < 0 { 328 | klog.V(4).InfoS("Current tolerations for pod tolerate forever, cancelling any scheduled deletion", "pod", podNamespacedName.String()) 329 | tc.cancelWorkWithEvent(podNamespacedName) 330 | return 331 | } 332 | 333 | startTime := now 334 | triggerTime := startTime.Add(minTolerationTime) 335 | // 这里对已经在驱逐队列中的元素进行了二次判断,表示是否需要延长或者缩短驱逐时间,但是存在 bug,具体可以看我提交的 PR: https://github.com/kubernetes/kubernetes/pull/109226,我就不再讲述逻辑 336 | scheduledEviction := tc.taintEvictionQueue.GetWorkerUnsafe(podNamespacedName.String()) 337 | if scheduledEviction != nil { 338 | startTime = scheduledEviction.CreatedAt 339 | if startTime.Add(minTolerationTime).Before(triggerTime) { 340 | return 341 | } 342 | tc.cancelWorkWithEvent(podNamespacedName) 343 | } 344 | // 最后,将需要驱逐的 pod 加入队列中,等待驱逐 345 | tc.taintEvictionQueue.AddWork(ctx, NewWorkArgs(podNamespacedName.Name, podNamespacedName.Namespace), startTime, triggerTime) 346 | } 347 | ``` 348 | 队列逻辑差不多清楚了,但是加入到队列中后怎么样执行好像没有讲到,以及 `CancelWork` 和 `AddWork` 具体做什么的也没说。所以下面我们讲一下到底驱逐逻辑是什么时候执行已经怎么执行的。 349 | 350 | 说到如何执行驱逐,我们需要先看下这个队列到底是一个什么队列,数据结构如下: 351 | ```golang 352 | type TimedWorkerQueue struct { 353 | // 锁 354 | sync.Mutex 355 | // 存放 worker 的字典 356 | workers map[string]*TimedWorker 357 | // work 执行方法 358 | workFunc func(ctx context.Context, args *WorkArgs) error 359 | clock clock.WithDelayedExecution 360 | } 361 | 362 | type TimedWorker struct { 363 | WorkItem *WorkArgs 364 | CreatedAt time.Time 365 | FireAt time.Time 366 | Timer clock.Timer 367 | } 368 | ``` 369 | 370 | 我们看到 worker 是带有计时功能的,所以我们大胆猜测是通过计时器触发函数执行。我们继续看 `AddWork` 方法: 371 | ```golang 372 | func (q *TimedWorkerQueue) AddWork(ctx context.Context, args *WorkArgs, createdAt time.Time, fireAt time.Time) { 373 | // 获取 key,各式就是 / 374 | key := args.KeyFromWorkArgs() 375 | klog.V(4).Infof("Adding TimedWorkerQueue item %v at %v to be fired at %v", key, createdAt, fireAt) 376 | 377 | q.Lock() 378 | defer q.Unlock() 379 | // 如果 workers 中存在,则跳过,避免重复执行 380 | if _, exists := q.workers[key]; exists { 381 | klog.Warningf("Trying to add already existing work for %+v. Skipping.", args) 382 | return 383 | } 384 | // 创建 worker,并保存到 workers 中,这个 getWrappedWorkerFuc方法很有意思,后面我们在看,大体作用就是执行 TimedWorkerQueue 的 workFunc 385 | worker := createWorker(ctx, args, createdAt, fireAt, q.getWrappedWorkerFunc(key), q.clock) 386 | q.workers[key] = worker 387 | } 388 | ``` 389 | 接着看 createWorker: 390 | ```golang 391 | func createWorker(ctx context.Context, args *WorkArgs, createdAt time.Time, fireAt time.Time, f func(ctx context.Context, args *WorkArgs) error, clock clock.WithDelayedExecution) *TimedWorker { 392 | // 我们前面在 processPodOnNode 方法中执行 createWorker 方法时,传入了两个 time.Now,目的就是触发 delay <=0, 立马执行函数 393 | delay := fireAt.Sub(createdAt) 394 | if delay <= 0 { 395 | go f(ctx, args) 396 | return nil 397 | } 398 | // AfterFunc 就是执行驱逐的关键方法,它是带有延迟执行功能的方法,delay 时间到了就会执行 399 | timer := clock.AfterFunc(delay, func() { f(ctx, args) }) 400 | return &TimedWorker{ 401 | WorkItem: args, 402 | CreatedAt: createdAt, 403 | FireAt: fireAt, 404 | Timer: timer, 405 | } 406 | } 407 | ``` 408 | 所以逻辑就很清楚了,我们通过 AddWork 把带有计时器的 worker 加入队列中,一旦计时器到期,延迟函数就会立马执行,进行驱逐,驱逐逻辑就封装在 `NewNoExecuteTaintManager` 的 `deletePodHandler` 方法中。 409 | 410 | 我们前面提到 `getWrappedWorkerFunc` 函数,我们看一下这个函数做了什么: 411 | ```golang 412 | func (q *TimedWorkerQueue) getWrappedWorkerFunc(key string) func(ctx context.Context, args *WorkArgs) error { 413 | return func(ctx context.Context, args *WorkArgs) error { 414 | // 实际执行 workFunc 415 | err := q.workFunc(ctx, args) 416 | q.Lock() 417 | defer q.Unlock() 418 | if err == nil { 419 | // 这里没有从 workers 中删除 key,而是赋值为 nil,主要原因是为了避免重复提交队列, 420 | // 我们在 AddWork 中会根据 key 校验是否是重复提交。但是有一个问题是 worker 中的 key 什么时候删除呢? 421 | // NoExecuteTaintManager 同样利用了 listWatch 机制,删除 pod 同样会推送事件过来, 422 | // 这个时候我们会删除对应的 key。 423 | q.workers[key] = nil 424 | } else { 425 | // 如果出错,则执行 delete 操作,可以再次提交事件 426 | delete(q.workers, key) 427 | } 428 | return err 429 | } 430 | } 431 | ``` 432 | 433 | CancelWork 逻辑更简单,就是从队列中删除 worker: 434 | ```golang 435 | func (q *TimedWorkerQueue) CancelWork(key string) bool { 436 | q.Lock() 437 | defer q.Unlock() 438 | worker, found := q.workers[key] 439 | result := false 440 | // 如果找到 worker,一是取消计时,二是直接从worker中删除 441 | if found { 442 | klog.V(4).Infof("Cancelling TimedWorkerQueue item %v at %v", key, time.Now()) 443 | if worker != nil { 444 | result = true 445 | worker.Cancel() 446 | } 447 | delete(q.workers, key) 448 | } 449 | return result 450 | } 451 | ``` 452 | 453 | ## 6. 总结 454 | `NoExecuteTaintManager` 进行 pod 驱逐的逻辑差不多就这么多,简单的说就是通过 listWatch 获取 node 和 pod 的更新事件,包装成带有计时器功能的数据结构放入驱逐队列中,一旦计时结束,则执行驱逐逻辑,并输出对应的事件。 -------------------------------------------------------------------------------- /kubectl/builder-visitor-pattern.md: -------------------------------------------------------------------------------- 1 | ## Kubectl Builder & Visitor 设计模式解析 2 | 3 | Kubernetes Version: v1.22@3b76c758317b 4 | Date: 2021.10.08 5 | 6 | 今天,跟大家分享下 `kubectl` 中常见的2种设计模式,`builder` 模式和 `visitor` 模式。我会结合具体的代码和大家一起重新温习下这些最基础的编程技巧。 7 | 8 | ### Builder 模式 9 | 10 | 1. 最简单的构造函数: 11 | 12 | // 默认构造函数 13 | p, _ := rocketmq.NewProducer() 14 | 15 | // 带有Timeout参数的构造函数 16 | p, _ := rocketmq.NewProducerWithTimeout(60*time.Second) 17 | 18 | > 缺点:扩展性不够 19 | 20 | 2. 含有配置参数的构造函数: 21 | 22 | type Config struct { 23 | Timeout *time.Time 24 | Retry int 25 | } 26 | 27 | p, _ := rocketmq.NewProducer(&config) 28 | 29 | func NewProducer(c *Config) *Producer { 30 | p := &Producer{} 31 | if c.Timeout != nil { 32 | p.Timeout = c.Timeout 33 | } 34 | 35 | if c.Retry > 0 { 36 | p.Retry = c.Retry 37 | } 38 | 39 | return p 40 | } 41 | 42 | > 缺点:不够简洁,需要判断参数是否为空 43 | 44 | 3. 通过闭包实现可变长参数的构造函数: 45 | 46 | sched, err := scheduler.New( 47 | cc.Client, 48 | // ... 49 | scheduler.WithKubeConfig(cc.KubeConfig), 50 | scheduler.WithProfiles(cc.ComponentConfig.Profiles...), 51 | // ... 52 | ) 53 | 54 | // 返回一个闭包函数 55 | func WithKubeConfig(cfg *restclient.Config) Option { 56 | return func(o *schedulerOptions) { 57 | o.kubeConfig = cfg 58 | } 59 | } 60 | 61 | func New(client clientset.Interface, opts ...Option) (*Scheduler, error) { 62 | // ... 63 | options := defaultSchedulerOptions 64 | for _, opt := range opts { 65 | opt(&options) // 将 New 方法传入的参数给了options 66 | } 67 | // ... 68 | } 69 | 70 | configurator := &Configurator{ 71 | componentConfigVersion: options.componentConfigVersion, 72 | percentageOfNodesToScore: options.percentageOfNodesToScore, 73 | podInitialBackoffSeconds: options.podInitialBackoffSeconds, 74 | podMaxBackoffSeconds: options.podMaxBackoffSeconds, 75 | extenders: options.extenders, 76 | frameworkCapturer: options.frameworkCapturer, 77 | parallellism: options.parallelism, 78 | } 79 | 80 | > 缺点:不够优雅(相较于builder模式而言) 81 | 82 | 4. builder模式 83 | 84 | r := f.NewBuilder(). 85 | // ... 86 | RequestChunksOf(chunkSize). 87 | Latest(). 88 | Do() 89 | 90 | // 返回 Builder 实例 91 | NewBuilder() *resource.Builder 92 | 93 | // 更新 Builder 实例的 latest值 94 | func (b *Builder) Latest() *Builder { 95 | b.latest = true 96 | return b 97 | } 98 | 99 | // 更新 Builder 实例的 limitChunks 100 | func (b *Builder) RequestChunksOf(chunkSize int64) *Builder { 101 | b.limitChunks = chunkSize 102 | return b 103 | } 104 | 105 | > 优点: 优雅且易读。设置好默认参数,通过链式调用动态配置需要修改的参数。 106 | 107 | 108 | ### Visitor 函数 109 | 1. 结构体方法链式调用 110 | 111 | person.SetAge(10).SetName("kobe").SetGender("male") 112 | 113 | 2. 循环调用 114 | 115 | http.Handler("/apiserver", authn, authz, admission) 116 | 117 | for i := range methods { 118 | methods[i].call() 119 | } 120 | 121 | 3. 使用 channel 122 | 123 | for { 124 | select { 125 | case <- ctx.Done(): 126 | return 127 | default: 128 | method <- methodsCh 129 | method.call() 130 | } 131 | } 132 | 133 | 4. visitor模式 134 | 135 | * 定义 136 | 137 | 138 | // Visitor 是带有 Visit 方法的接口 139 | type Visitor interface { 140 | Visit(VisitorFunc) error 141 | } 142 | 143 | // 一个实现了 Visitor 接口的结构体定义,需要有 visitor 字段 144 | type DecoratedVisitor struct { 145 | visitor Visitor 146 | decorators []VisitorFunc 147 | } 148 | 149 | // Visit 方法最关键的地方在于,它会调用自己 `visitor` 的 `Visit` 方法,形成调用链。 150 | func (v DecoratedVisitor) Visit(fn VisitorFunc) error { 151 | return v.visitor.Visit(func(info *Info, err error) error { 152 | if err != nil { 153 | return err 154 | } 155 | // ... 156 | return fn(info, nil) 157 | }) 158 | } 159 | 160 | 161 | * 声明 162 | 163 | 164 | // builder 模式构造函数 165 | r := f.NewBuilder(). 166 | // ... 167 | RequestChunksOf(chunkSize). 168 | Latest(). 169 | Do() 170 | 171 | func (b *Builder) Do() *Result { 172 | r := b.visitorResult() 173 | 174 | // ... 175 | 176 | r.visitor = NewFlattenListVisitor(r.visitor, b.objectTyper, b.mapper) 177 | 178 | // ... 179 | r.visitor = NewDecoratedVisitor(r.visitor, helpers...) 180 | 181 | return r 182 | } 183 | 184 | // Result 结构体 185 | type Result struct { 186 | visitor Visitor 187 | // ... 188 | } 189 | 190 | // Result 就是实现 Visitor 接口的结构体 191 | func (r *Result) Visit(fn VisitorFunc) error { 192 | if r.err != nil { 193 | return r.err 194 | } 195 | err := r.visitor.Visit(fn) 196 | return utilerrors.FilterOut(err, r.ignoreErrors...) 197 | } 198 | 199 | 200 | > 返回了一个 Visitor 多层嵌套的 Result 实例: `Result.Visitor -> NewDecoratedVisitor(NewFlattenListVisitor(Visitor))` 201 | 202 | * 调用 203 | 204 | infos, err := r.Infos() 205 | 206 | func (r *Result) Infos() ([]*Info, error) { 207 | // ... 208 | 209 | infos := []*Info{} 210 | err := r.visitor.Visit(func(info *Info, err error) error { 211 | if err != nil { 212 | return err 213 | } 214 | infos = append(infos, info) 215 | return nil 216 | }) 217 | 218 | // ... 219 | return infos, err 220 | } 221 | 222 | > 调用 `Result` 实例的 `Visit()` 方法,实现类似深度遍历搜索(`DFS`)的方法调用: `Visiotr.Visit()` -> `NewFlattenListVisitor.Visit()` -> `NewDecoratedVisitor.Visit()` 223 | 224 | * 注意 225 | 226 | 假设我们的Visit方法定义如下,num 表示变量数值: 227 | 228 | func (v NumVisitor) Visit(fn VisitorFunc) error { 229 | return v.visitor.Visit(func(info *Info, err error) error { 230 | fmt.Println("before function call-") 231 | err = fn(info, err) 232 | if err == nil { 233 | fmt.Println(err) 234 | } 235 | fmt.Println("after function call-") 236 | return err 237 | }) 238 | } 239 | 240 | 假设我们 visitor 嵌套关系为 `Result.Visitor = Num1Visitor(Num2Visitor(Num3Visitor))`,最终输出结果应该是: 241 | 242 | before function call-1 243 | before function call-2 244 | before function call-3 245 | ... 246 | after function call-3 247 | after function call-2 248 | after function call-1 249 | 250 | 其实就是一个单分支的 DFS 遍历树。 251 | 252 | ## 总结 253 | `kubernetes` 源代码中还有很多设计模式思想,今天只是介绍了 `kubectl` 中的 `builder` 和 `visitor` 两种,这也是我最初看源码的时候经常遇到和迷惑的地方,希望可以对大家有所帮助。 -------------------------------------------------------------------------------- /scheduler/Informer-mechanism.md: -------------------------------------------------------------------------------- 1 | ## Scheduler Informer 机制 (InProgres) 2 | 第二个配置项是 `InformerFactory`,它是一个 `sharedInformerFactory` 实例,该实例结构体如下: 3 | 4 | type sharedInformerFactory struct { 5 | client kubernetes.Interface 6 | namespace string 7 | tweakListOptions internalinterfaces.TweakListOptionsFunc 8 | lock sync.Mutex 9 | defaultResync time.Duration 10 | customResync map[reflect.Type]time.Duration 11 | informers map[reflect.Type]cache.SharedIndexInformer 12 | startedInformers map[reflect.Type]bool 13 | } 14 | `sharedInformerFactory` 负责构造各种 `informer` 对象,我们看到他有一个 `informers` 的 `map` 对象,用于存放各种 `informer`,通过共享一个 `sharedInformerFactory` 实例,`informer` 之间可以实现信息互通,以 `replicaController` 为例, 它不仅需要初始化 `replicationInformer`,还需要初始化 `podInformer`,这样就可以同时获得 `pod` 的创建删除事件,代码见下。同时用 `informer` 类型作为 `key`,也不需要竞争锁。 15 | 16 | func startReplicationController(ctx context.Context, controllerContext ControllerContext) (controller.Interface, bool, error) { 17 | go replicationcontroller.NewReplicationManager( 18 | controllerContext.InformerFactory.Core().V1().Pods(), // podInformer 19 | controllerContext.InformerFactory.Core().V1().ReplicationControllers(), // replicationInformer 20 | ).Run(ctx, int(controllerContext.ComponentConfig.ReplicationController.ConcurrentRCSyncs)) 21 | return nil, true, nil 22 | } 23 | 24 | 另外,细心的同学会发现还有一个 `Config()` 方法中还初始化了一个 `DynInformerFactory`,它的作用主要是针对一些 `CR` 资源,更多关于 `informer` 的介绍后面也会单独写一篇文章,这里主要是把这些东西都串起来。 25 | 26 | 27 | 28 | map[framework.ClusterEvent]sets.String 29 | 30 | 它是一个 `map` 结构,`key` 是一个结构体 `ClusterEvent`,`value` 是一个 `plugins name` 的 `map` 集合,之所以用 `map`,是因为 `golang` 没有 `set` 集合的概念,所以用同样具有去重功能的 `map` 来替代了。 31 | 32 | type ClusterEvent struct { 33 | Resource GVK 34 | ActionType ActionType 35 | Label string 36 | } 37 | 38 | 那 `c.clusterEventMap` 值从哪里来呢,我们往前看,来到 `pkg/scheduler/framework/runtime/framework.go` 的 `NewFramework()` 方法。 39 | 40 | func NewFramework(r Registry, profile *config.KubeSchedulerProfile, opts ...Option) (framework.Framework, error) { 41 | 42 | // ... 43 | 44 | for name, factory := range r { 45 | // ... 46 | fillEventToPluginMap(p, options.clusterEventMap) 47 | } 48 | } 49 | 50 | 这个方法调用了 `fillEventToPluginMap()`,其中 `p` 表示 `Plugin`,`clusterEventMap` 就是我们前面提到的参数,通过函数名我们判断是在这里进行了赋值操作。我们继续看这个方法的代码: 51 | 52 | func fillEventToPluginMap(p framework.Plugin, eventToPlugins map[framework.ClusterEvent]sets.String) { 53 | ext, ok := p.(framework.EnqueueExtensions) 54 | 55 | // ... 56 | 57 | events := ext.EventsToRegister() 58 | registerClusterEvents(p.Name(), eventToPlugins, events) 59 | } 60 | 61 | 核心代码就2行,`EventsToRegister()` 是一个接口方法,每个 `Plugin` 都需要实现这个方法,表示可能会引起该插件调度失败的事件,以 `NodeAffinity` 为例:TODO:。 `registerClusterEvents` 就会拼接我们最终想要的结果,其中一个 `event` 事件对应多个 `plugins` 名字。 62 | 63 | func (pl *NodeAffinity) EventsToRegister() []framework.ClusterEvent { 64 | return []framework.ClusterEvent{ 65 | {Resource: framework.Node, ActionType: framework.Add | framework.UpdateNodeLabel}, 66 | } 67 | } -------------------------------------------------------------------------------- /scheduler/cache.md: -------------------------------------------------------------------------------- 1 | # Scheduler Cache 机制 2 | 3 | Kubernetes Version: v1.23@45f2c63d 4 | Date: 2022.03.13 5 | 6 | ## 1. 开篇 7 | 调度器整体架构如下图所示,我们看到 cache 层存在的价值就是帮助调度器更有效的进行调度。它是事件驱动的,通过 reflector 进行 list/watch,所以有可能出现网络问题导致事件丢失。 8 | 9 | 下面我们一起从源码层面学习一下。 10 | ![cache](../snapshots/scheduler-architecture.png) 11 | 12 | ## 2. 初始化 13 | `Cache` 初始化时机是在 `scheduler` 初始化的时候,代码如下: 14 | ```golang 15 | // ttl 表示过期时间,所有 cache 是一个带有过期时间的缓存层 16 | func New(ttl time.Duration, stop <-chan struct{}) Cache { 17 | cache := newCache(ttl, cleanAssumedPeriod, stop) 18 | cache.run() 19 | return cache 20 | } 21 | ``` 22 | 23 | `Cache` 是一个接口定义,它的具体实现在 `newCache` 中,是一个 `cacheImpl` 结构体: 24 | ```golang 25 | type cacheImpl struct { 26 | stop <-chan struct{} // 中断 channel 信号 27 | ttl time.Duration // 表示过期时间 28 | period time.Duration // cache.run 中用到,表示方法运行的间隔时间 29 | mu sync.RWMutex // 读写锁 30 | assumedPods sets.String // 表示更新了 NodeName 的 pod,即找到了待调度的节点但是还未完成绑定 31 | podStates map[string]*podState // 存储所有的 pod,并表示 pod 当前状态,包括是否完成绑定,以及 pod 在 cache 中的过期时间 32 | nodes map[string]*nodeInfoListItem // 节点双向链表 33 | headNode *nodeInfoListItem // 最近更新的 node 34 | nodeTree *nodeTree // 树状结构,根据 zone 存储节点 35 | imageStates map[string]*imageState // 镜像状态,包括镜像大小以及哪些节点已经存在该镜像 36 | } 37 | ``` 38 | 39 | `run` 方法本质上就是循环调用 `cleanupExpiredAssumedPods` 方法,`period` 表示时间间隔: 40 | ```golang 41 | func (cache *cacheImpl) run() { 42 | go wait.Until(cache.cleanupExpiredAssumedPods, cache.period, cache.stop) 43 | } 44 | ``` 45 | 46 | `cleanupExpiredAssumedPods` 会循环调用 `cleanupAssumedPods` 方法来清理掉已经过期的 assumePod: 47 | ```golang 48 | func (cache *cacheImpl) cleanupAssumedPods(now time.Time) { 49 | cache.mu.Lock() 50 | defer cache.mu.Unlock() 51 | 52 | for key := range cache.assumedPods { 53 | ps, ok := cache.podStates[key] 54 | if !ok { 55 | klog.ErrorS(nil, "Key found in assumed set but not in podStates, potentially a logical error") 56 | os.Exit(1) 57 | } 58 | 59 | // 如果 binding 还在进行中,则拒绝过期该 pod 60 | if !ps.bindingFinished { 61 | klog.V(5).InfoS("Could not expire cache for pod as binding is still in progress", 62 | "pod", klog.KObj(ps.pod)) 63 | continue 64 | } 65 | 66 | // 如果 pod 已经过了最后期限,则从 cache中移除 67 | if now.After(*ps.deadline) { 68 | klog.InfoS("Pod expired", "pod", klog.KObj(ps.pod)) 69 | if err := cache.removePod(ps.pod); err != nil { 70 | klog.ErrorS(err, "ExpirePod failed", "pod", klog.KObj(ps.pod)) 71 | } 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | ## 3. Cache 基本方法 78 | 79 | ### 3.1 AssumePod 80 | `AssumePod` 在调度的 `assume` 阶段触发,假定 pod 已经完成调度。由于 `Assume` 发生在 `Add` 之前,因此也不存在更新和删除的操作,代码如下: 81 | ```golang 82 | func (cache *cacheImpl) AssumePod(pod *v1.Pod) error { 83 | // 获取 pod 的 UID 84 | key, err := framework.GetPodKey(pod) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // podStates 不是并发安全的,所以需要加锁 90 | cache.mu.Lock() 91 | defer cache.mu.Unlock() 92 | // 如果 podStates 中存在 pod,则报错 93 | if _, ok := cache.podStates[key]; ok { 94 | return fmt.Errorf("pod %v is in the cache, so can't be assumed", key) 95 | } 96 | 97 | // 将 pod 存入 cache,注意此时 pod 变成了 assumePod 98 | return cache.addPod(pod, true) 99 | } 100 | ``` 101 | 102 | ### 3.2 AddPod 103 | `AddPod` 方法负责将 pod 存入 cache 中,并从 assumedPods 中移除,源码如下: 104 | ```golang 105 | func (cache *cacheImpl) AddPod(pod *v1.Pod) error { 106 | key, err := framework.GetPodKey(pod) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | cache.mu.Lock() 112 | defer cache.mu.Unlock() 113 | 114 | currState, ok := cache.podStates[key] 115 | switch { 116 | // 如果 podStates 和 assumePods 中都有该 pod 记录,表示之前已经添加过 117 | case ok && cache.assumedPods.Has(key): 118 | // 如果 assume 和 assign 不是同一个 node,则需要更新 119 | if currState.pod.Spec.NodeName != pod.Spec.NodeName { 120 | if err = cache.updatePod(currState.pod, pod); err != nil { 121 | klog.ErrorS(err, "Error occurred while updating pod") 122 | } 123 | } else { 124 | // 否则需要将 assumePods 中的 pod 删除,重新进行调度。同时移除 deadline。本质和 updatePod 相差不大,但是代码效率更高 125 | delete(cache.assumedPods, key) 126 | cache.podStates[key].deadline = nil 127 | cache.podStates[key].pod = pod 128 | } 129 | case !ok: 130 | // 如果 podStates 没有 pod,则添加,注意这里 addPod 第二个参数为 false,表示并非是一个 assumePod 131 | if err = cache.addPod(pod, false); err != nil { 132 | klog.ErrorS(err, "Error occurred while adding pod") 133 | } 134 | default: 135 | // 其他情况入 podStates 中有值而 assumedPods 没有,表示可能是同一个 add 事件触发了两次,报错 136 | return fmt.Errorf("pod %v was already in added state", key) 137 | } 138 | return nil 139 | } 140 | ``` 141 | 142 | `updatePod` 方法逻辑很简单,先 remove 再 add: 143 | ```golang 144 | func (cache *cacheImpl) updatePod(oldPod, newPod *v1.Pod) error { 145 | if err := cache.removePod(oldPod); err != nil { 146 | return err 147 | } 148 | return cache.addPod(newPod, false) 149 | } 150 | ``` 151 | 我们先看一下 `removePod` 方法: 152 | ```golang 153 | func (cache *cacheImpl) removePod(pod *v1.Pod) error { 154 | // 获取 pod UID 155 | key, err := framework.GetPodKey(pod) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | n, ok := cache.nodes[pod.Spec.NodeName] 161 | // 如果 nodes 列表中没有该 node,打印错误日志,这里没有直接返回,因为后面的逻辑不会有副作用 162 | if !ok { 163 | klog.ErrorS(nil, "Node not found when trying to remove pod", "node", klog.KRef("", pod.Spec.NodeName), "pod", klog.KObj(pod)) 164 | } else { 165 | // 从 node 的 中移除 pod 相关信息,包括资源使用和端口占用等 166 | if err := n.info.RemovePod(pod); err != nil { 167 | return err 168 | } 169 | // 如果此时 node 中已经没有其他 pod,则可以删除该 node 相关信息 170 | if len(n.info.Pods) == 0 && n.info.Node() == nil { 171 | cache.removeNodeInfoFromList(pod.Spec.NodeName) 172 | } else { 173 | // 否则,将 node 移到列表最前面,表示最近更新 174 | cache.moveNodeInfoToHead(pod.Spec.NodeName) 175 | } 176 | } 177 | 178 | // 从 podStates 和 assumePods 中删除相关元素 179 | delete(cache.podStates, key) 180 | delete(cache.assumedPods, key) 181 | return nil 182 | } 183 | ``` 184 | 185 | 我们再看一下 `addPod()` 方法,它基本上就是 `removePod` 的反向操作: 186 | ```golang 187 | func (cache *cacheImpl) addPod(pod *v1.Pod, assumePod bool) error { 188 | // 获取 pod uid 189 | key, err := framework.GetPodKey(pod) 190 | if err != nil { 191 | return err 192 | } 193 | // 如果 nodes 中还没有对应的值,则初始化一个节点列表 194 | n, ok := cache.nodes[pod.Spec.NodeName] 195 | if !ok { 196 | n = newNodeInfoListItem(framework.NewNodeInfo()) 197 | cache.nodes[pod.Spec.NodeName] = n 198 | } 199 | // 添加 pod,完善节点信息,如计算 pod 资源占用率,端口使用情况等等 200 | n.info.AddPod(pod) 201 | // 将 node 添加到头部,表示最近更新 202 | cache.moveNodeInfoToHead(pod.Spec.NodeName) 203 | ps := &podState{ 204 | pod: pod, 205 | } 206 | // 添加 pod 到 podStates 中 207 | cache.podStates[key] = ps 208 | // 如果是assumePod,则添加至 `assumedPods` 集合 209 | if assumePod { 210 | cache.assumedPods.Insert(key) 211 | } 212 | return nil 213 | } 214 | ``` 215 | 216 | ### 3.3 ForgetPod 217 | `ForgetPod` 是 `assume` 的反向操作,负责删除 assumePod: 218 | ```golang 219 | func (cache *cacheImpl) ForgetPod(pod *v1.Pod) error { 220 | // 获取 pod uid 221 | key, err := framework.GetPodKey(pod) 222 | if err != nil { 223 | return err 224 | } 225 | 226 | // 加锁 227 | cache.mu.Lock() 228 | defer cache.mu.Unlock() 229 | 230 | // 逻辑检查 231 | currState, ok := cache.podStates[key] 232 | if ok && currState.pod.Spec.NodeName != pod.Spec.NodeName { 233 | return fmt.Errorf("pod %v was assumed on %v but assigned to %v", key, pod.Spec.NodeName, currState.pod.Spec.NodeName) 234 | } 235 | 236 | // 从 assumePods 中删除 pod 237 | if ok && cache.assumedPods.Has(key) { 238 | return cache.removePod(pod) 239 | } 240 | return fmt.Errorf("pod %v wasn't assumed so cannot be forgotten", key) 241 | } 242 | ``` 243 | 244 | ### 3.4 UpdatePod 245 | `UpdatePod` 负责更新 cache 中的 pod: 246 | ```golang 247 | func (cache *cacheImpl) UpdatePod(oldPod, newPod *v1.Pod) error { 248 | // 获取 old pod UID 249 | key, err := framework.GetPodKey(oldPod) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | // 加锁 255 | cache.mu.Lock() 256 | defer cache.mu.Unlock() 257 | 258 | currState, ok := cache.podStates[key] 259 | // AddPod 之后 assumedPods 中就没有该 pod 了,所以在这里做了一次检查。 260 | if ok && !cache.assumedPods.Has(key) { 261 | if currState.pod.Spec.NodeName != newPod.Spec.NodeName { 262 | klog.ErrorS(nil, "Pod updated on a different node than previously added to", "pod", klog.KObj(oldPod)) 263 | klog.ErrorS(nil, "scheduler cache is corrupted and can badly affect scheduling decisions") 264 | os.Exit(1) 265 | } 266 | // 更新逻辑 267 | return cache.updatePod(oldPod, newPod) 268 | } 269 | return fmt.Errorf("pod %v is not added to scheduler cache, so cannot be updated", key) 270 | } 271 | ``` 272 | 273 | ### 3.5 FinishBinding 274 | `FinishBinding` 在 scheduler bind 阶段负责结束绑定的相关逻辑: 275 | ```golang 276 | func (cache *cacheImpl) finishBinding(pod *v1.Pod, now time.Time) error { 277 | // 获取 pod uid 278 | key, err := framework.GetPodKey(pod) 279 | if err != nil { 280 | return err 281 | } 282 | 283 | // 加锁 284 | cache.mu.RLock() 285 | defer cache.mu.RUnlock() 286 | 287 | klog.V(5).InfoS("Finished binding for pod, can be expired", "pod", klog.KObj(pod)) 288 | currState, ok := cache.podStates[key] 289 | // 标记 bindingFinished 为 true,表示完成绑定 290 | // 更新 deadline 为 cache 默认的过期时间 291 | if ok && cache.assumedPods.Has(key) { 292 | dl := now.Add(cache.ttl) 293 | currState.bindingFinished = true 294 | currState.deadline = &dl 295 | } 296 | return nil 297 | } 298 | ``` 299 | 300 | ## 4. 机制 301 | 302 | ### 4.1 状态机 303 | 下图展示了 Cache 的状态机机制: 304 | 305 | +-------------------------------------------+ +----+ 306 | | Add | | | 307 | | | | | Update 308 | + Assume Add v v | 309 | Initial +--------> Assumed +------------+---> Added <--+ 310 | ^ + + | + 311 | | | | | | 312 | | | | Add | | Remove 313 | | | | | | 314 | | | | + | 315 | +----------------+ +-----------> Expired +----> Deleted 316 | Forget Expire 317 | 318 | 1. pod 被成功调度后,会执行 assume 添加到 cache 中,此时状态为 Assumed 319 | 2. 异步 bind 操作,如果超时,则调起 forget 操作,将 pod 从 cache 中删除 320 | 3. cache 会在后台定期清理过期 assumed pod 321 | 4. 如果 bind 成功,则会收到 AddEvent,将 pod 从 assumedPods 中移除 322 | 5. 如果收到 UpdateEvent,则更新 pod 323 | 6. 如果收到 DeleteEvent,则从 cache 中删除 pod 324 | 325 | ### 4.2 具体事件 326 | * 快照 snapshot 327 | 328 | 每一轮调度周期开始时 scheduler 会进行一次快照操作,调用 Cache `UpdateSnapshot` 方法,对 Cache 的节点信息进行快照。 329 | 330 | * list/watch 事件 331 | 332 | Pod: 333 | 334 | Pod:AddFunc -> Cache:AddPod 335 | 336 | Pod:UpdateFunc -> Cache:UpdatePod 337 | 338 | Pod:DeleteFunc -> Cache:RemovePod 339 | 340 | Node: 341 | 342 | Node:AddFunc -> Cache:AddNode 343 | 344 | Node:UpdateFunc -> Cache:UpdateNode 345 | 346 | Node:DeleteFunc -> Cache:RemoveNode 347 | 348 | * 跳过调度周期 349 | 350 | 如果 pod 是 assumePod,即在 cache 的 assumePods 中存在,则跳过调度周期。 351 | 352 | * Unreserve 353 | 354 | 所以调度失败进入 Unreserve 阶段的 pod,都会调用 cache 的 `ForgetPod` 方法。 355 | 356 | 357 | ## 5. 总结 358 | 以上就是 Cache 的大致工作逻辑,通过 reflector 监听 api 事件,其中 pod 大体经过了 assumed -> added -> updated -> deleted 这样一个流程,并在 cache 中进行了信息的整合。最后在调度的时候通过快照获得集群节点信息,提高调度的效率。 -------------------------------------------------------------------------------- /scheduler/event.md: -------------------------------------------------------------------------------- 1 | # Scheduler Event机制 2 | 3 | Kubernetes Version: v1.23@a504daa0 4 | Date: 2022.03.19 5 | 6 | ## 1. 开篇 7 | Kubernetes 通过 `informer` 的 `reflector` 组件监听 API 事件,这些事件在 scheduler 中会触发对应的 cache 操作和队列任务,今天我们就从源码层面看一下它们是如何工作的。 8 | 9 | ## 2. 数据结构 10 | 在将事件处理的具体逻辑之前,我们先捋一捋会涉及到的几种数据结构: 11 | 1. `ResourceEventHandler` 接口,定义如下: 12 | ```golang 13 | type ResourceEventHandler interface { 14 | OnAdd(obj interface{}) 15 | OnUpdate(oldObj, newObj interface{}) 16 | OnDelete(obj interface{}) 17 | } 18 | ``` 19 | 这个接口定义了处理 Event 的3个方法,分别是 Add 事件的 `OnAdd` 方法,Update 事件的 `OnUpdate` 方法,以及 Delete 事件的 `OnDelete` 方法。 20 | 21 | 2. `FilteringResourceEventHandler` 是接口 `ResourceEventHandler` 的一个具体实现: 22 | ```golang 23 | type FilteringResourceEventHandler struct { 24 | FilterFunc func(obj interface{}) bool 25 | Handler ResourceEventHandler 26 | } 27 | ``` 28 | `FilterFunc` 用来过滤事件,`Handler` 则封装了具体的处理方法,我们一次看一下3个方法: 29 | 30 | OnAdd: 31 | ```golang 32 | func (r FilteringResourceEventHandler) OnAdd(obj interface{}) { 33 | // 如果没有通过 FilterFunc,则直接返回 34 | // 否则执行 OnAdd 逻辑 35 | if !r.FilterFunc(obj) { 36 | return 37 | } 38 | r.Handler.OnAdd(obj) 39 | } 40 | ``` 41 | 42 | OnUpdate: 43 | ```golang 44 | func (r FilteringResourceEventHandler) OnUpdate(oldObj, newObj interface{}) { 45 | newer := r.FilterFunc(newObj) 46 | older := r.FilterFunc(oldObj) 47 | switch { 48 | // 这里进行了聚合操作,虽然是 Update 事件,但是处理方式却不尽相同 49 | case newer && older: 50 | r.Handler.OnUpdate(oldObj, newObj) 51 | case newer && !older: 52 | r.Handler.OnAdd(newObj) 53 | case !newer && older: 54 | r.Handler.OnDelete(oldObj) 55 | default: 56 | // do nothing 57 | } 58 | } 59 | ``` 60 | 61 | OnDelete: 62 | ```golang 63 | func (r FilteringResourceEventHandler) OnDelete(obj interface{}) { 64 | // 如果没有通过 Filter,则直接返回 65 | // 否则进行删除逻辑 66 | if !r.FilterFunc(obj) { 67 | return 68 | } 69 | r.Handler.OnDelete(obj) 70 | } 71 | ``` 72 | 73 | ## 3. 事件处理 74 | `scheduler` 事件处理逻辑位于代码 `pkg/scheduler/eventhandlers.go:L251`,其中 75 | ```golang 76 | func addAllEventHandlers( 77 | sched *Scheduler, 78 | informerFactory informers.SharedInformerFactory, 79 | dynInformerFactory dynamicinformer.DynamicSharedInformerFactory, 80 | gvkMap map[framework.GVK]framework.ActionType, 81 | ) { 82 | // 监听 pod 事件并触发 scheduler cache 对应的处理方法,详见 https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/cache.md 83 | informerFactory.Core().V1().Pods().Informer().AddEventHandler( 84 | // 前面讲过 FilteringResourceEventHandler 是 ResourceEventHandler 的具体实现,其中 FilterFunc 用于过滤事件 85 | cache.FilteringResourceEventHandler{ 86 | FilterFunc: func(obj interface{}) bool { 87 | switch t := obj.(type) { 88 | // 如果是 pod,则判断 pod 是否为已调度 Pod,可以通过 pod.Spec.NodeName 是否有值判断 89 | case *v1.Pod: 90 | return assignedPod(t) 91 | // 处理丢失的删除事件 92 | case cache.DeletedFinalStateUnknown: 93 | if _, ok := t.Obj.(*v1.Pod); ok { 94 | return true 95 | } 96 | utilruntime.HandleError(fmt.Errorf("unable to convert object %T to *v1.Pod in %T", obj, sched)) 97 | return false 98 | default: 99 | utilruntime.HandleError(fmt.Errorf("unable to handle object in %T: %T", sched, obj)) 100 | return false 101 | } 102 | }, 103 | // 具体的处理逻辑,前面的文章已经讲过 104 | Handler: cache.ResourceEventHandlerFuncs{ 105 | AddFunc: sched.addPodToCache, 106 | UpdateFunc: sched.updatePodInCache, 107 | DeleteFunc: sched.deletePodFromCache, 108 | }, 109 | }, 110 | ) 111 | // 上面是触发 cache 相关操作,这里是触发 scheduler queue 相关逻辑 112 | informerFactory.Core().V1().Pods().Informer().AddEventHandler( 113 | cache.FilteringResourceEventHandler{ 114 | FilterFunc: func(obj interface{}) bool { 115 | switch t := obj.(type) { 116 | case *v1.Pod: 117 | // 判断 pod 是否完成调度,即 pod.Spec.NodeName 有值 118 | // 另外,还需要该 pod SchedulerName 为已经注册的 调度器名称 119 | return !assignedPod(t) && responsibleForPod(t, sched.Profiles) 120 | // 处理丢失的删除事件 121 | case cache.DeletedFinalStateUnknown: 122 | if pod, ok := t.Obj.(*v1.Pod); ok { 123 | return responsibleForPod(pod, sched.Profiles) 124 | } 125 | utilruntime.HandleError(fmt.Errorf("unable to convert object %T to *v1.Pod in %T", obj, sched)) 126 | return false 127 | default: 128 | utilruntime.HandleError(fmt.Errorf("unable to handle object in %T: %T", sched, obj)) 129 | return false 130 | } 131 | }, 132 | // 丢到队列中进行处理,这里不详细介绍 133 | Handler: cache.ResourceEventHandlerFuncs{ 134 | AddFunc: sched.addPodToSchedulingQueue, 135 | UpdateFunc: sched.updatePodInSchedulingQueue, 136 | DeleteFunc: sched.deletePodFromSchedulingQueue, 137 | }, 138 | }, 139 | ) 140 | 141 | // 注册 Node 事件处理函数 142 | informerFactory.Core().V1().Nodes().Informer().AddEventHandler( 143 | cache.ResourceEventHandlerFuncs{ 144 | AddFunc: sched.addNodeToCache, 145 | UpdateFunc: sched.updateNodeInCache, 146 | DeleteFunc: sched.deleteNodeFromCache, 147 | }, 148 | ) 149 | 150 | // 动态构造事件处理方法,其中 ActionType 表示事件类型,通过 & 进行与判断 151 | buildEvtResHandler := func(at framework.ActionType, gvk framework.GVK, shortGVK string) cache.ResourceEventHandlerFuncs { 152 | funcs := cache.ResourceEventHandlerFuncs{} 153 | // Add 事件 154 | if at&framework.Add != 0 { 155 | evt := framework.ClusterEvent{Resource: gvk, ActionType: framework.Add, Label: fmt.Sprintf("%vAdd", shortGVK)} 156 | funcs.AddFunc = func(_ interface{}) { 157 | sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(evt, nil) 158 | } 159 | } 160 | // Update 事件 161 | if at&framework.Update != 0 { 162 | evt := framework.ClusterEvent{Resource: gvk, ActionType: framework.Update, Label: fmt.Sprintf("%vUpdate", shortGVK)} 163 | funcs.UpdateFunc = func(_, _ interface{}) { 164 | sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(evt, nil) 165 | } 166 | } 167 | // Delete 事件 168 | if at&framework.Delete != 0 { 169 | evt := framework.ClusterEvent{Resource: gvk, ActionType: framework.Delete, Label: fmt.Sprintf("%vDelete", shortGVK)} 170 | funcs.DeleteFunc = func(_ interface{}) { 171 | sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(evt, nil) 172 | } 173 | } 174 | return funcs 175 | } 176 | 177 | // 处理各类事件 178 | for gvk, at := range gvkMap { 179 | switch gvk { 180 | case framework.Node, framework.Pod: 181 | // 前面已经单独处理过 Node 和 Pod 事件,这里就不再处理了 182 | case framework.CSINode: 183 | informerFactory.Storage().V1().CSINodes().Informer().AddEventHandler( 184 | buildEvtResHandler(at, framework.CSINode, "CSINode"), 185 | ) 186 | case framework.CSIDriver: 187 | informerFactory.Storage().V1().CSIDrivers().Informer().AddEventHandler( 188 | buildEvtResHandler(at, framework.CSIDriver, "CSIDriver"), 189 | ) 190 | case framework.CSIStorageCapacity: 191 | informerFactory.Storage().V1beta1().CSIStorageCapacities().Informer().AddEventHandler( 192 | buildEvtResHandler(at, framework.CSIStorageCapacity, "CSIStorageCapacity"), 193 | ) 194 | case framework.PersistentVolume: 195 | informerFactory.Core().V1().PersistentVolumes().Informer().AddEventHandler( 196 | buildEvtResHandler(at, framework.PersistentVolume, "Pv"), 197 | ) 198 | case framework.PersistentVolumeClaim: 199 | informerFactory.Core().V1().PersistentVolumeClaims().Informer().AddEventHandler( 200 | buildEvtResHandler(at, framework.PersistentVolumeClaim, "Pvc"), 201 | ) 202 | case framework.StorageClass: 203 | if at&framework.Add != 0 { 204 | informerFactory.Storage().V1().StorageClasses().Informer().AddEventHandler( 205 | cache.ResourceEventHandlerFuncs{ 206 | AddFunc: sched.onStorageClassAdd, 207 | }, 208 | ) 209 | } 210 | if at&framework.Update != 0 { 211 | informerFactory.Storage().V1().StorageClasses().Informer().AddEventHandler( 212 | cache.ResourceEventHandlerFuncs{ 213 | UpdateFunc: func(_, _ interface{}) { 214 | sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(queue.StorageClassUpdate, nil) 215 | }, 216 | }, 217 | ) 218 | } 219 | default: 220 | // 使用 dynamic informer 221 | // 测试用例可能不注册 dynInformerFactory,所以这里跳过 222 | if dynInformerFactory == nil { 223 | continue 224 | } 225 | // 对 GVK 格式进行校验 226 | if strings.Count(string(gvk), ".") < 2 { 227 | klog.ErrorS(nil, "incorrect event registration", "gvk", gvk) 228 | continue 229 | } 230 | 231 | gvr, _ := schema.ParseResourceArg(string(gvk)) 232 | dynInformer := dynInformerFactory.ForResource(*gvr).Informer() 233 | dynInformer.AddEventHandler( 234 | buildEvtResHandler(at, gvk, strings.Title(gvr.Resource)), 235 | ) 236 | } 237 | } 238 | } 239 | ``` 240 | 241 | 我们看到所谓的事件处理,其实就是将捕获的事件转化为调度事件。另外,我们注意到方法中的 `gvkMap` 存储了所有的事件,那么这些事件是从何而来的呢,回溯 `addAllEventHandlers` 方法,发现在初始化 scheduler 的时候,会把对应的事件传入: 242 | 243 | ```golang 244 | addAllEventHandlers(sched, informerFactory, dynInformerFactory, unionedGVKs(clusterEventMap)) 245 | ``` 246 | 其中的 `clusterEventMap` 就是原始的事件集合,它是一个 `cluster event -> plugin names` 的字典,但奇怪的是,没有在 scheduler 初始化方法中看到填充字典的逻辑,既然 clusterEventMap 的值是插件名称,而 scheduler framework 负责管理所有的插件,是不是在 framework 初始化的时候处理了该逻辑呢,事实也确实如何,我们在之前讲解 framework 框架的时候也提到过。 247 | ```golang 248 | clusterEventMap map[framework.ClusterEvent]sets.String 249 | ``` 250 | 251 | ## 4. 事件注册 252 | 事件注册逻辑的入口位于 `pkg/scheduler/framework/runtime/framework.go:L330` `NewFramework` 方法中: 253 | ```golang 254 | func fillEventToPluginMap(p framework.Plugin, eventToPlugins map[framework.ClusterEvent]sets.String) { 255 | // `EnqueueExtensions` 是一个接口,插件需要实现该接口的 `EventsToRegister` 方法 256 | ext, ok := p.(framework.EnqueueExtensions) 257 | if !ok { 258 | // 为了保持兼容性,如果插件没有实现接口,则注册所有的事件 259 | registerClusterEvents(p.Name(), eventToPlugins, allClusterEvents) 260 | return 261 | } 262 | 263 | // 获取需要注册的事件 264 | events := ext.EventsToRegister() 265 | // 如果没有,则返回 266 | if len(events) == 0 { 267 | klog.InfoS("Plugin's EventsToRegister() returned nil", "plugin", p.Name()) 268 | return 269 | } 270 | // 注册事件 271 | registerClusterEvents(p.Name(), eventToPlugins, events) 272 | } 273 | ``` 274 | 275 | 如此,完成了事件的注册逻辑。我们以插件 `PodTopologySpread` 为例,看看他的 `EventsToRegister` 方法: 276 | ```golang 277 | func (pl *PodTopologySpread) EventsToRegister() []framework.ClusterEvent { 278 | return []framework.ClusterEvent{ 279 | {Resource: framework.Pod, ActionType: framework.All}, 280 | {Resource: framework.Node, ActionType: framework.Add | framework.Delete | framework.UpdateNodeLabel}, 281 | } 282 | } 283 | ``` 284 | 我们看到插件注册了 Pod 所有的事件,以及 Node 的增删事件和 label 更新事件。 285 | 286 | ## 5. 总结 287 | 至此,scheduler 事件机制就说完了。核心逻辑就是通过 informer reflector 机制监听 API 事件,并通过插件注册对应事件的监听规则,转化为调度事件,执行缓存和队列逻辑。 -------------------------------------------------------------------------------- /scheduler/framework.md: -------------------------------------------------------------------------------- 1 | # Scheduler 调度框架 2 | 3 | Kubernetes Version: v1.23@4ade9f25 4 | Date: 2022.02.07 5 | 6 | ## 1. 开篇 7 | 调度框架是面向 Kubernetes 调度器的一种插件架构, 它定义了多个扩展点,插件注册后在一个或者多个扩展点被调用。整个调度周期可以分为两个阶段,分别是调度周期和绑定周期,官方文档见[这里](https://kubernetes.io/zh/docs/concepts/scheduling-eviction/scheduling-framework/)。 8 | 9 | 下图显示了调度框架的扩展点: 10 | ![framework](../snapshots/scheduling-framework-extensions.png) 11 | ### 1.1 PreFilter 12 | `PreFilter` 用于预处理 Pod 的相关信息,或者检查集群或 Pod 必须满足的某些条件。 如果 PreFilter 插件返回错误,则调度周期将终止。`PreFilterPlugin` 需要实现3个接口: 13 | ```golang 14 | Name() string 15 | PreFilter(ctx context.Context, state *CycleState, p *v1.Pod) *Status 16 | PreFilterExtensions() PreFilterExtensions 17 | ``` 18 | 19 | ### 1.2 Filter 20 | `Filter` 用于过滤出不能运行该 Pod 的节点。对于每个节点, 调度器将按照其配置顺序调用这些过滤插件。如果任何过滤插件将节点标记为不可行, 则不会为该节点调用剩下的过滤插件。节点可以被同时进行评估。`Filter` 需要实现2个接口: 21 | ```golang 22 | Name() string 23 | Filter(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *NodeInfo) *Status 24 | ``` 25 | 26 | ### 1.3 PreScore 27 | `PreScore` 用于执行 “前置评分” 工作,即生成一个可共享状态供评分插件使用。 如果 PreScore 插件返回错误,则调度周期将终止。`PreScorePlugin` 需要实现3个接口: 28 | ```golang 29 | Name() string 30 | PreScore(ctx context.Context, state *CycleState, pod *v1.Pod, nodes []*v1.Node) *Status 31 | ``` 32 | 33 | ### 1.4 Score 34 | `Score` 用于对通过过滤阶段的节点进行排名。调度器将为每个节点调用每个评分插件。 将有一个定义明确的整数范围,代表最小和最大分数。 在标准化评分阶段之后,调度器将根据配置的插件权重 合并所有插件的节点分数。插件实现接口: 35 | ```golang 36 | Name() string 37 | Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status) 38 | ScoreExtensions() ScoreExtensions 39 | ``` 40 | 41 | ### 1.5 Normalize Score 42 | `Normalize Score` 插件用于在调度器计算节点的排名之前修改分数。 在此扩展点注册的插件将使用同一插件的评分 结果被调用。 每个插件在每个调度周期调用一次。 43 | `Normalize Score` 其实就是 score 插件实现的 `ScoreExtensions` 方法。 44 | 45 | ### 1.6 Reserve 46 | Reserve 是一个信息性的扩展点。 管理运行时状态的插件(也称为“有状态插件”)应该使用此扩展点,以便 调度器在节点给指定 Pod 预留了资源时能够通知该插件。 这是在调度器真正将 Pod 绑定到节点之前发生的,并且它存在是为了防止 在调度器等待绑定成功时发生竞争情况。 47 | 48 | ### 1.7 UnReserve 49 | 这是个信息性的扩展点。 如果 Pod 被保留,然后在后面的阶段中被拒绝,则 Unreserve 插件将被通知。 Unreserve 插件应该清楚保留 Pod 的相关状态。 50 | 51 | ### 1.8 Permit 52 | Permit 插件在每个 Pod 调度周期的最后调用,用于防止或延迟 Pod 的绑定。 一个允许插件可以做以下三件事之一: 53 | 54 | 1. 批准 55 | 56 | 一旦所有 Permit 插件批准 Pod 后,该 Pod 将被发送以进行绑定。 57 | 58 | 2. 拒绝 59 | 60 | 如果任何 Permit 插件拒绝 Pod,则该 Pod 将被返回到调度队列。 这将触发Unreserve 插件。 61 | 62 | 3. 等待(带有超时) 63 | 64 | 如果一个 Permit 插件返回 “等待” 结果,则 Pod 将保持在一个内部的 “等待中” 的 Pod 列表,同时该 Pod 的绑定周期启动时即直接阻塞直到得到 批准。如果超时发生,等待 变成 拒绝,并且 Pod 将返回调度队列,从而触发 Unreserve 插件。 65 | 66 | ### 1.9 PreBind 67 | PreBind 插件用于执行 Pod 绑定前所需的任何工作。 例如,一个预绑定插件可能需要提供网络卷并且在允许 Pod 运行在该节点之前 将其挂载到目标节点上。 68 | 69 | 如果任何 PreBind 插件返回错误,则 Pod 将被拒绝 并且 退回到调度队列中。 70 | 71 | ### 1.10 Bind 72 | Bind 插件用于将 Pod 绑定到节点上。直到所有的 PreBind 插件都完成,Bind 插件才会被调用。 各绑定插件按照配置顺序被调用。绑定插件可以选择是否处理指定的 Pod。 如果绑定插件选择处理 Pod,剩余的绑定插件将被跳过。 73 | 74 | ### 1.11 PostBind 75 | 这是个信息性的扩展点。 绑定后插件在 Pod 成功绑定后被调用。这是绑定周期的结尾,可用于清理相关的资源。 76 | 77 | 下面,我们就从代码层面了解整个调度框架的工作机制。 78 | 79 | ## 2. 插件注册 80 | 首先是注册插件,我们以最新版本 `v1beta3` 为例,代码位于 `pkg/scheduler/apis/config/v1beta3/default_plugins.go`: 81 | ```golang 82 | func getDefaultPlugins() *v1beta3.Plugins { 83 | plugins := &v1beta3.Plugins{ 84 | MultiPoint: v1beta3.PluginSet{ 85 | Enabled: []v1beta3.Plugin{ 86 | {Name: names.PrioritySort}, 87 | {Name: names.NodeUnschedulable}, 88 | {Name: names.NodeName}, 89 | {Name: names.TaintToleration, Weight: pointer.Int32(3)}, 90 | {Name: names.NodeAffinity, Weight: pointer.Int32(2)}, 91 | {Name: names.NodePorts}, 92 | {Name: names.NodeResourcesFit, Weight: pointer.Int32(1)}, 93 | {Name: names.VolumeRestrictions}, 94 | {Name: names.EBSLimits}, 95 | {Name: names.GCEPDLimits}, 96 | {Name: names.NodeVolumeLimits}, 97 | {Name: names.AzureDiskLimits}, 98 | {Name: names.VolumeBinding}, 99 | {Name: names.VolumeZone}, 100 | {Name: names.PodTopologySpread, Weight: pointer.Int32(2)}, 101 | {Name: names.InterPodAffinity, Weight: pointer.Int32(2)}, 102 | {Name: names.DefaultPreemption}, 103 | {Name: names.NodeResourcesBalancedAllocation, Weight: pointer.Int32(1)}, 104 | {Name: names.ImageLocality, Weight: pointer.Int32(1)}, 105 | {Name: names.DefaultBinder}, 106 | }, 107 | }, 108 | } 109 | applyFeatureGates(plugins) 110 | 111 | return plugins 112 | } 113 | ``` 114 | 115 | `MultiPoint` 定义了默认启用的 plugins,减少了用户心智,不需要在每个扩展点单独设置,但是也有其他问题,比如不清楚这个 plugin 到底实现了哪个扩展点。 116 | 117 | `getDefaultPlugins` 方法何时调用,我们需要追溯到 `RegisterDefaults`,该方法会在初始化的时候调用,完成一些默认方法的注册,所谓默认方法,最常见的就是补充默认参数,`RegisterDefaults` 代码如下: 118 | ```golang 119 | func RegisterDefaults(scheme *runtime.Scheme) error { 120 | // ... 121 | scheme.AddTypeDefaultingFunc(&v1beta3.InterPodAffinityArgs{}, func(obj interface{}) { SetObjectDefaults_InterPodAffinityArgs(obj.(*v1beta3.InterPodAffinityArgs)) }) 122 | scheme.AddTypeDefaultingFunc(&v1beta3.KubeSchedulerConfiguration{}, func(obj interface{}) { 123 | SetObjectDefaults_KubeSchedulerConfiguration(obj.(*v1beta3.KubeSchedulerConfiguration)) 124 | }) 125 | // ... 126 | return nil 127 | } 128 | ``` 129 | 130 | 其中 `AddTypeDefaultingFunc`,会将默认方法存到 `map[reflect.Type]func(interface{})` 中: 131 | ```golang 132 | func (s *Scheme) AddTypeDefaultingFunc(srcType Object, fn func(interface{})) { 133 | s.defaulterFuncs[reflect.TypeOf(srcType)] = fn 134 | } 135 | ``` 136 | `RegisterDefaults` 方法中注册了 `SetObjectDefaults_KubeSchedulerConfiguration` 方法,该方法会完成 `KubeSchedulerConfiguration` 一些默认参数的配置工作,其中通过调用 `setDefaults_KubeSchedulerProfile` 方法完成 `getDefaultPlugins` 的调用 137 | ```golang 138 | func setDefaults_KubeSchedulerProfile(prof *v1beta3.KubeSchedulerProfile) { 139 | // Set default plugins. 140 | prof.Plugins = mergePlugins(getDefaultPlugins(), prof.Plugins) 141 | // ... 142 | } 143 | ``` 144 | 145 | 我们看到该方法会将默认配置的 plugin 和我们在 profile 中自定义的 plugin 配置进行 merge 操作,完成最终的 plugin 设置。 146 | 147 | 注册完成后,何时调用该方法呢?我们在 scheduler server `Setup` 的时候会调用 `Default` 方法,获得 config 配置: 148 | ```golang 149 | func Default() (*config.KubeSchedulerConfiguration, error) { 150 | versionedCfg := v1beta3.KubeSchedulerConfiguration{} 151 | versionedCfg.DebuggingConfiguration = *v1alpha1.NewRecommendedDebuggingConfiguration() 152 | 153 | scheme.Scheme.Default(&versionedCfg) 154 | // ... 155 | } 156 | ``` 157 | 158 | 另外,对于通过使用 `config file` 进行初始化的情况,也会调用下面的方法进行初始化: 159 | ```golang 160 | cfg, err := loadConfigFromFile(o.ConfigFile) 161 | ``` 162 | 163 | scheme 通过调用 `Default()`方法,完成所有默认方法的调用,我们看一下 scheme 的 `Default` 方法,其实就是调用之前 map 中存储的方法: 164 | ```golang 165 | func (s *Scheme) Default(src Object) { 166 | if fn, ok := s.defaulterFuncs[reflect.TypeOf(src)]; ok { 167 | fn(src) 168 | } 169 | } 170 | ``` 171 | 172 | 至此,我们明白了整个插件的注册及初始化流程。 173 | ## 3. 调度周期 174 | 调度周期可以分为两个阶段,也就是我们常说的预选和优选阶段。代码入口位于 `pkg/scheduler/scheduler.go:L455`: 175 | ```golang 176 | scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, sched.Extenders, fwk, state, pod) 177 | ``` 178 | 179 | ### 3.1 预选阶段 180 | 预选阶段代码位于 `pkg/scheduler/generic_scheduler.go:L213` ,我们一起看一下: 181 | ```golang 182 | func (g *genericScheduler) findNodesThatFitPod(ctx context.Context, extenders []framework.Extender, fwk framework.Framework, state *framework.CycleState, pod *v1.Pod) ([]*v1.Node, framework.Diagnosis, error) { 183 | // diagnosis 用于记录调度失败相关信息,方便后期进行分析 184 | diagnosis := framework.Diagnosis{ 185 | NodeToStatusMap: make(framework.NodeToStatusMap), 186 | UnschedulablePlugins: sets.NewString(), 187 | } 188 | 189 | // PreFilter 扩展点 190 | s := fwk.RunPreFilterPlugins(ctx, state, pod) 191 | 192 | // 状态返回失败 193 | if !s.IsSuccess() { 194 | // 状态返回失败并且不是不可调度错误,直接返回错误结果(不可调度错误一般是因为资源不够用,可以等待进行新一轮调度,否则就是其他错误,重新调度也是出错,所以可以直接返回) 195 | if !s.IsUnschedulable() { 196 | return nil, diagnosis, s.AsError() 197 | } 198 | 199 | // 记录节点状态 200 | for _, n := range allNodes { 201 | diagnosis.NodeToStatusMap[n.Node().Name] = s 202 | } 203 | 204 | // 记录失败插件信息 205 | diagnosis.UnschedulablePlugins.Insert(s.FailedPlugin()) 206 | return nil, diagnosis, nil 207 | } 208 | 209 | // ... 抢占逻辑 210 | 211 | // Filter 扩展点 212 | feasibleNodes, err := g.findNodesThatPassFilters(ctx, fwk, state, pod, diagnosis, allNodes) 213 | if err != nil { 214 | return nil, diagnosis, err 215 | } 216 | 217 | // ... extender 逻辑,这里不展开 218 | 219 | return feasibleNodes, diagnosis, nil 220 | } 221 | ``` 222 | 223 | 接下来,我们重点看一下 `RunPreFilterPlugins` 和 `findNodesThatPassFilters` 方法。 224 | 225 | `RunPreFilterPlugins` 位于 `pkg/scheduler/framework/runtime/framework.go`: 226 | ```golang 227 | func (f *frameworkImpl) RunPreFilterPlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod) (status *framework.Status) { 228 | // 遍历所有 PreFilterPlugins 229 | for _, pl := range f.preFilterPlugins { 230 | // 执行每个 plugin 的 PreFilter 方法 231 | status = f.runPreFilterPlugin(ctx, pl, state, pod) 232 | // 如果返回状态失败,则直接返回,终止调度周期 233 | // 如果是不可调度原因,记录失败的插件名称,并直接返回 status, kubernetes 会将它包装成 FitError,FitError 会参与后续的 pod 重调度 234 | if !status.IsSuccess() { 235 | status.SetFailedPlugin(pl.Name()) 236 | if status.IsUnschedulable() { 237 | return status 238 | } 239 | return framework.AsStatus(fmt.Errorf("running PreFilter plugin %q: %w", pl.Name(), status.AsError())).WithFailedPlugin(pl.Name()) 240 | } 241 | } 242 | 243 | return nil 244 | } 245 | ``` 246 | `RunPreFilterPlugins` 方法主要负责遍历所有的 PreFilter plugins 并进行一些预处理,如果有任何一个插件返回状态失败则终止调度。 247 | 248 | `findNodesThatPassFilters` 方法位于 `pkg/scheduler/generic_scheduler.go`: 249 | ```golang 250 | func (g *genericScheduler) findNodesThatPassFilters( 251 | ctx context.Context, 252 | fwk framework.Framework, 253 | state *framework.CycleState, 254 | pod *v1.Pod, 255 | diagnosis framework.Diagnosis, 256 | nodes []*framework.NodeInfo) ([]*v1.Node, error) { 257 | // ... 258 | 259 | checkNode := func(i int) { 260 | // ... 找到 nodeInfo 261 | 262 | // 执行 Filter 方法 263 | status := fwk.RunFilterPluginsWithNominatedPods(ctx, state, pod, nodeInfo) 264 | // 如果出错,则发送错误到 channel,并立马执行 cancel() 结束并发 265 | if status.Code() == framework.Error { 266 | errCh.SendErrorWithCancel(status.AsError(), cancel) 267 | return 268 | } 269 | 270 | // 如果成功,则添加到 feasibleNodes 中 271 | if status.IsSuccess() { 272 | length := atomic.AddInt32(&feasibleNodesLen, 1) 273 | // 如果查找的节点数达到要求,则立即终止 Filter 阶段 274 | if length > numNodesToFind { 275 | cancel() 276 | atomic.AddInt32(&feasibleNodesLen, -1) 277 | } else { 278 | feasibleNodes[length-1] = nodeInfo.Node() 279 | } 280 | // 如果错误,则记录调度失败相关信息 281 | } else { 282 | statusesLock.Lock() 283 | diagnosis.NodeToStatusMap[nodeInfo.Node().Name] = status 284 | diagnosis.UnschedulablePlugins.Insert(status.FailedPlugin()) 285 | statusesLock.Unlock() 286 | } 287 | } 288 | 289 | // ... 290 | 291 | // 针对所有节点并发执行 292 | fwk.Parallelizer().Until(ctx, len(nodes), checkNode) 293 | 294 | // ... 295 | 296 | // 返回合适的节点列表 297 | return feasibleNodes, nil 298 | } 299 | ``` 300 | 301 | 这里我们需要对 `RunFilterPluginsWithNominatedPods` 方法再深入展开一下: 302 | ```golang 303 | func (f *frameworkImpl) RunFilterPluginsWithNominatedPods(ctx context.Context, state *framework.CycleState, pod *v1.Pod, info *framework.NodeInfo) *framework.Status { 304 | var status *framework.Status 305 | 306 | podsAdded := false 307 | // 这里执行了两次 Filter 308 | // 第一次将节点上完成抢占但未调度成功,我们称之为 nominatedPod 加入到 state 进行 Filter 流程, 309 | // 之所以这么做原因是不希望本轮调度会让之前抢占成功的 pod 再次被调度的时候由于如资源不足等原因被拒绝 310 | // 第二次则不考虑该情况有走了一遍 Filter 流程是担心之前的 nominatedPod 会对 Filter 结果造成影响,尤其是一些亲和性相关的 FilterPlugin 311 | for i := 0; i < 2; i++ { 312 | stateToUse := state 313 | nodeInfoToUse := info 314 | if i == 0 { 315 | var err error 316 | podsAdded, stateToUse, nodeInfoToUse, err = addNominatedPods(ctx, f, pod, state, info) 317 | if err != nil { 318 | return framework.AsStatus(err) 319 | } 320 | } else if !podsAdded || !status.IsSuccess() { 321 | break 322 | } 323 | 324 | // RunFilterPlugins 会遍历每一个 plugin,返回一个 map。默认情况下,只要有一个 plugin 调度失败,就直接终止调度周期,除非设置 runAllFilters = true。 325 | statusMap := f.RunFilterPlugins(ctx, stateToUse, pod, nodeInfoToUse) 326 | // 这里对status进行了一次 merge 操作 327 | status = statusMap.Merge() 328 | if !status.IsSuccess() && !status.IsUnschedulable() { 329 | return status 330 | } 331 | } 332 | 333 | return status 334 | } 335 | ``` 336 | 337 | ### 3.2 优选阶段 338 | 优选代码位于 `pkg/scheduler/generic_scheduler.go:L396` 代码如下: 339 | ```golang 340 | func prioritizeNodes( 341 | ctx context.Context, 342 | extenders []framework.Extender, 343 | fwk framework.Framework, 344 | state *framework.CycleState, 345 | pod *v1.Pod, 346 | nodes []*v1.Node, 347 | ) (framework.NodeScoreList, error) { 348 | // ... 349 | 350 | // PreScore 阶段 351 | preScoreStatus := fwk.RunPreScorePlugins(ctx, state, pod, nodes) 352 | if !preScoreStatus.IsSuccess() { 353 | return nil, preScoreStatus.AsError() 354 | } 355 | 356 | // Score 阶段,返回score 357 | scoresMap, scoreStatus := fwk.RunScorePlugins(ctx, state, pod, nodes) 358 | if !scoreStatus.IsSuccess() { 359 | return nil, scoreStatus.AsError() 360 | } 361 | 362 | // result 用来记录所有的得分情况 363 | result := make(framework.NodeScoreList, 0, len(nodes)) 364 | 365 | // ... 简单汇总所有的得分,其中涉及到 framework.Extender 逻辑,这里不展开,后面会有单独的文章 366 | 367 | return result, nil 368 | } 369 | ``` 370 | 371 | 我们继续看一下 `RunPreScorePlugins` 和 `RunScorePlugins` 两个方法: 372 | ```golang 373 | func (f *frameworkImpl) RunPreScorePlugins( 374 | ctx context.Context, 375 | state *framework.CycleState, 376 | pod *v1.Pod, 377 | nodes []*v1.Node, 378 | ) (status *framework.Status) { 379 | // ... 380 | 381 | // 遍历 PreScore Plugins,执行 PreScore 方法 382 | for _, pl := range f.preScorePlugins { 383 | status = f.runPreScorePlugin(ctx, pl, state, pod, nodes) 384 | if !status.IsSuccess() { 385 | return framework.AsStatus(fmt.Errorf("running PreScore plugin %q: %w", pl.Name(), status.AsError())) 386 | } 387 | } 388 | 389 | return nil 390 | } 391 | ``` 392 | 393 | `RunPreScorePlugins` 方法很简单,就是遍历各个 `PreScorePlugin` 进行前置的一些 state 计算,为后面的 `Score` 阶段使用。我们再看 `RunScorePlugins`: 394 | ```golang 395 | func (f *frameworkImpl) RunScorePlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodes []*v1.Node) (ps framework.PluginToNodeScores, status *framework.Status) { 396 | // ... 397 | 398 | // 申明 map 用于存储插件得分 399 | pluginToNodeScores := make(framework.PluginToNodeScores, len(f.scorePlugins)) 400 | for _, pl := range f.scorePlugins { 401 | pluginToNodeScores[pl.Name()] = make(framework.NodeScoreList, len(nodes)) 402 | } 403 | ctx, cancel := context.WithCancel(ctx) 404 | errCh := parallelize.NewErrorChannel() 405 | 406 | // 遍历所有的 scorePlugin 并发执行 Score 方法 407 | f.Parallelizer().Until(ctx, len(nodes), func(index int) { 408 | for _, pl := range f.scorePlugins { 409 | nodeName := nodes[index].Name 410 | s, status := f.runScorePlugin(ctx, pl, state, pod, nodeName) 411 | // 如果 score() 失败,则终止调度周期 412 | if !status.IsSuccess() { 413 | err := fmt.Errorf("plugin %q failed with: %w", pl.Name(), status.AsError()) 414 | errCh.SendErrorWithCancel(err, cancel) 415 | return 416 | } 417 | // 如果 score() 成功,则记录该 plugin 对应的分数 418 | pluginToNodeScores[pl.Name()][index] = framework.NodeScore{ 419 | Name: nodeName, 420 | Score: s, 421 | } 422 | } 423 | }) 424 | 425 | // 如果 score() 发生错误,则终止 426 | if err := errCh.ReceiveError(); err != nil { 427 | return nil, framework.AsStatus(fmt.Errorf("running Score plugins: %w", err)) 428 | } 429 | 430 | // 为了避免得分超过基准,根据最大和最小得分进行标准化操作 431 | f.Parallelizer().Until(ctx, len(f.scorePlugins), func(index int) { 432 | pl := f.scorePlugins[index] 433 | nodeScoreList := pluginToNodeScores[pl.Name()] 434 | if pl.ScoreExtensions() == nil { 435 | return 436 | } 437 | status := f.runScoreExtension(ctx, pl, state, pod, nodeScoreList) 438 | if !status.IsSuccess() { 439 | err := fmt.Errorf("plugin %q failed with: %w", pl.Name(), status.AsError()) 440 | errCh.SendErrorWithCancel(err, cancel) 441 | return 442 | } 443 | }) 444 | 445 | // 同样如果发生错误,则终止 446 | if err := errCh.ReceiveError(); err != nil { 447 | return nil, framework.AsStatus(fmt.Errorf("running Normalize on Score plugins: %w", err)) 448 | } 449 | 450 | // 根据各个插件的权重计算最终得分 451 | f.Parallelizer().Until(ctx, len(f.scorePlugins), func(index int) { 452 | pl := f.scorePlugins[index] 453 | // Score plugins' weight has been checked when they are initialized. 454 | weight := f.scorePluginWeight[pl.Name()] 455 | nodeScoreList := pluginToNodeScores[pl.Name()] 456 | 457 | for i, nodeScore := range nodeScoreList { 458 | // return error if score plugin returns invalid score. 459 | if nodeScore.Score > framework.MaxNodeScore || nodeScore.Score < framework.MinNodeScore { 460 | err := fmt.Errorf("plugin %q returns an invalid score %v, it should in the range of [%v, %v] after normalizing", pl.Name(), nodeScore.Score, framework.MinNodeScore, framework.MaxNodeScore) 461 | errCh.SendErrorWithCancel(err, cancel) 462 | return 463 | } 464 | nodeScoreList[i].Score = nodeScore.Score * int64(weight) 465 | } 466 | }) 467 | if err := errCh.ReceiveError(); err != nil { 468 | return nil, framework.AsStatus(fmt.Errorf("applying score defaultWeights on Score plugins: %w", err)) 469 | } 470 | 471 | // 返回最后的得分情况 472 | return pluginToNodeScores, nil 473 | } 474 | ``` 475 | 优选阶段到这还没有结束,最后还需要通过 `selectHost` 从所有的节点中选出得分最高的节点作为调度节点,方法位于 `pkg/scheduler/generic_scheduler.go:144`,方法比较简单,这里就不展开了。 476 | 477 | ### 3.3 抢占阶段 478 | 调度结束,如果调度失败就会进入到我们的[抢占流程](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/priority-preemption.md),由于之前的文章已经介绍过,这里就不介绍了,点击跳转可以看到关于抢占调度相关文章。 479 | 480 | ### 3.4 Reserve 阶段 481 | 代码位于 `pkg/scheduler/scheduler.go:L507`: 482 | ```golang 483 | assumedPodInfo := podInfo.DeepCopy() 484 | assumedPod := assumedPodInfo.Pod 485 | 486 | // 将选出的节点绑定到 pod.Spec.NodeName 487 | err = sched.assume(assumedPod, scheduleResult.SuggestedHost) 488 | if err != nil { 489 | // 如果出错,则进行错误处理 490 | sched.handleSchedulingFailure(fwk, assumedPodInfo, err, SchedulerError, clearNominatedNode) 491 | return 492 | } 493 | 494 | // 执行 Reserve Plugin 495 | if sts := fwk.RunReservePluginsReserve(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost); !sts.IsSuccess() { 496 | // 如果执行出错,则进行回滚操作,执行 UnreservePlugins,这里需要注意的是 Unreserve 方法应该是幂等的 497 | fwk.RunReservePluginsUnreserve(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) 498 | 499 | // 从 cache 中移除该 pod,我们在 assume 方法中会将 pod 信息存到 cache 中 500 | if forgetErr := sched.SchedulerCache.ForgetPod(assumedPod); forgetErr != nil { 501 | klog.ErrorS(forgetErr, "Scheduler cache ForgetPod failed") 502 | } 503 | // 出错时进行错误处理 504 | sched.handleSchedulingFailure(fwk, assumedPodInfo, sts.AsError(), SchedulerError, clearNominatedNode) 505 | return 506 | } 507 | ``` 508 | 509 | 我们再看一下 `Reserve` 阶段两个方法,`assume` 和 `RunReservePluginsReserve`,`assume` 方法如下: 510 | ```golang 511 | func (sched *Scheduler) assume(assumed *v1.Pod, host string) error { 512 | // 将节点绑定到 pod 上 513 | assumed.Spec.NodeName = host 514 | 515 | // AssumePod 会将 pod 信息存到 SchedulerCache 中 516 | if err := sched.SchedulerCache.AssumePod(assumed); err != nil { 517 | klog.ErrorS(err, "Scheduler cache AssumePod failed") 518 | return err 519 | } 520 | if sched.SchedulingQueue != nil { 521 | // 如果 pod 是抢占提名的 pod,则将它移除,因为已经调度成功了,不需要再抢占 522 | sched.SchedulingQueue.DeleteNominatedPodIfExists(assumed) 523 | } 524 | 525 | return nil 526 | } 527 | ``` 528 | 529 | 再看 `RunReservePluginsReserve` 方法,该方法位于 `pkg/scheduler/framework/runtime/framework.go:L1044`,其实逻辑也很简单: 530 | ```golang 531 | func (f *frameworkImpl) RunReservePluginsReserve(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (status *framework.Status) { 532 | // ... 533 | 534 | // 遍历所有的 reservePlugins 535 | for _, pl := range f.reservePlugins { 536 | // 执行接口方法 runReservePluginReserve 537 | status = f.runReservePluginReserve(ctx, pl, state, pod, nodeName) 538 | // 如果失败,则直接返回失败结果,后续插件不再执行 539 | if !status.IsSuccess() { 540 | err := status.AsError() 541 | return framework.AsStatus(fmt.Errorf("running Reserve plugin %q: %w", pl.Name(), err)) 542 | } 543 | } 544 | return nil 545 | } 546 | ``` 547 | 548 | ### 3.5 Permit 549 | 550 | ```golang 551 | // Run "permit" plugins. 552 | runPermitStatus := fwk.RunPermitPlugins(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) 553 | // 如果返回拒绝或者错误(既不是等待也不是成功),则走 Unreserve 流程 554 | if runPermitStatus.Code() != framework.Wait && !runPermitStatus.IsSuccess() { 555 | // 输出对应错误原因 556 | var reason string 557 | if runPermitStatus.IsUnschedulable() { 558 | reason = v1.PodReasonUnschedulable 559 | } else { 560 | reason = SchedulerError 561 | } 562 | 563 | // 顺序执行 Unreserve plugin 564 | fwk.RunReservePluginsUnreserve(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) 565 | 566 | // 将 pod 从 cache 中移除 567 | if forgetErr := sched.SchedulerCache.ForgetPod(assumedPod); forgetErr != nil { 568 | klog.ErrorS(forgetErr, "Scheduler cache ForgetPod failed") 569 | } 570 | // 最后进行错误处理并返回 571 | sched.handleSchedulingFailure(fwk, assumedPodInfo, runPermitStatus.AsError(), reason, clearNominatedNode) 572 | return 573 | } 574 | 575 | // ... 576 | ``` 577 | Permit 阶段会返回3种状态,成功,拒绝,等待。成功就会走到下一个流程,拒绝刚才我们已经分析过了,那等待呢?我们会在下面的绑定周期中继续介绍。 578 | 579 | ## 4. 绑定周期 580 | 调度周期结束,就来到绑定周期,绑定阶段也有多个扩展点,分别是 `PreBind`,`Bind`,`PostBind`。绑定阶段的代码是通过 `go func()` 异步进行的。在讲这几个扩展点之前,我们先看一下 `Permit` 阶段的等待情况: 581 | 582 | ### 4.1 Permit 收尾阶段 583 | ```golang 584 | bindingCycleCtx, cancel := context.WithCancel(ctx) 585 | defer cancel() 586 | 587 | // 对于 Permit 阶段需要等待的 Pod,一直等待直到返回或者超时 588 | waitOnPermitStatus := fwk.WaitOnPermit(bindingCycleCtx, assumedPod) 589 | if !waitOnPermitStatus.IsSuccess() { 590 | // 输出原因 591 | var reason string 592 | if waitOnPermitStatus.IsUnschedulable() { 593 | reason = v1.PodReasonUnschedulable 594 | } else { 595 | reason = SchedulerError 596 | } 597 | 598 | // 执行 Unreserve Plugins 599 | fwk.RunReservePluginsUnreserve(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) 600 | // 将 pod 从 cache 中移除 601 | if forgetErr := sched.Cache.ForgetPod(assumedPod); forgetErr != nil { 602 | klog.ErrorS(forgetErr, "scheduler cache ForgetPod failed") 603 | } 604 | 605 | // 处理错误情况 606 | sched.handleSchedulingFailure(fwk, assumedPodInfo, waitOnPermitStatus.AsError(), reason, clearNominatedNode) 607 | return 608 | } 609 | ``` 610 | 611 | ### 4.2 PreBind 阶段 612 | 当 Permit 成功,就进入到 `PreBind` 阶段: 613 | ```golang 614 | // 执行 PreBind Plugins 615 | preBindStatus := fwk.RunPreBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) 616 | 617 | // 如果执行失败 618 | if !preBindStatus.IsSuccess() { 619 | // 触发执行 Unreserve Plugins 620 | fwk.RunReservePluginsUnreserve(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) 621 | 622 | // 从 cache 中移除 pod 623 | if forgetErr := sched.Cache.ForgetPod(assumedPod); forgetErr != nil { 624 | klog.ErrorS(forgetErr, "scheduler cache ForgetPod failed") 625 | } 626 | 627 | // 处理错误情况 628 | sched.handleSchedulingFailure(fwk, assumedPodInfo, preBindStatus.AsError(), SchedulerError, clearNominatedNode) 629 | return 630 | } 631 | ``` 632 | 633 | ### 4.3 Bind/PostBind 阶段 634 | 最后执行 `Bind` 和 `PostBInd`,我们一起看一下: 635 | ```golang 636 | // 执行 bind 流程 637 | err := sched.bind(bindingCycleCtx, fwk, assumedPod, scheduleResult.SuggestedHost, state) 638 | if err != nil { 639 | // 如果失败执行 Unreserve Plugins 640 | fwk.RunReservePluginsUnreserve(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) 641 | // 从 cache 中移除 pod 642 | if err := sched.Cache.ForgetPod(assumedPod); err != nil { 643 | klog.ErrorS(err, "scheduler cache ForgetPod failed") 644 | } 645 | // 处理错误情况 646 | sched.handleSchedulingFailure(fwk, assumedPodInfo, fmt.Errorf("binding rejected: %w", err), SchedulerError, clearNominatedNode) 647 | } else { 648 | // 如果 bind 成功,则进行 PostBind 阶段 649 | fwk.RunPostBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) 650 | 651 | // ... 652 | } 653 | ``` 654 | 我们深入的看一下 `bind` 方法: 655 | ```golang 656 | func (sched *Scheduler) bind(ctx context.Context, fwk framework.Framework, assumed *v1.Pod, targetNode string, state *framework.CycleState) (err error) { 657 | defer func() { 658 | // Binding 收尾工作,主要是将 cache 中 pod state 标记为结束绑定,并为它添加一个过期时间,后台会定期清理过期的 pod 659 | sched.finishBinding(fwk, assumed, targetNode, err) 660 | }() 661 | 662 | // extender binding 663 | 664 | // 执行 Bind Plugins,如果成功,则直接返回 665 | bindStatus := fwk.RunBindPlugins(ctx, state, assumed, targetNode) 666 | if bindStatus.IsSuccess() { 667 | return nil 668 | } 669 | // 如果失败,返回错误信息 670 | if bindStatus.Code() == framework.Error { 671 | return bindStatus.AsError() 672 | } 673 | } 674 | ``` 675 | 至此,绑定周期结束,也意味着整个调度流程结束了。 676 | 677 | ## 5. 总结 678 | 我们看到 `scheduling framework` 通过定义好扩展点接口的方式,使得整个调度流程看起来十分清晰,同时也易于扩展,不管是对于开发者或者终端用户而言,都非常容易集成新的功能到 `framework` 中。另外,这也是一个很好的工程示例,值得认真研究和学习。 -------------------------------------------------------------------------------- /scheduler/initialization.md: -------------------------------------------------------------------------------- 1 | # Scheduler 初始化 2 | 3 | Kubernetes Version: v1.22@80056f73a614 4 | Date: 2021.10.17 5 | 6 | ## 1. 开篇 7 | Kube-Scheduler是Kubernetes架构设计中的核心模块,负责整个集群的容器编排和调度策略。得益于Kubernetes整体架构设计的一致性,我们知道所有的组件启动命令都位于 `cmd` 文件夹下各个子模块的 `main` 方法内,`scheduling` 模块的具体代码位置位于 `cmd/kube-scheduler/scheduler.go` ,代码如下: 8 | 9 | func main() { 10 | command := app.NewSchedulerCommand() 11 | code := cli.Run(command) 12 | os.Exit(code) 13 | } 14 | 15 | 16 | 代码很简单,分为3步,分别是注册 Command,运行 Command,退出程序,我们一步一步看。 17 | 18 | ## 2. 注册 Command 19 | `app.NewSchedulerCommand()` 代码位于 `cmd/kube-scheduler/app/server.go`, 我把主要的逻辑梳理下,省略的代码用 `...` 标记。 20 | 21 | func NewSchedulerCommand(registryOptions ...Option) *cobra.Command { 22 | options.NewOptions() 23 | 24 | // ... 25 | 26 | opts.Flags() 27 | 28 | cmd := &cobra.Command{ 29 | // ... 30 | 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | if err := runCommand(cmd, opts, registryOptions...); err != nil { 33 | return err 34 | } 35 | return nil 36 | }, 37 | 38 | // ... 39 | } 40 | 41 | // ... 42 | return cmd 43 | } 44 | 45 | 46 | ### 2.1 `options.NewOptions()` 47 | 主要负责补全默认的启动参数,如默认端口10259,默认 log 配置, 类似于工厂函数。 48 | 49 | ### 2.2 `opts.Flags()` 50 | 主要负责解析命令行参数,这里用到了 `spf13/cobra` 库。举个例子: 51 | 52 | fs.StringVar(&o.ConfigFile, "config", o.ConfigFile, "The path to the configuration file.")` 53 | 54 | `fs` 是 `Struct FlagSet` 实例,定义所有 `flags` 的集合。该行代码第一个参数接受命令行参数变量,第二个参数指定命令行参数名称,第三个表示默认值,第四个参数设置命令行参数提示信息。最后将该 `flag` 加入到 `fs` 集合中。 55 | 56 | ### 2.3 定义 `cobra.Command` 对象 57 | 其中最重要的是 `RunE` 方法的定义,这是实际执行的方法。我们看一下 `Command` 数据结构,他有很多字段,我们挑几个常用的看一下,代码位于 `vendor/github.com/spf13/cobra/command.go`: 58 | 59 | type Command struct { 60 | PersistentPreRun func(cmd *Command, args []string) 61 | PersistentPreRunE func(cmd *Command, args []string) error 62 | PreRun func(cmd *Command, args []string) 63 | PreRunE func(cmd *Command, args []string) error 64 | Run func(cmd *Command, args []string) 65 | RunE func(cmd *Command, args []string) error 66 | PostRun func(cmd *Command, args []string) 67 | PostRunE func(cmd *Command, args []string) error 68 | PersistentPostRun func(cmd *Command, args []string) 69 | PersistentPostRunE func(cmd *Command, args []string) error 70 | 71 | 72 | // SilenceErrors is an option to quiet errors down stream. 73 | SilenceErrors bool 74 | // SilenceUsage is an option to silence usage when an error occurs. 75 | SilenceUsage bool 76 | } 77 | 78 | 在数据结构中,我们看到很多 `Run` 方法和 `RunE` 方法,他们的区别在于是否返回 `error`,而这些 `Run` 方法有一个调用顺序的区别,`PersistentPreRun -> PreRun -> Run -> PostRun ->PersistentPostRun`, `RunE` 方法也是一样的顺序。 79 | 80 | `SilenceErrors` 和 `SilenceUsage` 用来静默是否由 `cobra` 输出错误和使用信息,默认情况下,如果解析 `command` 出现错误,会输出错误和使用信息,代码同样位于 `vendor/github.com/spf13/cobra/command.go`: 81 | 82 | err = cmd.execute(flags) 83 | if err != nil { 84 | if err == flag.ErrHelp { 85 | cmd.HelpFunc()(cmd, args) 86 | return cmd, nil 87 | } 88 | 89 | if !cmd.SilenceErrors && !c.SilenceErrors { 90 | c.PrintErrln("Error:", err.Error()) 91 | } 92 | 93 | if !cmd.SilenceUsage && !c.SilenceUsage { 94 | c.Println(cmd.UsageString()) 95 | } 96 | } 97 | 98 | ## 3. 运行 Command 99 | `cli.Run(command)` 方法会执行 `command` 命令,其实主要是执行 上面定义的 `RunE` 方法,我们看一下: 100 | 101 | RunE: func(cmd *cobra.Command, args []string) error { 102 | if err := opts.Complete(&namedFlagSets); err != nil { 103 | return err 104 | } 105 | if err := runCommand(cmd, opts, registryOptions...); err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | 112 | ### 3.1. `opts.Complete()` 113 | 用于补全初始化所需的配置,并作为参数传入 `runCommand` 方法 114 | 115 | ### 3.2. `runCommand` 116 | 包含了真正运行 `kube-scheduler` 的代码: 117 | 118 | func runCommand(cmd *cobra.Command, opts *options.Options, registryOptions ...Option) error { 119 | ... 120 | 121 | ctx, cancel := context.WithCancel(context.Background()) 122 | defer cancel() 123 | 124 | go func() { 125 | stopCh := server.SetupSignalHandler() 126 | <-stopCh 127 | cancel() 128 | }() 129 | 130 | cc, sched, err := Setup(ctx, opts, registryOptions...) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | return Run(ctx, cc, sched) 136 | } 137 | 138 | `Setup()` 方法主要用于初始化 `Scheduler` 实例,`Run()` 则根据配置运行 `scheduler`,具体逻辑会在后面的系列文章中说明,不作为本次文章重点。 139 | ## 4. 退出程序 140 | `main` 方法中 `cli.Run` 命令会根据执行情况返回对应的错误码,成功返回0,错误返回非0。调用 `os.Exit(code)` 退出程序。 141 | 142 | ## 5. 其他 143 | ### 5.1 如何自定义 `flag` 启动 `kube-scheduler` 144 | 假设我们使用 `kubeadm` 来安装 `kubernetes cluster`,登陆到 `master` 节点,我们可以在 `/etc/kubernetes/manifests` 下发现几个 `yaml` 文件。 145 | 146 | etcd.yaml 147 | kube-apiserver.yaml 148 | kube-controller-manager.yaml 149 | kube-scheduler.yaml 150 | 151 | 这几个组件都是以 `static pod` 的方式运行,所谓 static pod ,他们都是由 kubelet 守护进程直接管理,不需要 API 服务器 监管。直接修改 `kube-scheduler.yaml` 文件,kubelet 会监听到文件变动直接重启 `static pod`, 配置命令如下: 152 | 153 | spec: 154 | containers: 155 | - command: 156 | - kube-scheduler 157 | - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf 158 | - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf 159 | - --bind-address=127.0.0.1 160 | - --kubeconfig=/etc/kubernetes/scheduler.conf 161 | - --leader-elect=true 162 | - --port=0 163 | 164 | ### 5.2 如何编译 `kube-scheduler` 二进制文件 165 | `kubernetes` 根项目直接执行 `build/run.sh make kube-scheduler` 166 | -------------------------------------------------------------------------------- /scheduler/plugin.md: -------------------------------------------------------------------------------- 1 | # Scheduler 插件 2 | 3 | Kubernetes Version: v1.22@80056f73a614 4 | Date: 2021.10.17 5 | 6 | ## 1. 开篇 7 | Kube-Scheduler是Kubernetes架构设计中的核心模块,负责整个集群的容器编排和调度策略。得益于Kubernetes整体架构设计的一致性,我们知道所有的组件启动命令都位于 `cmd` 文件夹下各个子模块的 `main` 方法内,`scheduling` 模块的具体代码位置位于 `cmd/kube-scheduler/scheduler.go` ,代码如下: -------------------------------------------------------------------------------- /scheduler/priority-preemption.md: -------------------------------------------------------------------------------- 1 | # scheduler 优先级和抢占 2 | 3 | Kubernetes Version: v1.24@f1da8cd3e20 4 | Date: 2022.01.15 5 | 6 | ## 1. 开篇 7 | Pod 可以有 优先级。 优先级表示一个 Pod 相对于其他 Pod 的重要性。 如果一个 Pod 无法被调度,调度程序会尝试抢占(驱逐)较低优先级的 Pod, 以使悬决 Pod 可以被调度。这就是 `kubernetes` 优先级于抢占机制。今天我们一起从源码角度看一下它是如何实现的。 8 | 9 | ## 2. 如何设置 `pod` 优先级 10 | 我们一般通过 [`PriorityClass`](https://kubernetes.io/zh/docs/concepts/scheduling-eviction/pod-priority-preemption/) 来设置 `pod` 优先级的方法,它是通过 `AdmissionController` 来实现的,主要代码逻辑位于 `plugin/pkg/admission/priority` 的 `Admit()` 方法: 11 | 12 | ```golang 13 | func (p *Plugin) admitPod(a admission.Attributes) error { 14 | operation := a.GetOperation() 15 | pod, ok := a.GetObject().(*api.Pod) 16 | 17 | // 更新操作 18 | if operation == admission.Update { 19 | oldPod, ok := a.GetOldObject().(*api.Pod) 20 | 21 | // 如果原pod有优先级,则需要保留 22 | if pod.Spec.Priority == nil && oldPod.Spec.Priority != nil { 23 | pod.Spec.Priority = oldPod.Spec.Priority 24 | } 25 | 26 | // 同样保留抢占策略 27 | if pod.Spec.PreemptionPolicy == nil && oldPod.Spec.PreemptionPolicy != nil { 28 | pod.Spec.PreemptionPolicy = oldPod.Spec.PreemptionPolicy 29 | } 30 | return nil 31 | } 32 | 33 | // 创建操作 34 | if operation == admission.Create { 35 | var priority int32 36 | var preemptionPolicy *apiv1.PreemptionPolicy 37 | 38 | // 如果没有配置优先级,则配置默认优先级 39 | if len(pod.Spec.PriorityClassName) == 0 { 40 | var err error 41 | var pcName string 42 | pcName, priority, preemptionPolicy, err = p.getDefaultPriority() 43 | pod.Spec.PriorityClassName = pcName 44 | } else { 45 | // 如果配置了优先级,则进行赋值操作 46 | pc, err := p.lister.Get(pod.Spec.PriorityClassName) 47 | 48 | priority = pc.Value 49 | preemptionPolicy = pc.PreemptionPolicy 50 | } 51 | 52 | // 设置pod优先级 53 | pod.Spec.Priority = &priority 54 | 55 | // 配置抢占策略 56 | if p.nonPreemptingPriority { 57 | var corePolicy core.PreemptionPolicy 58 | if preemptionPolicy != nil { 59 | corePolicy = core.PreemptionPolicy(*preemptionPolicy) 60 | 61 | // 设置pod抢占策略 62 | pod.Spec.PreemptionPolicy = &corePolicy 63 | } 64 | } 65 | } 66 | return nil 67 | } 68 | ``` 69 | 70 | 通过 `admitPod()` 方法,我们就完成对 `pod` 优先级和抢占策略的设置。 71 | 72 | ## 3. 优先级调度 73 | `pod` 完成优先级设置,之后会进入到调度流程,调度队列会根据优先级进行排序,我们在[优先级调度](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/queue.md#2-scheduler-%E4%B8%89%E7%BA%A7%E9%98%9F%E5%88%97)中讲 `activeQ` 的时候已经介绍过,`activeQ` 是一个优先级队列,会根据 `pod.Spec.Priority` 值进行排序,`Priority` 值越大,优先级越高,排在队列的越前面,所以会被优先调度。 74 | 75 | ## 4. 抢占机制 76 | `Preemption` 现在是以一个插件的形式工作在 `PostFilter` 这个扩展点,这个插件名叫 `DefaultPreemption`,是一个默认的内置插件,代码位于 `pkg/scheduler/framework/plugins/defaultpreemption:default_preemption.go`。 77 | 78 | 我们先看一下 `PostFilter` 扩展点代码逻辑(由于本篇文章着重介绍抢占,其他扩展点逻辑不展开): 79 | ```golang 80 | func (sched *Scheduler) scheduleOne(ctx context.Context) { 81 | // ... 82 | 83 | // 调度逻辑 84 | scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, sched.Extenders, fwk, state, pod) 85 | 86 | // 如果找不到合适的节点,就进行抢占调度 87 | if err != nil { 88 | // nominatingInfo 包括候选节点相关信息 89 | var nominatingInfo *framework.NominatingInfo 90 | if fitError, ok := err.(*framework.FitError); ok { 91 | // ... 92 | // 执行抢占逻辑 93 | result, status := fwk.RunPostFilterPlugins(ctx, state, pod, fitError.Diagnosis.NodeToStatusMap) 94 | 95 | // 如果抢占成功,则将候选节点相关信息赋值给 nominatingInfo,如果失败,则直接返回 96 | if status.Code() == framework.Error { 97 | klog.ErrorS(nil, "Status after running PostFilter plugins for pod", "pod", klog.KObj(pod), "status", status) 98 | } else { 99 | klog.V(5).InfoS("Status after running PostFilter plugins for pod", "pod", klog.KObj(pod), "status", status) 100 | } 101 | if result != nil { 102 | nominatingInfo = result.NominatingInfo 103 | } 104 | } 105 | 106 | // 处理所有调度失败后续问题,包括抢占调度,后面我们还会介绍。 107 | sched.handleSchedulingFailure(fwk, podInfo, err, v1.PodReasonUnschedulable, nominatingInfo) 108 | return 109 | } 110 | 111 | // ...省略其他调度逻辑 112 | } 113 | ``` 114 | 115 | ### 4.1 抢占流程之抢 116 | 抢占逻辑位于代码 `RunPostFilterPlugins()` 方法中,它会遍历每个 `Plugin`,并调用他们的 `PostFilter()` 方法,我们一起看一下默认 `PostFilter` 扩展点插件 `DefaultPreemption` 的方法: 117 | ```golang 118 | func (pl *DefaultPreemption) PostFilter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, m framework.NodeToStatusMap) (*framework.PostFilterResult, *framework.Status) { 119 | // 构造 Evaluator 120 | pe := preemption.Evaluator{ 121 | PluginName: names.DefaultPreemption, 122 | Handler: pl.fh, 123 | PodLister: pl.podLister, 124 | PdbLister: pl.pdbLister, 125 | State: state, 126 | Interface: pl, 127 | } 128 | 129 | // 调用抢占逻辑 130 | return pe.Preempt(ctx, pod, m) 131 | } 132 | ``` 133 | 134 | 我们继续看 `pe.Preempt()` 方法: 135 | ```golang 136 | func (ev *Evaluator) Preempt(ctx context.Context, pod *v1.Pod, m framework.NodeToStatusMap) (*framework.PostFilterResult, *framework.Status) { 137 | 138 | podNamespace, podName := pod.Namespace, pod.Name 139 | pod, err := ev.PodLister.Pods(pod.Namespace).Get(pod.Name) 140 | 141 | // 检查pod是否具备抢占条件,如果 pod 已经被提名过一次且提名节点上的 pod 正在终止,此时应该拒绝抢占。 142 | if !ev.PodEligibleToPreemptOthers(pod, m[pod.Status.NominatedNodeName]) { 143 | return nil, framework.NewStatus(framework.Unschedulable) 144 | } 145 | 146 | // 找到所有的候选节点 147 | candidates, nodeToStatusMap, err := ev.findCandidates(ctx, pod, m) 148 | if err != nil && len(candidates) == 0 { 149 | return nil, framework.AsStatus(err) 150 | } 151 | 152 | // 如果没有候选节点,则返回 153 | if len(candidates) == 0 { 154 | return nil, framework.NewStatus(framework.Unschedulable, fitError.Error()) 155 | } 156 | 157 | // 找出最合适的那个节点 158 | bestCandidate := ev.SelectCandidate(candidates) 159 | if bestCandidate == nil || len(bestCandidate.Name()) == 0 { 160 | return nil, framework.NewStatus(framework.Unschedulable) 161 | } 162 | 163 | // 抢占准备工作 164 | if status := ev.prepareCandidate(bestCandidate, pod, ev.PluginName); !status.IsSuccess() { 165 | return nil, status 166 | } 167 | 168 | // 返回抢占节点,结束抢占流程 169 | return &framework.PostFilterResult{NominatedNodeName: bestCandidate.Name()}, framework.NewStatus(framework.Success) 170 | } 171 | ``` 172 | 173 | 这就是整个抢占机制的大致流程,下面我们就其中的 `findCandidates()`,`selectCandidate()` 和 `prepareCandidate()` 这三个最核心的方法分别进行讲解。 174 | 175 | #### 4.1.1 findCandidates 176 | `findCandidates()` 顾名思义就是寻找所有可能的候选节点,那它是基于何种规则进行删选呢,我们一起看一下: 177 | ```golang 178 | func (ev *Evaluator) findCandidates(ctx context.Context, pod *v1.Pod, m framework.NodeToStatusMap) ([]Candidate, framework.NodeToStatusMap, error) { 179 | // 获取所有快照 nodes 180 | allNodes, err := ev.Handler.SnapshotSharedLister().NodeInfos().List() 181 | 182 | // 删选掉即使抢占也无法调度的节点 183 | potentialNodes, unschedulableNodeStatus := nodesWherePreemptionMightHelp(allNodes, m) 184 | if len(potentialNodes) == 0 { 185 | return nil, unschedulableNodeStatus, nil 186 | } 187 | 188 | // 获取pdb资源 189 | pdbs, err := getPodDisruptionBudgets(ev.PdbLister) 190 | 191 | // 获取一个偏移量和候选的节点数量 192 | offset, numCandidates := ev.GetOffsetAndNumCandidates(int32(len(potentialNodes))) 193 | 194 | // 模拟抢占事件,返回符合条件的候选节点及不符合条件的节点调度状态 195 | candidates, nodeStatuses, err := ev.DryRunPreemption(ctx, pod, potentialNodes, pdbs, offset, numCandidates) 196 | 197 | // nodeStatuses 负责存储所有不符合条件的节点调度状态,如果没有找到符合要求的节点,则输出失败记录 198 | for node, nodeStatus := range unschedulableNodeStatus { 199 | nodeStatuses[node] = nodeStatus 200 | } 201 | 202 | // 返回结果 203 | return candidates, nodeStatuses, err 204 | } 205 | ``` 206 | 207 | 208 | findCandidates() 方法中最重要的逻辑位于 DryRunPreemption 中,我们一起看一下它的逻辑: 209 | ```golang 210 | func (ev *Evaluator) DryRunPreemption(...) ([]Candidate, framework.NodeToStatusMap, error) { 211 | // ... 212 | 213 | checkNode := func(i int) { 214 | nodeInfoCopy := potentialNodes[(int(offset)+i)%len(potentialNodes)].Clone() 215 | stateCopy := ev.State.Clone() 216 | 217 | // SelectVictimsOnNode方法逻辑主要有2段: 218 | // 1. 模拟移除所有优先级低的pod,判断是否满足抢占要求 219 | // 2. 如果满足抢占要求,再模拟尽可能少的移除pod 220 | // 3. 返回的 pods 按照 priority 优先级降序完成了排序 221 | // pods 表示需要驱逐的pod,numPDBViolations 表示违反PDB约束且需要被驱逐的pod数 222 | pods, numPDBViolations, status := ev.SelectVictimsOnNode(ctx, stateCopy, pod, nodeInfoCopy, pdbs) 223 | 224 | // 删选 pod 状态成功并且需要被驱逐的 pod 数不为0 225 | if status.IsSuccess() && len(pods) != 0 { 226 | victims := extenderv1.Victims{ 227 | Pods: pods, 228 | NumPDBViolations: int64(numPDBViolations), 229 | } 230 | c := &candidate{ 231 | victims: &victims, 232 | name: nodeInfoCopy.Node().Name, 233 | } 234 | // 如果没有违反 PDB 约束,就加入到 nonViolatingCandidates 候选节点列表 235 | // 否则加入到 violatingCandidates 候选节点列表 236 | if numPDBViolations == 0 { 237 | nonViolatingCandidates.add(c) 238 | } else { 239 | violatingCandidates.add(c) 240 | } 241 | nvcSize, vcSize := nonViolatingCandidates.size(), violatingCandidates.size() 242 | // 找到符合候选节点数量的节点就终止,在超大规模集群下可以提高性能 243 | if nvcSize > 0 && nvcSize+vcSize >= numCandidates { 244 | cancel() 245 | } 246 | return 247 | } 248 | 249 | // 没有需要被驱逐的 pod, 则表明该节点在抢占机制中无法发挥作用 250 | if status.IsSuccess() && len(pods) == 0 { 251 | status = framework.AsStatus(fmt.Errorf("expected at least one victim pod on node %q", nodeInfoCopy.Node().Name)) 252 | } 253 | 254 | // 对于出错的节点,记录错误原因 255 | statusesLock.Lock() 256 | if status.Code() == framework.Error { 257 | errs = append(errs, status.AsError()) 258 | } 259 | nodeStatuses[nodeInfoCopy.Node().Name] = status 260 | statusesLock.Unlock() 261 | } 262 | 263 | // 异步执行并返回符合条件的节点切片 264 | fh.Parallelizer().Until(parallelCtx, len(potentialNodes), checkNode) 265 | return append(nonViolatingCandidates.get(), violatingCandidates.get()...), nodeStatuses, utilerrors.NewAggregate(errs) 266 | } 267 | ``` 268 | 269 | #### 4.1.2 selectCandidate 270 | 找到所有的候选节点后,我们需要选择其中的一个节点作为最终的候选节点,方法 `selectCandidate()` 代码如下: 271 | ```golang 272 | func (ev *Evaluator) SelectCandidate(candidates []Candidate) Candidate { 273 | // 如果没有候选节点,则返回nil 274 | if len(candidates) == 0 { 275 | return nil 276 | } 277 | // 如果只有一个,则直接返回 278 | if len(candidates) == 1 { 279 | return candidates[0] 280 | } 281 | 282 | // 主要是进行转化,变成 map[nodeName]victims 这种结构 283 | victimsMap := ev.CandidatesToVictimsMap(candidates) 284 | 285 | // 从所有的节点中选出最终候选节点,PDB 和 victims 数量都是参考维度 286 | candidateNode := pickOneNodeForPreemption(victimsMap) 287 | 288 | // 如果从 victimsMap 中找到对应的节点,就返回 289 | if victims := victimsMap[candidateNode]; victims != nil { 290 | return &candidate{ 291 | victims: victims, 292 | name: candidateNode, 293 | } 294 | } 295 | 296 | // 否则就走这段逻辑,我理解这里是做了一层保护作用,理论上不应该到这里 297 | klog.ErrorS(errors.New("no candidate selected"), "Should not reach here", "candidates", candidates) 298 | // To not break the whole flow, return the first candidate. 299 | return candidates[0] 300 | } 301 | ``` 302 | 303 | #### 4.1.3 prepareCandidate 304 | 找到对应的抢占节点后就来到了最后我们的准备工作 305 | ```golang 306 | func (ev *Evaluator) prepareCandidate(c Candidate, pod *v1.Pod, pluginName string) *framework.Status { 307 | // 获取 ClientSet 308 | fh := ev.Handler 309 | cs := ev.Handler.ClientSet() 310 | for _, victim := range c.Victims().Pods { 311 | // 如果 pod 在队列中处于等待状态,就拒绝该轮调度,否则就直接删除 312 | if waitingPod := fh.GetWaitingPod(victim.UID); waitingPod != nil { 313 | waitingPod.Reject(pluginName, "preempted") 314 | } else if err := util.DeletePod(cs, victim); err != nil { 315 | return framework.AsStatus(err) 316 | } 317 | } 318 | 319 | // 由于同一个节点上可能同时存在更低优先级的 pod 也处于抢占周期,此时他们是不应该抢占成功的, 320 | // 因为即使抢占成功了也会又被驱逐,所以此时清理这些低优先级的抢占 pod 321 | nominatedPods := getLowerPriorityNominatedPods(fh, pod, c.Name()) 322 | if err := util.ClearNominatedNodeName(cs, nominatedPods...); err != nil { 323 | klog.ErrorS(err, "Cannot clear 'NominatedNodeName' field") 324 | // We do not return as this error is not critical. 325 | } 326 | 327 | return nil 328 | } 329 | ``` 330 | 331 | ### 4.2 抢占流程之占 332 | 至此,我们的抢逻辑执行完毕,驱逐了优先级更低且必要的 pod,那我们该如何调度呢。我们上面提到 `handleSchedulingFailure` 方法,其中的玄机就在该方法中: 333 | ```golang 334 | func (sched *Scheduler) handleSchedulingFailure(fwk framework.Framework, podInfo *framework.QueuedPodInfo, err error, reason string, nominatingInfo *framework.NominatingInfo) { 335 | // 调度失败的 pod 可以重新入列 336 | sched.Error(podInfo, err) 337 | 338 | // 把抢占节点及 pod 信息存到 nominator 中 339 | sched.SchedulingQueue.AddNominatedPod(podInfo.PodInfo, nominatingInfo) 340 | 341 | pod := podInfo.Pod 342 | 343 | // 更新 pod 信息 344 | if err := updatePod(sched.client, pod, &v1.PodCondition{ 345 | Type: v1.PodScheduled, 346 | Status: v1.ConditionFalse, 347 | Reason: reason, 348 | Message: err.Error(), 349 | }, nominatingInfo); err != nil { 350 | klog.ErrorS(err, "Error updating pod", "pod", klog.KObj(pod)) 351 | } 352 | } 353 | ``` 354 | `handleSchedulingFailure` 方法会处理所有调度失败后续逻辑,其中 `sched.Error` 方法,他的原方法是 `MakeDefaultErrorFunc` 方法,该方法位于 `pkg/scheduler/factory.go` 文件,篇幅有限,我们提一下这个方法有一个很重要的逻辑是会将调度失败的 pod 重新放入调度队列中(后面章节我们会详细介绍该方法),这样调度失败的 pod 会进行重新调度。 355 | 356 | `updatePod` 方法也很重要,他会更新 `pod.Status` 的 `NominatedNodeName` 为当前候选节点的 name,这样,在下一次调度的时候可以更快的进行调度,我们回过头去看 `genericScheduler` 的 `findNodesThatFitPod` 方法,其中有一段逻辑: 357 | ```golang 358 | 359 | func (g *genericScheduler) findNodesThatFitPod(...) ([]*v1.Node, framework.Diagnosis, error) { 360 | // ... 361 | 362 | // 如果 `pod.Status.NominatedNodeName` 有值,表明这个 pod 经历过抢占流程,这里直接对候选节点进行直接甄别,而不需要重新走一次遍历所有节点的流程,这里的 `feasibleNodes` 看起来有很多节点,其实只有一个候选节点。如果候选节点不符合要求,`feasibleNodes` 则为空 363 | if len(pod.Status.NominatedNodeName) > 0 { 364 | feasibleNodes, err := g.evaluateNominatedNode(ctx, extenders, pod, fwk, state, diagnosis) 365 | if err != nil { 366 | klog.ErrorS(err, "Evaluation failed on nominated node", "pod", klog.KObj(pod), "node", pod.Status.NominatedNodeName) 367 | 368 | // 如果候选节点符合要求,则直接返回,不需要再去找其他节点了。 369 | if len(feasibleNodes) != 0 { 370 | return feasibleNodes, diagnosis, nil 371 | } 372 | } 373 | 374 | // ... 375 | ``` 376 | 至此,整个抢占机制算是走通了。 377 | 378 | ## `nominator` 提名器 379 | 我们在 [`prepareCandidate`](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/priority-preemption.md#413-preparecandidate)章节遇到过一个方法叫 `getLowerPriorityNominatedPods`,他可以获取某一个节点上所有同样处于抢占周期且优先级较低的 pod,他是如何实现的,其实是通过一个叫 `NominatedPodsForNode` 的方法,说到这,就不得不提一个结构体变量,叫 `nominator`,我们看一下: 380 | ```golang 381 | type nominator struct { 382 | // podLister is used to verify if the given pod is alive. 383 | podLister listersv1.PodLister 384 | // nominatedPods is a map keyed by a node name and the value is a list of 385 | // pods which are nominated to run on the node. These are pods which can be in 386 | // the activeQ or unschedulableQ. 387 | nominatedPods map[string][]*framework.PodInfo 388 | // nominatedPodToNode is map keyed by a Pod UID to the node name where it is 389 | // nominated. 390 | nominatedPodToNode map[types.UID]string 391 | 392 | sync.RWMutex 393 | } 394 | ``` 395 | 这个数据结构就是用来存储所有的抢占结果,它实现了 `PodNominator` 接口: 396 | ```golang 397 | type PodNominator interface { 398 | // AddNominatedPod adds the given pod to the nominator or 399 | // updates it if it already exists. 400 | AddNominatedPod(pod *PodInfo, nominatingInfo *NominatingInfo) 401 | // DeleteNominatedPodIfExists deletes nominatedPod from internal cache. It's a no-op if it doesn't exist. 402 | DeleteNominatedPodIfExists(pod *v1.Pod) 403 | // UpdateNominatedPod updates the with . 404 | UpdateNominatedPod(oldPod *v1.Pod, newPodInfo *PodInfo) 405 | // NominatedPodsForNode returns nominatedPods on the given node. 406 | NominatedPodsForNode(nodeName string) []*PodInfo 407 | } 408 | ``` 409 | 这个接口实现了对 `nominator` 的增删改操作,这样,当有需要时我们就可以通过 `NominatedPodsForNode` 获取到 node 节点所有的 nominatedPod。 410 | 411 | ## 总结 412 | 以上就是整个抢占调度的全部逻辑,如果有说的不对不好的,欢迎指正! -------------------------------------------------------------------------------- /scheduler/queue.md: -------------------------------------------------------------------------------- 1 | # Scheduler 调度队列 2 | 3 | Kubernetes Version: v1.22@3b76c758317b 4 | Date: 2021.11.08 5 | 6 | ## 1. Scheduler 优先级队列 7 | 在之前的文章 [Kube-Scheduler 启动](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/start-scheduler.md)中,我们初始化了一个优先级队列,当时由于篇幅有限,我们只是简单的提了一下。这篇文章,我们将对整个队列进行全方位的解读,包括入列出列操作,三级队列彼此之间如何协作等等。 8 | 9 | ### 1.1 初始化优先级队列 10 | 11 | podQueue := internalqueue.NewSchedulingQueue( 12 | lessFn, 13 | c.informerFactory, 14 | internalqueue.WithPodInitialBackoffDuration(time.Duration(c.podInitialBackoffSeconds)*time.Second), 15 | internalqueue.WithPodMaxBackoffDuration(time.Duration(c.podMaxBackoffSeconds)*time.Second), 16 | internalqueue.WithPodNominator(nominator), 17 | internalqueue.WithClusterEventMap(c.clusterEventMap), 18 | ) 19 | 20 | 我们先看一下 `NewSchedulingQueue` 的几个入参: 21 | 22 | 第一个入参是 `lessFn`, 它是一个用来对 `scheduling queue` 中的 `pods` 进行排序的方法,定义如下: 23 | 24 | type LessFunc func(podInfo1, podInfo2 *QueuedPodInfo) bool 25 | 26 | 它的实际方法位于 `pkg/scheduler/framework/plugins/queuesort/priority_sort.go`,通过 `NewInTreeRegistry()` 方法完成注册,我们一起来看一下这个 `Less()` 方法: 27 | 28 | func (pl *PrioritySort) Less(pInfo1, pInfo2 *framework.QueuedPodInfo) bool { 29 | p1 := corev1helpers.PodPriority(pInfo1.Pod) 30 | p2 := corev1helpers.PodPriority(pInfo2.Pod) 31 | return (p1 > p2) || (p1 == p2 && pInfo1.Timestamp.Before(pInfo2.Timestamp)) 32 | } 33 | 34 | `PodPriority()` 方法获取 `pod.Spec.Priority` 值,如果 `priority` 是 `nil` 则返回0。`less()` 方法其实就是对两个 `pod` 的优先级进行比较,如果相等则比较他们加入队列的时间戳,创建时间越早的 `pod` 优先级越高。至于这个方法什么时候会被调用,下面我们会讲到。 35 | 36 | 第二个参数是之前定义过的 `SharedInformerFactory`,用来 `watch` `pod` 事件,并参与后面的调度。 37 | 38 | 第三个参数是 `podInitialBackoffDuration`,表示初始化等待时间,默认1s,第四个参数是 `WithPodMaxBackoffDuration`,表示最大等待时间,默认10s。这两个参数主要用于计算 `backupDuration`,所谓 `backupDuration` 是指带有指数回避策略的等待重试时间,当 `pod` 调度失败,会进行重试,为了避免频繁进行失败重试,所以每次重试间隔时间会指数级递增,最长不超过 `maxBackoffDuration`。 39 | 40 | 第五个参数是 `PodNominator`,主要用于抢占调度流程,这里不做过多介绍。 41 | 42 | 第六个参数是 `clusterEventMap`,主要是和 `informer` 交互,绑定具体的事件和对应的处理方法,我们会在 `informer` 章节中详细的介绍。 43 | 44 | 至此,优先级队列初始化完成。 45 | 46 | ### 1.2 启动优先级队列 47 | 启动优先级队列在之前的文章中我们已经提到过,通过调用 `Run()` 方法启动了两个后台循环队列,一个是 `backoffQ` 队列,还有一个 `unschedulableQ` 队列,这两个循环队列主要是存储调度失败的和无法调度的 `pod` 。 48 | 49 | func (p *PriorityQueue) Run() { 50 | go wait.Until(p.flushBackoffQCompleted, 1.0*time.Second, p.stop) 51 | go wait.Until(p.flushUnschedulableQLeftover, 30*time.Second, p.stop) 52 | } 53 | 54 | #### 1.2.1 `flushBackoffQCompleted` 55 | 我们先看 `flushBackoffQCompleted`,每隔1s执行一次该方法,实现如下: 56 | 57 | func (p *PriorityQueue) flushBackoffQCompleted() { 58 | // 上锁 59 | p.lock.Lock() 60 | defer p.lock.Unlock() 61 | 62 | for { 63 | // 获取 heap 堆顶元素,如果不存在,表示队列为空,返回。平均时间复杂度 O(1) 64 | rawPodInfo := p.podBackoffQ.Peek() 65 | if rawPodInfo == nil { 66 | return 67 | } 68 | 69 | pod := rawPodInfo.(*framework.QueuedPodInfo).Pod 70 | boTime := p.getBackoffTime(rawPodInfo.(*framework.QueuedPodInfo)) 71 | 72 | // 如果还没到重试时间,则继续等待,直接返回 73 | if boTime.After(p.clock.Now()) { 74 | return 75 | } 76 | 77 | // 否则,就弹出一个 pod 78 | _, err := p.podBackoffQ.Pop() 79 | if err != nil { 80 | return 81 | } 82 | 83 | // 加入 activeQ,并唤醒接收方 goroutine 84 | p.activeQ.Add(rawPodInfo) 85 | defer p.cond.Broadcast() 86 | } 87 | } 88 | 89 | 90 | #### 1.2.2 `flushUnschedulableQLeftover` 91 | `flushUnschedulableQLeftover` 方法逻辑更简单,每隔30s执行一次。 92 | 93 | func (p *PriorityQueue) flushUnschedulableQLeftover() { 94 | // 上锁 95 | p.lock.Lock() 96 | defer p.lock.Unlock() 97 | 98 | var podsToMove []*framework.QueuedPodInfo 99 | currentTime := p.clock.Now() 100 | for _, pInfo := range p.unschedulableQ.podInfoMap { 101 | // 获取 pod 入列时间 102 | lastScheduleTime := pInfo.Timestamp 103 | 104 | // 如果在 `unschedulableQ` 中存储时间超过60s,则将它转移到 `activeQ` 或者 `podBackoffQ` 105 | if currentTime.Sub(lastScheduleTime) > unschedulableQTimeInterval { 106 | podsToMove = append(podsToMove, pInfo) 107 | } 108 | } 109 | 110 | if len(podsToMove) > 0 { 111 | // 转移 112 | p.movePodsToActiveOrBackoffQueue(podsToMove, UnschedulableTimeout) 113 | } 114 | } 115 | 116 | ## 2. `Scheduler` 三级队列 117 | `scheduler` 的三级队列定义在优先级队列的数据结构中,我们一起看一下: 118 | 119 | type PriorityQueue struct { 120 | activeQ *heap.Heap 121 | podBackoffQ *heap.Heap 122 | unschedulableQ *UnschedulablePodsMap 123 | schedulingCycle int64 124 | } 125 | 126 | 1. `activeQ` 127 | 128 | 存储所有等待调度的 `pod`,它是一个 `heap` 数据结构,准确的说是一个大顶堆,插入和更新队列平均时间复杂度是 `O(logN)`,获取队列元素的平均时间复杂度是 `O(1)`。 129 | 130 | `heap` 堆栈在进行构建优先级队列的时候有一个很重要的方法就是如何比较优先级,那这个方法是怎么定义的呢,其实就是我们在[初始化优先级队列](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/queue.md#11-%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BC%98%E5%85%88%E7%BA%A7%E9%98%9F%E5%88%97)中所讲的 `lessFn` 方法。 131 | 132 | 2. `podBackoffQ` 133 | 134 | 存储所有调度失败的 `pod`,并且带有指数回避策略。同样,它也是一个 `heap` 堆栈。那 `podBackoffQ` 中元素的优先级是如何进行比较的呢,方法如下: 135 | 136 | func (p *priorityqueue) podscomparebackoffcompleted(podinfo1, podinfo2 interface{}) bool { 137 | pinfo1 := podinfo1.(*framework.queuedpodinfo) 138 | pinfo2 := podinfo2.(*framework.queuedpodinfo) 139 | bo1 := p.getbackofftime(pinfo1) 140 | bo2 := p.getbackofftime(pinfo2) 141 | return bo1.before(bo2) 142 | } 143 | 144 | 方法很简单,谁距离下一次等待调度的时间越短,谁优先级越高。 145 | 146 | 147 | 3. `unschedulableQ` 148 | 149 | 存储所有因为资源不够而排队的 `pod`,它是一个包装了 `map` 的数据结构 150 | 151 | type UnschedulablePodsMap struct { 152 | // pod 实际保存在这个 map 中 153 | podInfoMap map[string]*framework.QueuedPodInfo 154 | keyFunc func(*v1.Pod) string 155 | metricRecorder metrics.MetricRecorder 156 | } 157 | 4. `schedulingCycle` 158 | 159 | 逻辑时钟,表示调度周期。每次从优先级队列中 `Pop()` 一个元素出来,就累加。 160 | ## 3. 队列工作流程 161 | ### 3.1 入列 (activeQ) 162 | 优先级队列入列(这里主要指 `activeQ`)操作是通过 `Informer` 实现的,代码位于 `pkg/scheduler/eventhandler.go`,`addAllEventHandlers()` 有一段代码如下: 163 | 164 | Handler: cache.ResourceEventHandlerFuncs{ 165 | AddFunc: sched.addPodToSchedulingQueue, 166 | UpdateFunc: sched.updatePodInSchedulingQueue, 167 | DeleteFunc: sched.deletePodFromSchedulingQueue, 168 | } 169 | 170 | 所有的新增、更新、删除事件都会触发对应的入列,更新和出列操作,我们一起看一下这三种操作的具体代码逻辑。 171 | 172 | `Add` 事件 173 | 174 | func (p *PriorityQueue) Add(pod *v1.Pod) error { 175 | p.lock.Lock() 176 | defer p.lock.Unlock() 177 | 178 | pInfo := p.newQueuedPodInfo(pod) 179 | if err := p.activeQ.Add(pInfo); err != nil { 180 | return err 181 | } 182 | 183 | if p.unschedulableQ.get(pod) != nil { 184 | p.unschedulableQ.delete(pod) 185 | } 186 | 187 | if err := p.podBackoffQ.Delete(pInfo); err == nil { 188 | } 189 | 190 | p.cond.Broadcast() 191 | 192 | return nil 193 | } 194 | 195 | 1. 首先通过加锁的方式锁住队列。`p.lock` 本身是一个读写锁 `sync.RWMutex`,并通过 `defer` 方法保证最后会释放锁。 196 | 197 | 2. 构造队列元素。`activeQ` 中元素的类型是 `podInfo`,数据结构如下: 198 | 199 | type QueuedPodInfo struct { 200 | *PodInfo 201 | Timestamp time.Time 202 | Attempts int 203 | InitialAttemptTimestamp time.Time 204 | UnschedulablePlugins sets.String 205 | } 206 | 207 | 除了包括本身的一些 `pod` 信息,还有加入队列的时间,调度次数,因为过程中可能出现调度失败,需要重新调度。`UnschedulablePlugins` 则表示调度失败的插件名称。 208 | 209 | 3. 添加队列元素。 `p.activeQ.Add(pInfo)` 其实就是往底层的 `heap` 插入元素。同时,我们需要保证 `podBackoffQ` 和 `unschedulableQ` 中没有对应的元素,否则会重复调度。 210 | 211 | 4. 通知。 `activeQ` 使用条件变量实现通知功能,一旦入列完成,通过 `Broadcast()` 方法唤醒所有正在监听的 `goroutine`,后面我们讲出列逻辑的时候还会涉及到。 212 | 213 | `Update` 事件 214 | 215 | func (sched *Scheduler) updatePodInSchedulingQueue(oldObj, newObj interface{}) { 216 | oldPod, newPod := oldObj.(*v1.Pod), newObj.(*v1.Pod) 217 | // Bypass update event that carries identical objects; otherwise, a duplicated 218 | // Pod may go through scheduling and cause unexpected behavior (see #96071). 219 | if oldPod.ResourceVersion == newPod.ResourceVersion { 220 | return 221 | } 222 | 223 | isAssumed, err := sched.SchedulerCache.IsAssumedPod(newPod) 224 | if err != nil { 225 | utilruntime.HandleError(fmt.Errorf("failed to check whether pod %s/%s is assumed: %v", newPod.Namespace, newPod.Name, err)) 226 | } 227 | if isAssumed { 228 | return 229 | } 230 | 231 | if err := sched.SchedulingQueue.Update(oldPod, newPod); err != nil { 232 | utilruntime.HandleError(fmt.Errorf("unable to update %T: %v", newObj, err)) 233 | } 234 | } 235 | 236 | 1. 对比 `oldPod` 和 `newPod` 的 `ResourceVersion`,如果不一致,则返回,保证更新的是同一个 `pod` 对象。 237 | 238 | 2. 判断 `pod` 是否是 `assumedPod`,`assumedPod` 你可以简单理解为已经找到对应调度节点的 `pod`,不再需要重新走调度流程。 239 | 240 | 3. 如果都满足条件,则走更新流程,代码如下。代码很长,我把具体逻辑用中文标出。 241 | 242 | func (p *PriorityQueue) Update(oldPod, newPod *v1.Pod) error { 243 | // 持有锁,防止冲突 244 | p.lock.Lock() 245 | defer p.lock.Unlock() 246 | 247 | if oldPod != nil { 248 | // 构造podInfo对象 249 | oldPodInfo := newQueuedPodInfoForLookup(oldPod) 250 | 251 | // 如果 `activeQ` 中存在,则更新该对象并重新添加到 `activeQ` 队列中,该添加方法是幂等的,不会重复创建。 252 | if oldPodInfo, exists, _ := p.activeQ.Get(oldPodInfo); exists { 253 | pInfo := updatePod(oldPodInfo, newPod) 254 | return p.activeQ.Update(pInfo) 255 | } 256 | 257 | // 如果 `podBackoffQ` 中存在,则同样更新对象并添加到 `podBackoffQ`,入列操作同样也是幂等的。 258 | if oldPodInfo, exists, _ := p.podBackoffQ.Get(oldPodInfo); exists { 259 | pInfo := updatePod(oldPodInfo, newPod) 260 | return p.podBackoffQ.Update(pInfo) 261 | } 262 | } 263 | 264 | // 判断 unschedulableQ 中是否存在该 pod 265 | if usPodInfo := p.unschedulableQ.get(newPod); usPodInfo != nil { 266 | // 如果存在就更新该 pod 267 | pInfo := updatePod(usPodInfo, newPod) 268 | 269 | // 判断该 pod 中是否有影响调度的字段发生了更新,如果有,则判断是否可以加入到 activeQ,否则重新加入 unschedulableQ 270 | if isPodUpdated(oldPod, newPod) { 271 | // 判断该 pod 是否已经结束了等待时间,如果还没有,就重新加入到 podBackoffQ 队列中,否则加入 activeQ 队列 272 | if p.isPodBackingoff(usPodInfo) { 273 | if err := p.podBackoffQ.Add(pInfo); err != nil { 274 | return err 275 | } 276 | p.unschedulableQ.delete(usPodInfo.Pod) 277 | } else { 278 | if err := p.activeQ.Add(pInfo); err != nil { 279 | return err 280 | } 281 | p.unschedulableQ.delete(usPodInfo.Pod) 282 | p.cond.Broadcast() 283 | } 284 | } else { 285 | p.unschedulableQ.addOrUpdate(pInfo) 286 | } 287 | 288 | return nil 289 | } 290 | 291 | 292 | // 如果任何队列中都没有找到,则直接加入到 activeQ 队列,并广播入列信息 293 | pInfo := p.newQueuedPodInfo(newPod) 294 | if err := p.activeQ.Add(pInfo); err != nil { 295 | return err 296 | } 297 | p.cond.Broadcast() 298 | return nil 299 | } 300 | 301 | Delete 事件 302 | 303 | func (sched *Scheduler) deletePodFromSchedulingQueue(obj interface{}) { 304 | if err := sched.SchedulingQueue.Delete(pod); err != nil { 305 | utilruntime.HandleError(fmt.Errorf("unable to dequeue %T: %v", obj, err)) 306 | } 307 | 308 | // ... 309 | 310 | if fwk.RejectWaitingPod(pod.UID) { 311 | sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(queue.AssignedPodDelete, nil) 312 | } 313 | } 314 | 315 | 1. 从优先级队列中删除 `pod` 316 | 2. 从 waitingPods 中删除该pod,如果成功,表示该 pod 之前已经成功 `assumed`,现在从优先级队列中删除,则会改变集群的资源利用情况,所以会立即触发 `MoveAllToActiveOrBackoffQueue()` 方法,将 `unschedulableQ` 中的元素全部转移至 `podBackoffQ` 或者 `activeQ` 中,我们看一下具体实现 `movePodsToActiveOrBackoffQueue`: 317 | 318 | func (p *PriorityQueue) movePodsToActiveOrBackoffQueue(podInfoList []*framework.QueuedPodInfo, event framework.ClusterEvent) { 319 | moved := false 320 | 321 | // podInfoList 是 unschedulableQ 中全部元素 322 | for _, pInfo := range podInfoList { 323 | 324 | // 如果该元素还在重试等待时间内,则加入 `podBackoffQ`,否则加入 `activeQ` 325 | if p.isPodBackingoff(pInfo) { 326 | if err := p.podBackoffQ.Add(pInfo); err != nil { 327 | klog.ErrorS(err, "Error adding pod to the backoff queue", "pod", klog.KObj(pod)) 328 | } else { 329 | p.unschedulableQ.delete(pod) 330 | } 331 | } else { 332 | if err := p.activeQ.Add(pInfo); err != nil { 333 | klog.ErrorS(err, "Error adding pod to the scheduling queue", "pod", klog.KObj(pod)) 334 | } else { 335 | p.unschedulableQ.delete(pod) 336 | } 337 | } 338 | } 339 | 340 | // ... 341 | 342 | // 如果 move 成功,则唤醒 goroutine 343 | if moved { 344 | p.cond.Broadcast() 345 | } 346 | } 347 | 348 | ### 3.2 出列 349 | 前面说到优先级队列通过条件变量实现通知功能,那消费端肯定也是通过条件变量被唤醒,调用代码入口位于 `pkg/scheduler/scheduler.go` 的 `scheduleOne()` 方法,具体实现代码如下: 350 | 351 | func MakeNextPodFunc(queue SchedulingQueue) func() *framework.QueuedPodInfo { 352 | return func() *framework.QueuedPodInfo { 353 | podInfo, err := queue.Pop() 354 | // ... 355 | } 356 | } 357 | 358 | 其实实现很简单,就是调用优先级队列的 `Pop()` 方法,我们一起看一下: 359 | 360 | func (p *PriorityQueue) Pop() (*framework.QueuedPodInfo, error) { 361 | // 加锁 362 | p.lock.Lock() 363 | defer p.lock.Unlock() 364 | 365 | // 如果 activeQ 中存在元素,则不会等待,否则进入等待状态,直到被唤醒 366 | for p.activeQ.Len() == 0 { 367 | if p.closed { 368 | return nil, fmt.Errorf(queueClosed) 369 | } 370 | p.cond.Wait() 371 | } 372 | 373 | // 从 activeQ 中弹出元素 374 | obj, err := p.activeQ.Pop() 375 | if err != nil { 376 | return nil, err 377 | } 378 | pInfo := obj.(*framework.QueuedPodInfo) 379 | 380 | // ... 381 | 382 | return pInfo, err 383 | } 384 | 385 | ### 3.3 `unschedulableQ` 入列 386 | `unschedulableQ` 入列主要在两个地方,第一个是在更新优先级队列的时候,即 `Update()` 方法,我们在[3.1 入列(activeQ)](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/queue.md#31-%E5%85%A5%E5%88%97-activeq) 讲 `Update` 事件的时候聊过了。第二个是在调用 `AddUnschedulableIfNotPresent()` 方法的时候,实现如下: 387 | 388 | func (p *PriorityQueue) AddUnschedulableIfNotPresent(pInfo *framework.QueuedPodInfo, podSchedulingCycle int64) error { 389 | p.lock.Lock() 390 | defer p.lock.Unlock() 391 | 392 | pod := pInfo.Pod 393 | if p.unschedulableQ.get(pod) != nil { 394 | } 395 | 396 | if _, exists, _ := p.activeQ.Get(pInfo); exists { 397 | } 398 | if _, exists, _ := p.podBackoffQ.Get(pInfo); exists { 399 | } 400 | 401 | if p.moveRequestCycle >= podSchedulingCycle { 402 | if err := p.podBackoffQ.Add(pInfo); err != nil { 403 | } 404 | } else { 405 | p.unschedulableQ.addOrUpdate(pInfo) 406 | } 407 | 408 | return nil 409 | } 410 | 411 | 其实逻辑很清晰,首先判断三级队列中是否存在该元素,如果存在,就返回,否则执行入列操作。但是这里我们做了一个逻辑判断,判断 `moveRequestCycle` 和 `podSchedulingCycle` 大小。`podSchedulingCycle` 我们知道它是一个逻辑概念,表示调度周期,那 `moveRequestCycle` 是什么意思呢?我们可以把它理解为一个缓存,每次执行 `movePodsToActiveOrBackoffQueue()` 方法,都会将 `podSchedulingCycle` 的值赋值给他,而触发 `movePodsToActiveOrBackoffQueue` 的时机除了后台定时任务,当集群资源发生变化的时候,也需要触发该方法,优化调度流程,让 `unschedulableQ` 中的 `pod` 尽快得到调度。 412 | 413 | 另外,调用 `AddUnschedulableIfNotPresent()` 的方法是 `MakeDefaultErrorFunc()`,是 `Scheduler` 处理 `error` 错误默认方法。 414 | 415 | 我们把这几个概念串起来,就理解了大致逻辑,当调度失败的时候,我们通过判断 `moveRequestCycle` 和 `podSchedulingCycle` 大小,就可以判断当前集群资源是否发生变化,如果没有,则重新加入 `unschedulableQ`,否则加入 `podBackoffQ`,有些同学问为什么不加入到 `activeQ`,因为我们前面刚调度失败,不应该再加入到 `activeQ`。 416 | 417 | ### 3.4 `podBackoffQ` 入列 418 | `podBackoffQ` 入列操作其实都穿插在了之前的逻辑中,一个是 `AddUnschedulableIfNotPresent()` 方法,一个是优先级队列的 `Update()` 方法,还有一个是 `movePodsToActiveOrBackoffQueue()` 方法,这里就不展开了。 419 | 420 | ## 4. 总结 421 | `Scheduler` 队列主要采用了三级队列加两个后台任务的方式来进行任务调度,同时采用锁的机制保证线程安全,使用条件变量进行信号通信,另外还加入了一些逻辑时钟来优化调度流程,整体逻辑还是有点复杂,而且代码冗余,所以社区也在想办法优化这几个队列。 -------------------------------------------------------------------------------- /scheduler/start-scheduler.md: -------------------------------------------------------------------------------- 1 | # Scheduler 启动 2 | 3 | Kubernetes Version: v1.22@392de8012eb4116 4 | Date: 2021.10.31 5 | 6 | ## 1. 开篇 7 | [上篇文章](https://github.com/kerthcet/kubernetes-design/blob/main/scheduler/initialization.md)讲了`scheduler` 程序启动前的初始化流程,但是程序启动相关逻辑并没有讲,今天就围绕这一块代码展开,看看 `scheduler` 启动到底运行了哪些服务。 8 | 9 | 代码入口位于 `cmd/kube-scheduler/app/server.go:runCommand()`,大致代码如下,分为信号量处理,`scheduler` 初始化以及最后运行 `scheduler`。 10 | 11 | func runCommand(cmd *cobra.Command, opts *options.Options, registryOptions ...Option) error { 12 | // ... 13 | 14 | ctx, cancel := context.WithCancel(context.Background()) 15 | defer cancel() 16 | go func() { 17 | stopCh := server.SetupSignalHandler() 18 | <-stopCh 19 | cancel() 20 | }() 21 | 22 | cc, sched, err := Setup(ctx, opts, registryOptions...) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | return Run(ctx, cc, sched) 28 | } 29 | 30 | ## 2. 信号量处理 31 | 我们先说说 `server.SetupSignalHandler()`,主要用于通过信号量控制程序运行,目前支持 `SIGTERM` 和 `SIGINT`。这段代码逻辑很清晰,但是我觉得有一些值得学习的地方: 32 | 33 | func SetupSignalContext() context.Context { 34 | close(onlyOneSignalHandler) 35 | 36 | shutdownHandler = make(chan os.Signal, 2) 37 | 38 | ctx, cancel := context.WithCancel(context.Background()) 39 | signal.Notify(shutdownHandler, shutdownSignals...) 40 | go func() { 41 | <-shutdownHandler 42 | cancel() 43 | <-shutdownHandler 44 | os.Exit(1) // second signal. Exit directly. 45 | }() 46 | 47 | return ctx 48 | } 49 | 50 | 第一个是 `close(onlyOneSignalHandler)`, `onlyOneSignalHandler` 是全局申明的 `channel`,通过 `close()` 方法保证整个方法只能调用一次,第二次调用就会引发 `panic`。 51 | 52 | var onlyOneSignalHandler = make(chan struct{}) 53 | 54 | 第二个是声明了长度为2的 channel `shutdownHandler`,用于接受信号量输入,第一次信号量输入会终止程序启动,第二次输入则会立即退出。可以学习一下来实现优雅的终止程序。 55 | 56 | ## 3. 初始化 `Scheduler` 57 | 配置的初始化操作主要是在 `Setup()` 方法中实现: 58 | 59 | func Setup(ctx context.Context, opts *options.Options, outOfTreeRegistryOptions ...Option) (*schedulerserverconfig.CompletedConfig, *scheduler.Scheduler, error) { 60 | 61 | opts.Validate() 62 | 63 | c, err := opts.Config() 64 | 65 | // ... 66 | 67 | outOfTreeRegistry := make(runtime.Registry) 68 | for _, option := range outOfTreeRegistryOptions { 69 | if err := option(outOfTreeRegistry); err != nil { 70 | return nil, nil, err 71 | } 72 | } 73 | 74 | sched, err := scheduler.New(...) 75 | 76 | return &cc, sched, nil 77 | } 78 | 79 | ### 3.1 `ops.Validata()` 80 | 这个方法主要是做参数校验,本身并没有太多复杂的业务逻辑,但是有一点值得学习的是该方法将所有的错误返回,并在最后通过 `NewAggregate()` 聚合,这样可以尽可能多的收集错误。 81 | 82 | func (o *Options) Validate() []error { 83 | var errs []error 84 | 85 | // ... 86 | errs = append(errs, o.Logs.Validate()...) 87 | 88 | return errs 89 | } 90 | 91 | ### 3.2 `opts.Config()` 92 | 这个方法用于生成所有的配置信息,并返回 `Config` 实例,比如 `kubeconfig` 配置,`client` 配置,以及 `metric` 和 `log` 配置等,代码如下: 93 | 94 | func (o *Options) Config() (*schedulerappconfig.Config, error) { 95 | // ... 96 | 97 | c := &schedulerappconfig.Config{} 98 | if err := o.ApplyTo(c); err != nil { 99 | return nil, err 100 | } 101 | 102 | // Prepare kube config. 103 | kubeConfig, err := createKubeConfig(c.ComponentConfig.ClientConnection, o.Master) 104 | 105 | // Prepare kube clients. 106 | client, eventClient, err := createClients(kubeConfig) 107 | 108 | c.EventBroadcaster = events.NewEventBroadcasterAdapter(eventClient) 109 | c.InformerFactory = scheduler.NewInformerFactory(client, 0) 110 | 111 | return c, nil 112 | } 113 | 114 | 115 | 这里有两个配置项值得一提,第一个是 `EventBroadcaster`,这里我们只需要知道它负责 `scheduler` 模块所有事件的上报、保存等处理工作。 116 | 117 | 118 | 第二个配置项是 `InformerFactory`,它主要通过 `ListAndWatch` 机制监听 `apiserver`,获得所有的事件,如 `pod` 创建,更新删除事件等,后面我们也会单独出一篇文章详细的讲解。 119 | 120 | ### 3.3 插件配置 121 | 插件相关代码如下: 122 | 123 | outOfTreeRegistry := make(runtime.Registry) 124 | for _, option := range outOfTreeRegistryOptions { 125 | if err := option(outOfTreeRegistry); err != nil { 126 | return nil, nil, err 127 | } 128 | } 129 | 130 | `kubernetes` 将插件分为两类,一类称为 `In-Tree plugin`,是核心代码的一部分,默认开启,另外还有一类叫 `OutOfTree Plugins`,`kubernetes` 官方托管了一个[插件库](https://github.com/kubernetes-sigs/scheduler-plugins),如果你想要使用外部插件,需要自己编译运行,后面插件专栏我会着重讲。最终生成的 `outOfTreeRegistry` 对象会作为参数传入 `scheduler.New()` 方法。 131 | 132 | 133 | ### 3.4 构造scheduler对象 134 | 这里就是将前面配置好的各类参数传入 `scheduler.New()` 方法,构造 `scheduler` 对象。 135 | 136 | func New(client clientset.Interface, 137 | informerFactory informers.SharedInformerFactory, 138 | dynInformerFactory dynamicinformer.DynamicSharedInformerFactory, 139 | recorderFactory profile.RecorderFactory, 140 | stopCh <-chan struct{}, 141 | opts ...Option) (*Scheduler, error) { 142 | 143 | // ... 144 | 145 | if options.applyDefaultProfile { 146 | var versionedCfg v1beta3.KubeSchedulerConfiguration 147 | scheme.Scheme.Default(&versionedCfg) 148 | cfg := config.KubeSchedulerConfiguration{} 149 | if err := scheme.Scheme.Convert(&versionedCfg, &cfg, nil); err != nil { 150 | return nil, err 151 | } 152 | options.profiles = cfg.Profiles 153 | } 154 | schedulerCache := internalcache.New(30*time.Second, stopEverything) 155 | 156 | registry := frameworkplugins.NewInTreeRegistry() 157 | if err := registry.Merge(options.frameworkOutOfTreeRegistry); err != nil { 158 | return nil, err 159 | } 160 | 161 | snapshot := internalcache.NewEmptySnapshot() 162 | clusterEventMap := make(map[framework.ClusterEvent]sets.String) 163 | 164 | configurator := &Configurator{ 165 | // ... 166 | } 167 | 168 | metrics.Register() 169 | 170 | sched, err := configurator.create() 171 | if err != nil { 172 | return nil, fmt.Errorf("couldn't create scheduler: %v", err) 173 | } 174 | 175 | // Additional tweaks to the config produced by the configurator. 176 | sched.StopEverything = stopEverything 177 | sched.client = client 178 | 179 | addAllEventHandlers(sched, informerFactory, dynInformerFactory, unionedGVKs(clusterEventMap)) 180 | 181 | return sched, nil 182 | } 183 | 184 | 我们把重要的逻辑拆开来讲一下。 185 | 186 | #### 3.4.1 API Convert 187 | 先看第一段逻辑: 188 | 189 | var versionedCfg v1beta3.KubeSchedulerConfiguration 190 | scheme.Scheme.Default(&versionedCfg) 191 | cfg := config.KubeSchedulerConfiguration{} 192 | if err := scheme.Scheme.Convert(&versionedCfg, &cfg, nil); err != nil { 193 | return nil, err 194 | } 195 | options.profiles = cfg.Profiles 196 | 197 | 我们前面讲到 `KubeSchedulerConfiguration` 目前的版本是 `v1beta3`,这里就是将我们配置的 `yaml` 文件转化为 `internal KubeSchedulerConfiguration`,这是 `kubernetes` 多版本处理的主要逻辑,就是将不同的 `api` 版本的代码统一转化为内部统一的版本,类似于接口层设计,主要是通过 `convert()` 方法实现。 198 | 199 | `profile` 用于配置插件的开关及相应的参数设置,可以丰富我们的调度方案,取代了以前多调度器的设计。可以通过直接定义 pod 的 `spec.schedulerName` 来选择你想要的调度方案。默认 `schedulerName` 是 `default-scheduler`。 200 | 201 | #### 3.4.2 SchedulerCache 202 | 第二段逻辑是关于声明一个缓存系统,默认30s超时时间,主要用来加速查询。 203 | 204 | schedulerCache := internalcache.New(30*time.Second, stopEverything) 205 | 206 | #### 3.4.3 InTree Plugins 207 | 第三段逻辑关于 `Plugin` 注册,我们前面已经注册了 `OutOfTree Plugin`,这里注册的是 `InTree Plugin`,并将它们合并。 208 | 209 | registry := frameworkplugins.NewInTreeRegistry() 210 | if err := registry.Merge(options.frameworkOutOfTreeRegistry); err != nil { 211 | return nil, err 212 | } 213 | 214 | #### 3.4.4 Snapshot 215 | 第四段逻辑是关于快照,声明了一个 `Snapshot` 对象,可以保存节点的信息。在每一轮调度前会调用快照方法保存当前所有节点的信息。 216 | 217 | snapshot := internalcache.NewEmptySnapshot() 218 | 219 | #### 3.4.5 注册 `metrics server` 220 | 221 | metrics.Register() 222 | 223 | #### 3.4.6 scheduler对象 224 | 第五段逻辑就是关于生成 `scheduler` 对象,我们看一下 `create()` 代码: 225 | 226 | func (c *Configurator) create() (*Scheduler, error) { 227 | // extender... 228 | 229 | nominator := internalqueue.NewPodNominator(c.informerFactory.Core().V1().Pods().Lister()) 230 | profiles, err := profile.NewMap(c.profiles, c.registry, c.recorderFactory, 231 | // ... 232 | ) 233 | 234 | podQueue := internalqueue.NewSchedulingQueue( 235 | // ... 236 | ) 237 | 238 | algo := NewGenericScheduler( 239 | c.schedulerCache, 240 | c.nodeInfoSnapshot, 241 | c.percentageOfNodesToScore, 242 | ) 243 | 244 | return &Scheduler{ 245 | SchedulerCache: c.schedulerCache, 246 | Algorithm: algo, 247 | Extenders: extenders, 248 | Profiles: profiles, 249 | NextPod: internalqueue.MakeNextPodFunc(podQueue), 250 | Error: MakeDefaultErrorFunc(c.client, c.informerFactory.Core().V1().Pods().Lister(), podQueue, c.schedulerCache), 251 | StopEverything: c.StopEverything, 252 | SchedulingQueue: podQueue, 253 | }, nil 254 | } 255 | 256 | 最前面是一段关于 `extender` 的代码,`extender` 是 `kubernetes` 自定义调度策略的一种扩展方式,侵入性小,无需修改 `scheduler` 代码,但是功能有限,目前只支持 `filter`,`priority`,`bind`,这里简单提一下不做详细介绍。 257 | 258 | 接下来申明了一个 `PodNominator`,`PodNominator` 是 `scheduler` 为了实现抢占调度定义的一种提名管理器,他记录了所有抢占成功但需要等待被抢占Pod退出的Pod。 259 | 260 | nominator := internalqueue.NewPodNominator(c.informerFactory.Core().V1().Pods().Lister()) 261 | 262 | 在 `3.4` 中我们讲到 `scheduler` 可以通过定义多个 `profile` 丰富调度的方案,每一个 `profile` 都对应一个 `framework`,`framework` 是实际的调度框架,管理所有的插件,在预选和优选过程中会被调用。这里,我们通过 `NewMap()` 方法返回了一个 `map` 对象,`key` 对应 `SchedulerName`,`value` 为 `framework`。 263 | 264 | type Map map[string]framework.Framework 265 | 266 | 随后,我们初始化了一个优先级队列,这个队列就是调度队列,所有没有被调度的 `pod` 都会被放到这个队列中等待调度。 267 | 268 | podQueue := internalqueue.NewSchedulingQueue(...) 269 | 270 | 最后,我们声明了一个 `genericScheduler`,并作为 `Scheduler` 的 `Algorithm` 字段对应的值,最后返回 `scheduler` 对象。 271 | 272 | algo := NewGenericScheduler( 273 | c.schedulerCache, 274 | c.nodeInfoSnapshot, 275 | c.percentageOfNodesToScore, 276 | ) 277 | 278 | #### 3.4.7 注册 eventHandler 279 | `addAllEventHandlers()` 方法主要用于注册如 `pod` 创建、删除、更新等对应的 `handler` 方法。 280 | 281 | ### 3.5 完成 Setup 282 | 至此,`scheduler` 初始化完成。 283 | 284 | ## 4. 运行 `Scheduler` 285 | `scheduler` 初始化完成,就到了 `Run()` 方法,代码如下,我们一一分析: 286 | 287 | func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *scheduler.Scheduler) error { 288 | // Configz registration. 289 | if cz, err := configz.New("componentconfig"); err == nil { 290 | cz.Set(cc.ComponentConfig) 291 | } 292 | 293 | // Prepare the event broadcaster. 294 | cc.EventBroadcaster.StartRecordingToSink(ctx.Done()) 295 | 296 | // Setup healthz checks. 297 | var checks []healthz.HealthChecker 298 | if cc.ComponentConfig.LeaderElection.LeaderElect { 299 | checks = append(checks, cc.LeaderElection.WatchDog) 300 | } 301 | 302 | waitingForLeader := make(chan struct{}) 303 | isLeader := func() bool { 304 | select { 305 | case _, ok := <-waitingForLeader: 306 | // if channel is closed, we are leading 307 | return !ok 308 | default: 309 | // channel is open, we are waiting for a leader 310 | return false 311 | } 312 | } 313 | 314 | // Start up the healthz server. 315 | if cc.SecureServing != nil { 316 | handler := buildHandlerChain(newHealthzAndMetricsHandler(&cc.ComponentConfig, cc.InformerFactory, isLeader, checks...), cc.Authentication.Authenticator, cc.Authorization.Authorizer) 317 | // TODO: handle stoppedCh returned by c.SecureServing.Serve 318 | if _, err := cc.SecureServing.Serve(handler, 0, ctx.Done()); err != nil { 319 | // fail early for secure handlers, removing the old error loop from above 320 | return fmt.Errorf("failed to start secure server: %v", err) 321 | } 322 | } 323 | 324 | // Start all informers. 325 | cc.InformerFactory.Start(ctx.Done()) 326 | // DynInformerFactory can be nil in tests. 327 | if cc.DynInformerFactory != nil { 328 | cc.DynInformerFactory.Start(ctx.Done()) 329 | } 330 | 331 | // Wait for all caches to sync before scheduling. 332 | cc.InformerFactory.WaitForCacheSync(ctx.Done()) 333 | // DynInformerFactory can be nil in tests. 334 | if cc.DynInformerFactory != nil { 335 | cc.DynInformerFactory.WaitForCacheSync(ctx.Done()) 336 | } 337 | 338 | // If leader election is enabled, runCommand via LeaderElector until done and exit. 339 | if cc.LeaderElection != nil { 340 | cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{ 341 | OnStartedLeading: func(ctx context.Context) { 342 | close(waitingForLeader) 343 | sched.Run(ctx) 344 | }, 345 | OnStoppedLeading: func() { 346 | select { 347 | case <-ctx.Done(): 348 | // We were asked to terminate. Exit 0. 349 | klog.InfoS("Requested to terminate, exiting") 350 | os.Exit(0) 351 | default: 352 | // We lost the lock. 353 | klog.Exitf("leaderelection lost") 354 | } 355 | }, 356 | } 357 | leaderElector, err := leaderelection.NewLeaderElector(*cc.LeaderElection) 358 | if err != nil { 359 | return fmt.Errorf("couldn't create leader elector: %v", err) 360 | } 361 | 362 | leaderElector.Run(ctx) 363 | 364 | return fmt.Errorf("lost lease") 365 | } 366 | 367 | // Leader election is disabled, so runCommand inline until done. 368 | close(waitingForLeader) 369 | sched.Run(ctx) 370 | return fmt.Errorf("finished without leader elect") 371 | } 372 | ### 4.1 注册 `ComponentConfig` 373 | 首先是生成 `ComponentConfig` 代码: 374 | 375 | if cz, err := configz.New("componentconfig"); err == nil { 376 | cz.Set(cc.ComponentConfig) 377 | } 378 | 379 | `ComponentConfig` 结构体包含了 `scheduler server` 的所有配置信息,它支持通过 `yaml` 的方式进行配置,目前 `API` 版本为 `kubescheduler.config.k8s.io/v1beta3`,`GVK` 定义如下。 380 | 381 | 382 | apiVersion: kubescheduler.config.k8s.io/v1beta3 383 | kind: KubeSchedulerConfiguration 384 | 385 | 这里,我们会将前面 `Setup()` 函数返回的 `ComponentConfig` 对象(`cc.ComponentConfig`)注册到一个全局的变量 `config` 中去,它是 `map[string]*Config{}` 结构,一些其他组件的配置信息也会注册到这个变量中,像 `ControllerManager`,`kube-proxy`, `kubelet`,主要作用是用于对外输出 `JSON` 配置信息,路由地址为 `/configz`。 386 | 387 | ### 4.2 开启 Event 监听并上报 388 | 389 | cc.EventBroadcaster.StartRecordingToSink(ctx.Done()) 390 | 391 | ### 4.3 启动 https 服务器 392 | 其中 `metrics` 服务也是在这里启动的。 393 | 394 | if cc.SecureServing != nil { 395 | handler := buildHandlerChain(newHealthzAndMetricsHandler(...), cc.Authentication.Authenticator, cc.Authorization.Authorizer) 396 | 397 | if _, err := cc.SecureServing.Serve(handler, 0, ctx.Done()); err != nil { 398 | return fmt.Errorf("failed to start secure server: %v", err) 399 | } 400 | } 401 | 402 | ### 4.4 `informer` 启动 403 | 这里不仅启动了 `informer`,而且还需要等待 `informer cache` 从 `apiserver` 同步全量数据完成,因为 `informer` 中需要有全量的数据,只有这样才可以不需要请求 `apiserver` 就知道当前集群的状态。 404 | 405 | cc.InformerFactory.Start(ctx.Done()) 406 | if cc.DynInformerFactory != nil { 407 | cc.DynInformerFactory.Start(ctx.Done()) 408 | } 409 | 410 | cc.InformerFactory.WaitForCacheSync(ctx.Done()) 411 | if cc.DynInformerFactory != nil { 412 | cc.DynInformerFactory.WaitForCacheSync(ctx.Done()) 413 | } 414 | 415 | ### 4.5 `scheduler leader` 选举 416 | 如果 `Scheduler` 开启了 `LeaderElection` 机制,则需要先选举出 `leader`,具体逻辑我们在 `scheduler` 高可用章节中会详细描述。 417 | 418 | ### 4.6 启动 `scheduler` 419 | 最后,我们启动 `scheduler` 实例,一共分为3步: 420 | 421 | sched.SchedulingQueue.Run() 422 | wait.UntilWithContext(ctx, sched.scheduleOne, 0) 423 | sched.SchedulingQueue.Close() 424 | 425 | #### 4.6.1 启动优先级队列: 426 | 427 | sched.SchedulingQueue.Run() 428 | 429 | 这个优先级队列的 `Run()` 方法启动了两个循环队列,负责将调度失败的和无法调度的 `pod` 按照一定策略重新加入到调度队列中。一个是 `BackoffQ`,存储在多个 `schedulingCycle` 中依旧调度失败的 `pod`,并且有 `backOff` 机制,每次重新调度的时间周期会逐渐拉长。第二个队列是 `UnschedulableQ`,存储由于资源不足无法调度的 `pod`。 430 | 431 | func (p *PriorityQueue) Run() { 432 | go wait.Until(p.flushBackoffQCompleted, 1.0*time.Second, p.stop) 433 | go wait.Until(p.flushUnschedulableQLeftover, 30*time.Second, p.stop) 434 | } 435 | 436 | #### 4.6.2 运行调度逻辑 437 | 438 | wait.UntilWithContext(ctx, sched.scheduleOne, 0) 439 | 440 | 这里我们深入的看一下 `UntilWithContext()` 方法,这个方法层层调用,最后调用的函数是 `BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan struct{})`,其中: 441 | 442 | `f` 是 `sched.ScheduleOne` 方法,并且把 `UntilWithContext` 的 `ctx` 值作为 `ScheduleOne` 的参数传入 443 | 444 | `backoff` 是一个 `jitteredBackoffManagerImpl` 实例,它的实际值为: 445 | 446 | &jitteredBackoffManagerImpl{ 447 | clock: &clock.RealClock{}, 448 | duration: 0, 449 | jitter: 0.0, 450 | backoffTimer: nil, 451 | } 452 | 453 | `sliding` 等于 `true`,而 `stopCh` 是 `UntilWithContext` 参数 `ctx` 的 `Done()` 方法对应的 `channel` 。 454 | 455 | 搞清楚了各个参数的输入值,我们看一下 `BackoffUntil` 方法,看他是如何调用调度逻辑的,该方法代码如下: 456 | 457 | func BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan struct{}) { 458 | var t clock.Timer 459 | for { 460 | select { 461 | case <-stopCh: 462 | return 463 | default: 464 | } 465 | 466 | if !sliding { 467 | t = backoff.Backoff() 468 | } 469 | 470 | func() { 471 | defer runtime.HandleCrash() 472 | f() 473 | }() 474 | 475 | if sliding { 476 | t = backoff.Backoff() 477 | } 478 | 479 | select { 480 | case <-stopCh: 481 | if !t.Stop() { 482 | <-t.C() 483 | } 484 | return 485 | case <-t.C(): 486 | } 487 | } 488 | } 489 | 490 | 首先申明了一个定时器,用来定义循环间隔,它实际值为 `backoff.Backoff()`,实际是一个间隔为0的定时器,所以该循环会不间断的运行 `scheduleOne` 方法,`sliding` 则定义了定时器是否需要包含 `scheduleOne` 的执行时间,`true` 则表示不需要,否则需要。最后 `stopCh` 则控制了何时终止程序运行。 491 | 492 | ### 4.6.3 终止运行 493 | 494 | sched.SchedulingQueue.Close() 495 | 496 | 一旦 `context.Done()` 结束运行上下文,则调用 `Close()` 方法关闭优先级队列,退出进程,调度器整个生命周期结束。 497 | 498 | ## 5. 总结 499 | 以上就是 `scheduler` 运行的全部流程,我们看到里面涵盖的东西很多,一篇文章无法 `cover` 所有内容,所以这篇文章主要是把涉及到的一些重要流程做一个介绍,后面我将分多次就某一个流程的完整生命周期进行详解解读, 比如 `scheduler` 的队列机制、`plugins` 机制,以及 `informer` 是如何运行的,等等。 500 | 501 | `敬请期待!` -------------------------------------------------------------------------------- /snapshots/scheduler-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerthcet/kubernetes-design/37af2de9ef873230b598f6ca3466d77ea2e1d585/snapshots/scheduler-architecture.png -------------------------------------------------------------------------------- /snapshots/scheduling-framework-extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerthcet/kubernetes-design/37af2de9ef873230b598f6ca3466d77ea2e1d585/snapshots/scheduling-framework-extensions.png -------------------------------------------------------------------------------- /snapshots/wechat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerthcet/kubernetes-design/37af2de9ef873230b598f6ca3466d77ea2e1d585/snapshots/wechat.jpeg --------------------------------------------------------------------------------