kube-scheduler介绍
kube-schedulerkube-scheduler是 kubernetes 系统的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理、更加充分的利用集群的资源。
kube-scheduler 是 kubernetes 的调度器,它的主要作用就是根据特定的调度算法和调度策略将 Pod 调度到合适的 Node 节点上去,是一个独立的二进制程序,启动之后会一直监听 API Server,获取到 PodSpec.NodeName 为空的 Pod,对每个 Pod 都会创建一个 binding
调度主要分为以下几个部分:
首先是预选过程,过滤掉不满足条件的节点,这个过程称为Predicates然后是优选过程,对通过的节点按照优先级排序,称之为Priorities最后从中选择优先级最高的节点,如果中间任何一步骤有错误,就直接返回错误
scheduler调度流程
调度主要分为以下几个部分:首先是预选过程,过滤掉不满足条件的节点,这个过程称为Predicates然后是优选过程,对通过的节点按照优先级排序,称之为Priorities最后从中选择优先级最高的节点,如果中间任何一步骤有错误,就直接返回错误具体显示为pod状态会一直pending
详细流程
首先,客户端通过 API Server 的 REST API 或者 kubectl 工具创建 Pod 资源
API Server 收到用户请求后,存储相关数据到 etcd 数据库中
调度器监听 API Server 查看为调度(bind)的 Pod 列表,循环遍历地为每个 Pod 尝试分配节点,这个分配过程就是我们上面提到的两个阶段:
预选阶段(Predicates),过滤节点,调度器用一组规则过滤掉不符合要求的 Node 节点,比如 Pod 设置了资源的 request,那么可用资源比 Pod 需要的资源少的主机显然就会被过滤掉
优选阶段(Priorities),为节点的优先级打分,将上一阶段过滤出来的 Node 列表进行打分,调度器会考虑一些整体的优化策略,比如把 Deployment 控制的多个 Pod 副本分布到不同的主机上,使用最低负载的主机等等策略
经过上面的阶段过滤后选择打分最高的 Node 节点和 Pod 进行 binding 操作,然后将结果存储到 etcd 中
最后被选择出来的 Node 节点对应的 kubelet 去执行创建 Pod 的相关操作目前在插件化以后,scheduler更细化为多个阶段
sort最先执行的阶段,该节点主要是对pod的排序,通过对PodPriority的比较进行排序,将优先度高的pod排在前面,也就是优先级抢占策略 ,一般用在静态pod这类需要高优先级的pod上
Pre-filter 扩展用于对 Pod 的信息进行预处理,或者检查一些集群或 Pod 必须满足的前提条件,如果 pre-filter 返回了 error,则调度过程终止。
Filter 扩展用于排除那些不能运行该 Pod 的节点,对于每一个节点,调度器将按顺序执行 filter 扩展;如果任何一个 filter 将节点标记为不可选,则余下的 filter 扩展将不会被执行。调度器可以同时对多个节点执行 filter 扩展。
Post-filter 是一个通知类型的扩展点,调用该扩展的参数是 filter 阶段结束后被筛选为可选节点的节点列表,可以在扩展中使用这些信息更新内部状态,或者产生日志或 metrics 信息。
Scoring 扩展用于为所有可选节点进行打分,调度器将针对每一个节点调用 Soring 扩展,评分结果是一个范围内的整数。在 normalize scoring 阶段,调度器将会把每个 scoring 扩展对具体某个节点的评分结果和该扩展的权重合并起来,作为最终评分结果。
Normalize scoring 扩展在调度器对节点进行最终排序之前修改每个节点的评分结果,注册到该扩展点的扩展在被调用时,将获得同一个插件中的 scoring 扩展的评分结果作为参数,调度框架每执行一次调度,都将调用所有插件中的一个 normalize scoring 扩展一次。
Reserve 是一个通知性质的扩展点,有状态的插件可以使用该扩展点来获得节点上为 Pod 预留的资源,该事件发生在调度器将 Pod 绑定到节点之前,目的是避免调度器在等待 Pod 与节点绑定的过程中调度新的 Pod 到节点上时,发生实际使用资源超出可用资源的情况。(因为绑定 Pod 到节点上是异步发生的)。这是调度过程的最后一个步骤,Pod 进入 reserved 状态以后,要么在绑定失败时触发 Unreserve 扩展,要么在绑定成功时,由 Post-bind 扩展结束绑定过程。
Permit 扩展用于阻止或者延迟 Pod 与节点的绑定。Permit 扩展可以做下面三件事中的一项:
approve(批准):当所有的 permit 扩展都 approve 了 Pod 与节点的绑定,调度器将继续执行绑定过程
deny(拒绝):如果任何一个 permit 扩展 deny 了 Pod 与节点的绑定,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展
wait(等待):如果一个 permit 扩展返回了 wait,则 Pod 将保持在 permit 阶段,直到被其他扩展 approve,如果超时事件发生,wait 状态变成 deny,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展
Pre-bind 扩展用于在 Pod 绑定之前执行某些逻辑。例如,pre-bind 扩展可以将一个基于网络的数据卷挂载到节点上,以便 Pod 可以使用。如果任何一个 pre-bind 扩展返回错误,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展。
Bind 扩展用于将 Pod 绑定到节点上:只有所有的 pre-bind 扩展都成功执行了,bind 扩展才会执行 调度框架按照 bind 扩展注册的顺序逐个调用 bind 扩展 具体某个 bind 扩展可以选择处理或者不处理该 Pod 如果某个 bind 扩展处理了该 Pod 与节点的绑定,余下的 bind 扩展将被忽略
Post-bind 是一个通知性质的扩展:Post-bind 扩展在 Pod 成功绑定到节点上之后被动调用 Post-bind 扩展是绑定过程的最后一个步骤,可以用来执行资源清理的动作
Unreserve 是一个通知性质的扩展,如果为 Pod 预留了资源,Pod 又在被绑定过程中被拒绝绑定,则 unreserve 扩展将被调用。Unreserve 扩展应该释放已经为 Pod 预留的节点上的计算资源。在一个插件中,reserve 扩展和 unreserve 扩展应该成对出现。
scheduler默认调度算法
scheduler中默认加载了许多调度算法具体如下
PodFitsResources:节点上剩余的资源是否大于 Pod 请求的资源
PodFitsHost:如果 Pod 指定了 NodeName,检查节点名称是否和 NodeName 匹配
PodFitsHostPorts:节点上已经使用的 port 是否和 Pod 申请的 port 冲突
PodSelectorMatches:过滤掉和 Pod 指定的 label 不匹配的节点
NoDiskConflict:已经 mount 的 volume 和 Pod 指定的 volume 不冲突,除非它们都是只读的
CheckNodeDiskPressure:检查节点磁盘空间是否符合要求
CheckNodeMemoryPressure:检查节点内存是否够用
scheduler代码相关解析
kubernetes 调度器的源码位于 kubernetes/pkg/scheduler中,具体结果如下所示
kube-scheduler的入口程序,对应的代码在 cmd/kube-scheduler/scheduler.go。
func main() {
rand.Seed(time.Now().UnixNano())
command := app.NewSchedulerCommand()
// TODO: once we switch everything over to Cobra commands, we can go back to calling
// utilflag.InitFlags() (by removing its pflag.Parse() call). For now, we have to set the
// normalize func and add the go flag set by hand.
pflag.CommandLine.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
// utilflag.InitFlags()
logs.InitLogs()
defer logs.FlushLogs()
if err := command.Execute(); err != nil {
os.Exit(1)
}
}
在代码中我们可以看到,mian方法在初始化时,率先执行了 command := app.NewSchedulerCommand()方法,在这个方法中可以看到支持参数Option,而Option则为out-of-tree的framework插件算法
// NewSchedulerCommand creates a *cobra.Command object with default parameters and registryOptions
func NewSchedulerCommand(registryOptions ...Option) *cobra.Command {
opts, err := options.NewOptions()
if err != nil {
klog.Fatalf("unable to initialize command options: %v", err)
}
cmd := &cobra.Command{
Use: "kube-scheduler",
Long: `The Kubernetes scheduler is a control plane process which assigns
Pods to Nodes. The scheduler determines which Nodes are valid placements for
each Pod in the scheduling queue according to constraints and available
resources. The scheduler then ranks each valid Node and binds the Pod to a
suitable Node. Multiple different schedulers may be used within a cluster;
kube-scheduler is the reference implementation.
See [scheduling](https://kubernetes.io/docs/concepts/scheduling-eviction/)
for more information about scheduling and the kube-scheduler component.`,
Run: func(cmd *cobra.Command, args []string) {
if err := runCommand(cmd, opts, registryOptions...); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
},
Args: func(cmd *cobra.Command, args []string) error {
for _, arg := range args {
if len(arg) > 0 {
return fmt.Errorf("%q does not take any arguments, got %q", cmd.CommandPath(), args)
}
}
return nil
},
}
fs := cmd.Flags()
namedFlagSets := opts.Flags()
verflag.AddFlags(namedFlagSets.FlagSet("global"))
globalflag.AddGlobalFlags(namedFlagSets.FlagSet("global"), cmd.Name())
for _, f := range namedFlagSets.FlagSets {
fs.AddFlagSet(f)
}
usageFmt := "Usage:\n %s\n"
cols, _, _ := term.TerminalSize(cmd.OutOrStdout())
cmd.SetUsageFunc(func(cmd *cobra.Command) error {
fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine())
cliflag.PrintSections(cmd.OutOrStderr(), namedFlagSets, cols)
return nil
})
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine())
cliflag.PrintSections(cmd.OutOrStdout(), namedFlagSets, cols)
})
cmd.MarkFlagFilename("config", "yaml", "yml", "json")
return cmd
}
首先执行 opts, err := options.NewOptions()函数,该函数初始化各个模块的默认配置,例如HTTP或HTTPS服务等
// NewOptions returns default scheduler app options.
func NewOptions() (*Options, error) {
cfg, err := newDefaultComponentConfig()
if err != nil {
return nil, err
}
hhost, hport, err := splitHostIntPort(cfg.HealthzBindAddress)
if err != nil {
return nil, err
}
o := &Options{
ComponentConfig: *cfg,
SecureServing: apiserveroptions.NewSecureServingOptions().WithLoopback(),
CombinedInsecureServing: &CombinedInsecureServingOptions{
Healthz: (&apiserveroptions.DeprecatedInsecureServingOptions{
BindNetwork: "tcp",
}).WithLoopback(),
Metrics: (&apiserveroptions.DeprecatedInsecureServingOptions{
BindNetwork: "tcp",
}).WithLoopback(),
BindPort: hport,
BindAddress: hhost,
},
Authentication: apiserveroptions.NewDelegatingAuthenticationOptions(),
Authorization: apiserveroptions.NewDelegatingAuthorizationOptions(),
Deprecated: &DeprecatedOptions{
UseLegacyPolicyConfig: false,
PolicyConfigMapNamespace: metav1.NamespaceSystem,
SchedulerName: corev1.DefaultSchedulerName,
HardPodAffinitySymmetricWeight: 1,
},
Metrics: metrics.NewOptions(),
Logs: logs.NewOptions(),
}
o.Authentication.TolerateInClusterLookupFailure = true
o.Authentication.RemoteKubeConfigFileOptional = true
o.Authorization.RemoteKubeConfigFileOptional = true
o.Authorization.AlwaysAllowPaths = []string{"/healthz"}
// Set the PairName but leave certificate directory blank to generate in-memory by default
o.SecureServing.ServerCert.CertDirectory = ""
o.SecureServing.ServerCert.PairName = "kube-scheduler"
o.SecureServing.BindPort = kubeschedulerconfig.DefaultKubeSchedulerPort
return o, nil
interface.go文件interface.go文件中定义了scheduler插件中各个组件的接口信息如PreFilterPlugin FilterPlugin等,我们所有的自定义插件(out of tree)都需要继承这个接口
plugins文件夹在插件化以后,所有的调度策略代码都在该文件夹下,如imagelocality等。我们如果需要扩展in-tree插件则也需要在该文件夹下实现对应的方法如我们要实现的imagearch调度策略就需要在该文件夹下实现interface.go中对应的接口,实现filter接口
factory.go文件
registry.go文件legacy_registry.go文件generic_scheduler.go文件
in-tree插件实现步骤
1.在/pkg/scheduler/framework/plugins目录下建立自己的插件文件夹,参考源码自带插件编写chajian.go以及BUILD文件,实现扩展点对应的接口2.修改/pkg/scheduler/framework/plugins/registry.go 在NewInTreeRegistry增加插件注册3.修改/pkg/scheduler/framework/plugins/legacy_registry.go 增加有关枚举和registerPredicateConfigProducer4.修改/pkg/scheduler/algorithmprovider/registry.go,在相应的pluginSet里增加你自己实现的扩展5.修改/pkg/scheduler/algorithmprovider/BUILD,在deps里增加你的插件目录,如果不是通过Google的brzael编译,这一步不需要6.编译 make all WHAT=cmd/kube-scheduler,在./_output/bin目录中可以看到编译完成的kube-scheduler可执行文件7.构建kube-scheduler镜像,直接使用原来的镜像作为基础镜像,覆盖/usr/local/bin里的kube-scheduler文件即可8.替换kube-scheduler镜像并进行测试
scheduler extender扩展
1.16版本以后已经不推荐使用extender扩展的形式实现原理是通过实现了reset接口的形式,所谓的scheduler extender其实就是一个可配置的 Webhook 而已,里面包含 过滤器 和 优先级 两个端点,分别对应调度周期中的两个主要阶段(过滤和打分)。
scheduler framework扩展
1.15以后推荐的实现方式scheduler插件化以后,主流的扩展方式为framework扩展,目前社区基于framework形式建立了项目https://github.com/kubernetes-sigs/scheduler-plugins,在该项目中增加了许多scheduler调度算法调度框架定义了一组扩展点,用户可以实现扩展点定义的接口(实现pkg/scheduler/framework/interface下Framework的接口)来定义自己的调度逻辑(我们称之为扩展),并将扩展注册到扩展点上,调度框架在执行调度工作流时,遇到对应的扩展点时,将调用用户注册的扩展。framework的实现方式如上述源码分析,我们需要在启动命令时,增加我们自己实现的plugin插件
command := app.NewSchedulerCommand(
app.WithPlugin(sample.Name, sample.New),
)
type PluginFactory = func(configuration *runtime.Unknown, f FrameworkHandle) (Plugin, error)如我们的业务需求是需要实现filter接口,则我们可以基于上次的源码分析中,在官方提供的https://github.com/kubernetes-sigs/scheduler-plugins中,实现对应的接口。并执行make文件等创建出对应的二进制文件构建相应的镜像。如果只是想在默认的default-schedule中增加算法,则配置文件中只需要如此配置
如果是想生成新的调度算法则要增加以下诸多配置ClusterRole、ServiceAccount、ClusterRoleBinding、部署ConfigMap,包含一个KubeSchedulerConfiguration类型的配置文件以及启动插件用的deployment(正式环境建议使用静态pod的形式)demo如下:
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: sample-scheduler-clusterrole
rules:
- apiGroups:
- ""
resources:
- endpoints
- events
verbs:
- create
- get
- update
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- pods
verbs:
- delete
- get
- list
- watch
- update
- apiGroups:
- ""
resources:
- bindings
- pods/binding
verbs:
- create
- apiGroups:
- ""
resources:
- pods/status
verbs:
- patch
- update
- apiGroups:
- ""
resources:
- replicationcontrollers
- services
verbs:
- get
- list
- watch
- apiGroups:
- apps
- extensions
resources:
- replicasets
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
- statefulsets
verbs:
- get
- list
- watch
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- persistentvolumeclaims
- persistentvolumes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- "storage.k8s.io"
resources:
- storageclasses
- csinodes
verbs:
- get
- list
- watch
- apiGroups:
- "coordination.k8s.io"
resources:
- leases
verbs:
- create
- get
- list
- update
- apiGroups:
- "events.k8s.io"
resources:
- events
verbs:
- create
- patch
- update
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sample-scheduler-sa
namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: sample-scheduler-clusterrolebinding
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: sample-scheduler-clusterrole
subjects:
- kind: ServiceAccount
name: sample-scheduler-sa
namespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:
name: scheduler-config
namespace: kube-system
data:
scheduler-config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
schedulerName: sample-scheduler
leaderElection:
leaderElect: true
lockObjectName: sample-scheduler
lockObjectNamespace: kube-system
plugins:
preFilter:
enabled:
- name: "sample-plugin"
filter:
enabled:
- name: "sample-plugin"
preBind:
enabled:
- name: "sample-plugin"
pluginConfig:
- name: "sample-plugin"
args:
favorite_color: "#326CE5"
favorite_number: 7
thanks_to: "thockin"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-scheduler
namespace: kube-system
labels:
component: sample-scheduler
spec:
replicas: 1
selector:
matchLabels:
component: sample-scheduler
template:
metadata:
labels:
component: sample-scheduler
spec:
serviceAccount: sample-scheduler-sa
priorityClassName: system-cluster-critical
volumes:
- name: scheduler-config
configMap:
name: scheduler-config
containers:
- name: scheduler-ctrl
image: cnych/sample-scheduler:v0.1.6
imagePullPolicy: IfNotPresent
args:
- sample-scheduler-framework
- --config=/etc/kubernetes/scheduler-config.yaml
- --v=3
resources:
requests:
cpu: "50m"
volumeMounts:
- name: scheduler-config
mountPath: /etc/kubernetes
这样在我们在部署容器时,只需要通过手动指定了一个 schedulerName 的字段,将其设置成上面我们自定义的调度器名称 sample-scheduler即可。