企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
这篇文章源于我在学习scheduler时找到的,阅读完了后让我感觉这确实是一篇非常Excellent的文章,所以决定翻译一下。 # :-: How does the Kubernetes scheduler work?   之前我们谈过了Kubernetes的整体结构,这周我又学到了kubernetes sceduler是如何工作的,所以我把它分享出来。这涉及到了调度器的具体工作方式。   这同样也阐述了如何从“我对于这个系统完全一无所知”到“ok,我理解了它的的基本设计决策与为什么是这样的”这一过程,并且是在,完全不问任何人的前提下,(因为事实上我不认识任何kubernetes的Contribute,所以也就无法找人向我解释。) 这只是一种很little的思维,但希望他能够对某些人有用。我找到的最有用的链接就是从让人大吃一惊的[kubernetes developer documentation folder](https://github.com/kubernetes/community/tree/8decfe42b8cc1e027da290c4e98fa75b3e98e2cc/contributors/devel)中的[Writing Controllers](https://github.com/kubernetes/community/blob/8decfe4/contributors/devel/controllers.md)这个文件。 ### what is the scheduler for? kubernetes schedulor负责调度pod到Node上,它的基本工作是: 1.你创建pod 2.scedulor 察觉到有一个新创建的pod还没有分配到node上 3.scheduler分配pod 他并不为运行的pod负责,那是kubelet的工作,因此它的工作就是确定每一个pod都有一个分配了的节点,Easy,right? Kubernetes的controller也有一个这样的概念,controller的工作是: 观察系统的状态 看目标状态与实际状态是否相符(就像需要被分配的pod一样)(这地方我还真是没想到) 重复 scheduler某种程度上就是一种controller,有很多不同的控制器,它们都有不同的工作并且独立运行。 所以,你可以想象scheduler的运行循环就像下面这样: ~~~ while True: pods = get_all_pods() for pod in pods: if pod.node == nil: assignNode(pod) ~~~ (注:后面可以看到,这个想象确实相当‘科学’)。 如果你并不对scheduler的详细细节感兴趣的话,读到这里就ok了,这确实是一个相当合理的运作结构。 我之所以认为scheduler是这样运作的是因为cronjob controller是这样运作的,且我只看过这部分的代码,cronjob控制器基本上就是遍历所有的cronjobs,查看是否有任何事情要做,休眠10秒,然后永远重复。Super simple! ### this isn’t how it works though so!这周我们在Kubernetes上附在了更多的工作,我们察觉到一个问题。 有时候一个pod会一直被卡在pending状态(没有为其分配node),如果我们重启scheduler,pod就能恢复正常, ([this issue](https://github.com/kubernetes/kubernetes/issues/49314)) 但这跟我对于scheduler的运作的想法并不匹配。正常来说不是应该不必重新启动就能分配到pod上吗。 因此我去阅读了一大堆代码,下面是我了解到的scheduler实际的工作原理, 因为我这周才知道这个,所以也可能有错。 ### how the scheduler works: a very quick code walkthrough 我们的入口在[scheduler.go](https://github.com/kubernetes/kubernetes/blob/e4551d50e57c089aab6f67333412d3ca64bc09ae/plugin/pkg/scheduler/scheduler.go)文件中,但事实上我把所有的文件都凑到了一块[concatenated all the files in the scheduler together](https://gist.github.com/jvns/5d492d66130a2f47b47820fd6b52eab5)。 scheduler的核心loop(commmit e4551d50e5): ([link](https://github.com/kubernetes/kubernetes/blob/e4551d50e57c089aab6f67333412d3ca64bc09ae/plugin/pkg/scheduler/scheduler.go#L150-L156)) ~~~ go wait.Until(sched.scheduleOne, 0, sched.config.StopEverything) ~~~ 大意就是,让scheduler一直不停的run下去。Cool,那具体怎么做的? ~~~ func (sched *Scheduler) scheduleOne() { pod := sched.config.NextPod() // do all the scheduler stuff for `pod` } ~~~ 那NextPod()是做什么的?来自于哪里? ~~~ func (f *ConfigFactory) getNextPod() *v1.Pod { for { pod := cache.Pop(f.podQueue).(*v1.Pod) if f.ResponsibleForPod(pod) { glog.V(4).Infof("About to try and schedule pod %v", pod.Name) return pod } } } ~~~ Okay,这很简单,从podQueue当中取到next pod。 但那pod是怎么放进去的呢(But how do pods end up on that queue?)?下面的代码就是: ~~~ podInformer.Informer().AddEventHandler( cache.FilteringResourceEventHandler{ Handler: cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { if err := c.podQueue.Add(obj); err != nil { runtime.HandleError(fmt.Errorf("unable to queue %T: %v", obj, err)) } }, ~~~ 添加了一个事件监听器,当有新pod添加进去时,将他放入到queen当中。 ### how the scheduler works, in English Okay,现在我们已经看完了代码,下面是一个总结: 1.首先每一个需要被调度的pod添加到queen当中。 2.当新的pod创建时,也会添加到queen当中。 3.调度器不听得取出pod,然后调度他。 4.就这样 这里有个有意思的事情是,如果出于某种原因pod调度失败,这里没有任何重新调度pod的尝试,pod从queen中取出,然后调度失败,然后就完了。他失去了他唯一的机会。(除非你重启scheduler,这种情况下所有都会被再一次添加到pod queen当中。) 当然scheduler没那么笨,当一个pod调度失败时,通常会调用一个像下面这样的错误处理。 ~~~go host, err := sched.config.Algorithm.Schedule(pod, sched.config.NodeLister) if err != nil { glog.V(1).Infof("Failed to schedule pod: %v/%v", pod.Namespace, pod.Name) sched.config.Error(pod, err) ~~~ 这个方法会把pod重新调度到queue中然后重试。 ### wait why did our pod get stuck then? 这很简单啦,说明了错误处理方法并不总是成功被调用当出现错误时。我们在error方法调用的地方做了点修改,然后看上去就能正常运转了。Cool! ### why is the scheduler designed this way? 我感觉下面这种设计企不是根据有健壮性: ~~~ while True: pods = get_all_pods() for pod in pods: if pod.node == nil: assignNode(pod) ~~~ 那为什么是采用caches,quene,然后还有callbacks这些复杂的操作呢?我回顾了下历史然后想到了可能是性能的原因------比如你可以看这篇文章[update on scalability updates for Kubernetes 1.6](https://blog.kubernetes.io/2017/03/scalability-updates-in-kubernetes-1.6.html) 还有这篇来自coreos 的[improving Kubernetes scheduler performance](https://coreos.com/blog/improving-kubernetes-scheduler-performance.html). 一篇说调度30000pods从14分钟到10分钟,另一篇说调度30000pod从2小时到10分钟,2小时真是太慢了,性能很重要。 ### what the scheduler actually uses: kubernetes “informers” 我还想谈及Kubernetes上的一个非常重要的所有kubernetes的controllers的设计。那就是informer,幸运的是这里我通过gooleing”kubernetes informer“找到了这篇文章。 这篇非常有用的文章称作[Writing Controllers](https://github.com/kubernetes/community/blob/8decfe4/contributors/devel/controllers.md),文章给你设计的建议关于编写controller(就像scheduler和cronjob controller)。VERY COOL。 如果我第一时间就能找到这篇文章的话,我肯定可以更快的理解这里发生的事情。 因此,informers!doc是这样写的: ~~~ Use SharedInformers. SharedInformers provide hooks to receive notifications of adds, updates, and deletes for a particular resource. They also provide convenience functions for accessing shared caches and determining when a cache is primed 使用SharedInformers。SharedInformers提供钩子来接收特定资源的添加、更新和删除通知。它们还提供了一些方便的函数来访问共享缓存,并确定何时启动缓存。 ~~~ 当一个controller运行起来后会创建一个”informer“(例如一个”pod informer“)负责以下功能: 1.首先列出所有的pod。 2.告诉你update cronjob控制器并不是使用一个informer(使用informers更复杂了,我想他根本不会考录那么多的性能因素),但许多其他的(大多数)controllers是这样做的。特别是scheduler,你可以看到他配置的infromers在[这里](https://github.com/kubernetes/kubernetes/blob/e4551d50e57c089aab6f67333412d3ca64bc09ae/plugin/pkg/scheduler/factory/factory.go#L175). ### requeueing 在”write controller“文档里面实际上有一些关于如何处理item的requeu。 过滤错误到顶层以实现连续的requeue,我们有一个workqueue.RateLimitingInterface允许简单的重新requeue一个合理的backoff(补偿?) 你的main方法应该返回一个错误当requeue必要时,如果不是的话,就应当使用utilruntime.HandleError返回一个nil,这会使得reviewers轻松的检查错误处理以及对于控制器并没有偶然的丢失掉应该重试东西而有信心。 这看上去是一个很好的建议,正确处理所有错误看起来很棘手,所以有一种简单的方法来确保审核人员能够知道错误被正确处理是很重要的!COOL。 ### you should “sync” your informers (or should you?) Okay,这是我最后学到的东西了。 Informers有一个sync的概念,像是重新启动程序一样,你会拿到每一个你所监视的资源列表,然后你可以检查事实上是不是ok,下面是”writing controllers“介绍的关于syncing。 Watchs与Informers将会”sync“,定期性的,他们会在集群中分发每一个匹配的对象到你的update方法上,这是一个很好的案例当你需要在对象上采取额外的动作时。此处留下。。。 因此,这暗示”你应当sync,如果你不sync,那么最后你会丢掉一个item然后在一不会重试“,这也正是我们所遇见的情况。 ### the kubernetes scheduler doesn’t resync so!!当我学了sync这个想法后,I was like....,wait,这是否意味着kubernetes scheduler从不resyncs?答案是,是sync的。下面是代码: ~~~ informerFactory := informers.NewSharedInformerFactory(kubecli, 0) // cache only non-terminal pods podInformer := factory.NewPodInformer(kubecli, 0)` ~~~ 这些数字0,代表了resync周期,我解读为他从不resync,Interesting!!为什么他从不resync?我并不确定,接着google了”kubernetes scheduler resync”, 然后找到了这个提交申请[#16840](https://github.com/kubernetes/kubernetes/pull/16840)(为scheduler添加resync),有下面两个评论。 @brendandburns-我非常反对有如此小的resync周期,因为它会明显的影响性能。 以及 我同意@wojtek-t,如果resync能够解决问题,这意味着我们隐藏了一个bug,我认为resync并不是正确的解决方案。 因此,项目维护着决定不再进行同步,因为当存在bugs,他们希望被发现且加以修复,而不是被resync隐藏。 ### some code-reading tips 就我目前所知,kubernetes scheduler内部如何运作目前还没有人写下来(下大多数事情一样)。 在阅读代码时这里有几件事情帮助了我 1 把各个文件聚集到一个文本中,我已经说过了但这确实对我很有帮助,因为在各种方法与文件中跳转确实让人头晕,特别是在我并不理解是怎样组织的情况下。 2 找准关键性的问题,在这里我主要是想弄清楚错误处理是如何工作的?如果一个pod并没有被调度会发生什么?这里有许多的代码....,关于他如何选择node的我并不需要去关心。 ### kubernetes is pretty good to work with so far Kubernetes是一个非常复杂的软件!为了让整个集群能够运行起来,你需要设置至少6个不同的组件(API server,scheduler,controller manage,container network例如flanned,kube-proxy,kubelet),如果你像我一样想要理解软件是如何运作的那就必须要去理解他们之间的交互以及诸多的配置。 但到目前为止文档都是很棒的,如果文档没有涵盖的话读源码也是很容易的,并且他们也非常愿意对提交请求作出回应。 我确实需要比通常更多的去阅读源码,这是一个很好的技能!