以阅读k8s其中的一个模块,scheduler为例子,来讲讲我是怎么读代码的
scheduler是k8s的调度模块,做的事情就是拿到pod之后在node中寻找合适的进行适配这么一个单纯的功能。实际上,我已经多次编译和构建这个程序并运行起来。在我的脑中,sheduler在整个系统中是这样的:
scheduler作为一个客户端,从apiserver中读取到需要分配的pod,和拥有的node,然后进行过滤和算分,最后把这个匹配信息通过apiserver写入到etcd里面,供下一步的kubelet去拉起pod使用。这样,立刻有几个问题浮现出来
问1.scheduler读取到的数据结构是怎么样的?(输入)
问2.scheduler写出的的数据结构是怎么样的?(输出)
问3.在前面的测试中,scheduler成为了系统的瓶颈,为什么?
kubernetes/plugin/cmd/kube-scheduler/scheduler.go
这段代码比较短就全文贴出来了
package main
import (
"runtime"
"k8s.io/kubernetes/pkg/healthz"
"k8s.io/kubernetes/pkg/util"
"k8s.io/kubernetes/pkg/version/verflag"
"k8s.io/kubernetes/plugin/cmd/kube-scheduler/app"
"github.com/spf13/pflag"
)
func init() {
healthz.DefaultHealthz() //忽略……
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) //忽略……
s := app.NewSchedulerServer() //关注,实际调用的初始化
s.AddFlags(pflag.CommandLine) //忽略,命令行解析
util.InitFlags()
util.InitLogs()
defer util.FlushLogs() //忽略,开日志等
verflag.PrintAndExitIfRequested()
s.Run(pflag.CommandLine.Args()) //关注,实际跑的口子
}
可以看到,对于细枝末节我一概忽略掉,进入下一层,但是,我并不是不提出问题,提出的问题会写在这里,然后从脑子里面“忘掉”,以减轻前进的负担
kubernetes/plugin/cmd/kube-scheduler/app/server.go
进入这个文件后,重点看的就是数据结构和方法:
SchedulerServer
这个结构存放了一堆配置信息,裸的,可以看到里面几个成员变量都是基本类型,int, string等 AlgorithmProvider
来创建具体算法的调度器。 再下一层的入口在:
sched := scheduler.New(config)
sched.Run()
对于这层的问题是:
问5.几个限流是怎么实现的?QPS和Brust有什么区别?
问6.算法提供者 AlgorithmProvider
是怎么被抽象出来的?需要完成什么事情?
答5.在翻了限流的代码后,发现来自于 kubernetes/Godeps/_workspace/src/github.com/juju/ratelimit
,实现的是一个令牌桶的算法,burst指的是在n个请求内保持qps平均值的度量。详见这篇 文章
kubernetes/plugin/pkg/scheduler/scheduler.go
答2:在这里我看到了输出的数据结构为:
b := &api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: pod.Namespace, Name: pod.Name},
Target: api.ObjectReference{
Kind: "Node",
Name: dest,
},
}
这个文件最重要的数据结构是:
type Config struct {
// It is expected that changes made via modeler will be observed
// by NodeLister and Algorithm.
Modeler SystemModeler
NodeLister algorithm.NodeLister
Algorithm algorithm.ScheduleAlgorithm
Binder Binder
// Rate at which we can create pods
// If this field is nil, we don't have any rate limit.
BindPodsRateLimiter util.RateLimiter
// NextPod should be a function that blocks until the next pod
// is available. We don't use a channel for this, because scheduling
// a pod may take some amount of time and we don't want pods to get
// stale while they sit in a channel.
NextPod func() *api.Pod
// Error is called if there is an error. It is passed the pod in
// question, and the error
Error func(*api.Pod, error)
// Recorder is the EventRecorder to use
Recorder record.EventRecorder
// Close this to shut down the scheduler.
StopEverything chan struct{}
}
数据结构是什么?数据结构就是舞台上的角色,而函数方法就是这些角色之间演出的一幕幕戏。对象是有生命的,从创建到数据流转,从产生到消亡。而作为开发者来说,首先是搞懂这些人物设定,是关公还是秦琼,是红脸还是黑脸?看懂了人,就看懂了戏。
这段代码里面,结合下面的方法,我可以得出这么几个印象:
问7.结合观看了 modeler.go
之后,发现这是在绑定后处理的,所谓的assuemPod,就是把绑定的pod放到一个队列里面去,不是很理解为什么这个互斥操作是放在bind之后做?
问8.Binder是怎么去做绑定操作的?
下一层入口:
dest, err := s.config.Algorithm.Schedule(pod, s.config.NodeLister)
kubernetes/plugin/pkg/scheduler/generic_scheduler.go
在调到这一层的时候,我发现自己走过头了,上面 s.config.Algorithm.Schedule
并不会直接调用 generic_scheduler.go
。对于一门面向对象的语言来说,最后的执行可能是一层接口套一层接口,而接口和实现的分离也造成了当你阅读到某个地方之后就无法深入下去。或者说,纯粹的自顶向下的阅读方式并不适合面向对象的代码。所以,目前我的阅读方法开始变成了碎片式阅读,先把整个代码目录树给看一遍,然后去最有可能解释我心中疑问的地方去寻找答案,然后一片片把真相拼合起来。
问9.generic_scheduler.go是怎么和scehduler.go产生关系的?
这是代码目录树:
从目录树中,可以看出调度算法的目录在 algrorithem
和 algrorithemprovider
里面,而把对象组装在一起的关键源代码是在:
答8.Binder的操作其实很简单,就是把pod和node的两个字段放到http请求中发送到apiserver去做绑定,这也和系统的整体架构是一致的
factory的最大作用,就是从命令行参数中获取到 --algorithm
和 --policy-config-file
来获取到必要算法名称和调度策略,来构建Config,Config其实是调度程序的核心数据结构。schduler这整个程序做的事情可以概括为:获取配置信息——构建Config——运行Config。这个过程类似于java中的sping组装对象,只不过在这里是通过代码显式进行的。从装配工厂中,我们看到了关键的一行
algo := scheduler.NewGenericScheduler(predicateFuncs, priorityConfigs, extenders, f.PodLister, r)
这样就把我上面的问9解答了
答9.scheduler.go是形式,generic_scheduler.go是内容,通过factory组装
也解答了问6
答6.factoryProvider仅仅是一个算法注册的键值对表达地,大部分的实现还是放在generic_scheduler里面的
这就涉及到调度的核心逻辑,就2行
filteredNodes, failedPredicateMap, err := findNodesThatFit()....
priorityList, err := PrioritizeNodes()...
这里我就不详细叙述细节了,读者可以按照我的路子去自己寻找答案。