kube-scheduler 是 kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理、更加充分的利用集群的资源,这也是我们选择使用 kubernetes 一个非常重要的理由。
默认情况下,kube-scheduler 提供的默认调度器能够满足我们绝大多数的要求,我们前面和大家接触的示例也基本上用的默认的策略,都可以保证我们的 Pod 可以被分配到资源充足的节点上运行。但是在实际的线上项目中,可能我们自己会比 kubernetes 更加了解我们自己的应用,比如我们希望一个 Pod 只能运行在特定的几个节点上,或者这几个节点只能用来运行特定类型的应用,这就需要我们的调度器能够可控。
kube-scheduler 的主要作用就是根据特定的调度算法和调度策略将 Pod 调度到合适的 Node 节点上去,是一个独立的二进制程序,启动之后会一直监听 API Server,获取到 PodSpec.NodeName 为空的 Pod,对每个 Pod 都会创建一个 binding。
这个过程在我们看来好像比较简单,但在实际的生产环境中,需要考虑的问题就有很多了:
考虑到实际环境中的各种复杂情况,kubernetes 的调度器采用插件化的形式实现,可以方便用户进行定制或者二次开发,我们可以自定义一个调度器并以插件形式和 kubernetes 进行集成。
kubernetes 调度器的源码位于 kubernetes/pkg/scheduler 中,大体的代码目录结构如下所示:(不同的版本目录结构可能不太一样)
kubernetes/pkg/scheduler
-- scheduler.go //调度相关的具体实现
|-- algorithm
| |-- predicates //节点筛选策略
| |-- priorities //节点打分策略
|-- algorithmprovider
| |-- defaults //定义默认的调度器
其中 Scheduler 创建和运行的核心程序,对应的代码在 pkg/scheduler/scheduler.go,如果要查看kube-scheduler 的入口程序,对应的代码在 cmd/kube-scheduler/scheduler.go。
调度主要分为以下几个部分:
Predicates 阶段首先遍历全部节点,过滤掉不满足条件的节点,属于强制性规则,这一阶段输出的所有满足要求的节点将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么 Pod 将会一直处于 Pending 状态,直到有节点满足条件,在这期间调度器会不断的重试。
所以我们在部署应用的时候,如果发现有 Pod 一直处于 Pending 状态,那么就是没有满足调度条件的节点,这个时候可以去检查下节点资源是否可用。
Priorities 阶段即再次对节点进行筛选,如果有多个节点都满足条件的话,那么系统会按照节点的优先级(priorites)大小对节点进行排序,最后选择优先级最高的节点来部署 Pod 应用。
下面是调度过程的简单示意图:
更详细的流程是这样的:
其中 Predicates 过滤有一系列的默认调度策略算法可以使用,我们这里简单列举几个:
除了这些过滤算法之外,还有一些其他的算法,更多更详细的我们可以查看源码文件:k8s.io/kubernetes/pkg/scheduler/algorithm/predicates/predicates.go。
而 Priorities 优先级是由一系列键值对组成的,键是该优先级的名称,值是它的权重值,同样,我们这里给大家列举几个具有代表性的选项:
除了这些策略之外,还有很多其他的策略,同样我们可以查看源码文件:k8s.io/kubernetes/pkg/scheduler/algorithm/priorities/ 了解更多信息。每一个优先级函数会返回一个 0-10 的分数,分数越高表示节点越优,同时每一个函数也会对应一个表示权重的值。最终主机的得分用以下公式计算得出:
finalScoreNode = (weight1 * priorityFunc1) + (weight2 * priorityFunc2) + … + (weightn * priorityFuncn)
作为 kubernetes 集群的默认调度器,kube-scheduler 主要负责将 Pod 调度到集群的 Node 上。在一个集群中,满足一个 Pod 调度请求的所有节点称之为 可调度 Node,调度器先在集群中找到一个 Pod 的可调度 Node,然后根据一系列函数对这些可调度 Node 进行打分,之后选出其中得分最高的 Node 来运行 Pod,最后,调度器将这个调度决定告知 kube-apiserver,这个过程叫做绑定。
在 Kubernetes 1.12 版本之前,kube-scheduler 会检查集群中所有节点的可调度性,并且给可调度节点打分。Kubernetes 1.12 版本添加了一个新的功能,允许调度器在找到一定数量的可调度节点之后就停止继续寻找可调度节点。该功能能提高调度器在大规模集群下的调度性能,这个数值是集群规模的百分比,这个百分比通过 percentageOfNodesToScore 参数来进行配置,其值的范围在 1 到 100 之间,最大值就是 100%,如果设置为 0 就代表没有提供这个参数配置。Kubernetes 1.14 版本又加入了一个特性,在该参数没有被用户配置的情况下,调度器会根据集群的规模自动设置一个集群比例,然后通过这个比例筛选一定数量的可调度节点进入打分阶段。该特性使用线性公式计算出集群比例,比如100个节点的集群下会取 50%,在 5000节点的集群下取 10%,这个自动设置的参数的最低值是 5%,换句话说,调度器至少会对集群中 5% 的节点进行打分,除非用户将该参数设置的低于 5。
注意: 当集群中的可调度节点少于 50 个时,调度器仍然会去检查所有节点,因为可调度节点太少,不足以停止调度器最初的过滤选择。如果我们想要关掉这个范围参数,可以将 percentageOfNodesToScore 值设置成 100。
percentageOfNodesToScore 的值必须在 1 到 100 之间,而且其默认值是通过集群的规模计算得来的,另外 50 个 Node 的数值是硬编码在程序里面的,设置这个值的作用在于:当集群的规模是数百个节点并且 percentageOfNodesToScore 参数设置的过低的时候,调度器筛选到的可调度节点数目基本不会受到该参数影响。当集群规模较小时,这个设置对调度器性能提升并不明显,但是在超过 1000 个 Node 的集群中,将调优参数设置为一个较低的值可以很明显的提升调度器性能。
不过值得注意的是,该参数设置后可能会导致只有集群中少数节点被选为可调度节点,很多 Node 都没有进入到打分阶段,这样就会造成一种后果,一个本来可以在打分阶段得分很高的 Node 甚至都不能进入打分阶段,由于这个原因,所以这个参数不应该被设置成一个很低的值,通常的做法是不会将这个参数的值设置的低于 10,很低的参数值一般在调度器的吞吐量很高且对 Node 的打分不重要的情况下才使用。换句话说,只有当你更倾向于在可调度节点中任意选择一个 Node 来运行这个 Pod 时,才使用很低的参数设置。
如果你的集群规模只有数百个节点或者更少,实际上并不推荐你将这个参数设置得比默认值更低,因为这种情况下不太会有效的提高调度器性能。
一般来说,我们有4种扩展 Kubernetes 调度器的方法。
这里我们可以简单介绍下后面两种方式的实现。
在进入调度器扩展程序之前,我们再来了解下 Kubernetes 调度程序是如何工作的:
可以通过查看官方文档,可以通过 --config
参数指定调度器将使用哪些参数,该配置文件应该包含一个 KubeSchedulerConfiguration 对象,如下所示格式:(/etc/kubernetes/scheduler-extender.yaml)
# 通过"--config" 传递文件内容
apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: "/etc/kubernetes/scheduler.conf"
algorithmSource:
policy:
file:
path: "/etc/kubernetes/scheduler-extender-policy.yaml" # 指定自定义调度策略文件
我们在这里应该输入的关键参数是 algorithmSource.policy,这个策略文件可以是本地文件也可以是 ConfigMap 资源对象,这取决于调度程序的部署方式,比如我们这里默认的调度器是静态 Pod 方式启动的,所以我们可以用本地文件的形式来配置。
该策略文件 /etc/kubernetes/scheduler-extender-policy.yaml 应该遵循 kubernetes/pkg/scheduler/apis/config/legacy_types.go#L28 的要求,在我们这里的 v1.16.2 版本中已经支持 JSON 和 YAML 两种格式的策略文件,下面是我们定义的一个简单的示例,可以查看 Extender 描述了解策略文件的定义规范:
apiVersion: v1
kind: Policy
extenders:
- urlPrefix: "http://127.0.0.1:8888/"
filterVerb: "filter"
prioritizeVerb: "prioritize"
weight: 1
enableHttps: false
我们这里的 Policy 策略文件是通过定义 extenders 来扩展调度器的,有时候我们不需要去编写代码,可以直接在该配置文件中通过指定 predicates 和 priorities 来进行自定义,如果没有指定则会使用默认的 DefaultProvier。
{
"kind": "Policy",
"apiVersion": "v1",
"predicates": [{
"name": "MatchNodeSelector"
}, {
"name": "PodFitsResources"
}, {
"name": "PodFitsHostPorts"
},{
"name": "HostName"
}
],
"priorities": [{
"name": "EqualPriority",
"weight": 2
}, {
"name": "ImageLocalityPriority",
"weight": 4
}, {
"name": "LeastRequestedPriority",
"weight": 2
}, {
"name": "BalancedResourceAllocation",
"weight": 2
}
],
"extenders": [{
"urlPrefix": "/prefix",
"filterVerb": "filter",
"prioritizeVerb": "prioritize",
"weight": 1,
"bindVerb": "bind",
"enableHttps": false
}]
}
改策略文件定义了一个 HTTP 的扩展程序服务,该服务运行在 127.0.0.1:8888 下面,并且已经将该策略注册到了默认的调度器中,这样在过滤和打分阶段结束后,可以将结果分别传递给该扩展程序的端点 / 和 /,在扩展程序中,我们可以进一步过滤并确定优先级,以适应我们的特定业务需求。
我们直接用 golang 来实现一个简单的调度器扩展程序,当然你可以使用其他任何编程语言,如下所示:
func main() {
router := httprouter.New()
router.GET("/", Index)
router.POST("/filter", Filter)
router.POST("/prioritize", Prioritize)
log.Fatal(http.ListenAndServe(":8888", router))
}
然后接下来我们需要实现 /filter 和 /prioritize 两个端点的处理程序。
其中 Filter 这个扩展函数接收一个输入类型为 schedulerapi.ExtenderArgs 的参数,然后返回一个类型为 *schedulerapi.ExtenderFilterResult
的值。在函数中,我们可以进一步过滤输入的节点:
// filter 根据扩展程序定义的预选规则来过滤节点
func filter(args schedulerapi.ExtenderArgs) *schedulerapi.ExtenderFilterResult {
var filteredNodes []v1.Node
failedNodes := make(schedulerapi.FailedNodesMap)
pod := args.Pod
for _, node := range args.Nodes.Items {
fits, failReasons, _ := podFitsOnNode(pod, node)
if fits {
filteredNodes = append(filteredNodes, node)
} else {
failedNodes[node.Name] = strings.Join(failReasons, ",")
}
}
result := schedulerapi.ExtenderFilterResult{
Nodes: &v1.NodeList{
Items: filteredNodes,
},
FailedNodes: failedNodes,
Error: "",
}
return &result
}
在过滤函数中,我们循环每个节点然后用我们自己实现的业务逻辑来判断是否应该批准该节点,这里我们实现比较简单,在 podFitsOnNode() 函数中我们只是简单的检查随机数是否为偶数来判断即可,如果是的话我们就认为这是一个幸运的节点,否则拒绝批准该节点。
var predicatesSorted = []string{LuckyPred}
var predicatesFuncs = map[string]FitPredicate{
LuckyPred: LuckyPredicate,
}
type FitPredicate func(pod *v1.Pod, node v1.Node) (bool, []string, error)
func podFitsOnNode(pod *v1.Pod, node v1.Node) (bool, []string, error) {
fits := true
var failReasons []string
for _, predicateKey := range predicatesSorted {
fit, failures, err := predicatesFuncs[predicateKey](pod, node)
if err != nil {
return false, nil, err
}
fits = fits && fit
failReasons = append(failReasons, failures...)
}
return fits, failReasons, nil
}
func LuckyPredicate(pod *v1.Pod, node v1.Node) (bool, []string, error) {
lucky := rand.Intn(2) == 0
if lucky {
log.Printf("pod %v/%v is lucky to fit on node %v\n", pod.Name, pod.Namespace, node.Name)
return true, nil, nil
}
log.Printf("pod %v/%v is unlucky to fit on node %v\n", pod.Name, pod.Namespace, node.Name)
return false, []string{LuckyPredFailMsg}, nil
}
同样的打分功能用同样的方式来实现,我们在每个节点上随机给出一个分数:
// it's webhooked to pkg/scheduler/core/generic_scheduler.go#PrioritizeNodes()
// 这个函数输出的分数会被添加会默认的调度器
func prioritize(args schedulerapi.ExtenderArgs) *schedulerapi.HostPriorityList {
pod := args.Pod
nodes := args.Nodes.Items
hostPriorityList := make(schedulerapi.HostPriorityList, len(nodes))
for i, node := range nodes {
score := rand.Intn(schedulerapi.MaxPriority + 1) // 在最大优先级内随机取一个值
log.Printf(luckyPrioMsg, pod.Name, pod.Namespace, score)
hostPriorityList[i] = schedulerapi.HostPriority{
Host: node.Name,
Score: score,
}
}
return &hostPriorityList
}
然后我们可以使用下面的命令来编译打包我们的应用:
$ GOOS=linux GOARCH=amd64 go build -o app
代码: 本节调度器扩展程序完整的代码获取地址:https://github.com/cnych/sample-scheduler-extender。
构建完成后,将应用 app 拷贝到 kube-scheduler 所在的节点直接运行即可。现在我们就可以将上面的策略文件配置到 kube-scheduler 组件中去了,我们这里集群是 kubeadm 搭建的,所以直接修改文件 /etc/kubernetes/manifests/kube-schduler.yaml 文件即可,内容如下所示:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: kube-scheduler
tier: control-plane
name: kube-scheduler
namespace: kube-system
spec:
containers:
- command:
- kube-scheduler
- --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
- --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
- --bind-address=127.0.0.1
- --kubeconfig=/etc/kubernetes/scheduler.conf
- --leader-elect=true
- --config=/etc/kubernetes/scheduler-extender.yaml
- --v=9
image: gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.2
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: 127.0.0.1
path: /healthz
port: 10251
scheme: HTTP
initialDelaySeconds: 15
timeoutSeconds: 15
name: kube-scheduler
resources:
requests:
cpu: 100m
volumeMounts:
- mountPath: /etc/kubernetes/scheduler.conf
name: kubeconfig
readOnly: true
- mountPath: /etc/kubernetes/scheduler-extender.yaml
name: extender
readOnly: true
- mountPath: /etc/kubernetes/scheduler-extender-policy.yaml
name: extender-policy
readOnly: true
hostNetwork: true
priorityClassName: system-cluster-critical
volumes:
- hostPath:
path: /etc/kubernetes/scheduler.conf
type: FileOrCreate
name: kubeconfig
- hostPath:
path: /etc/kubernetes/scheduler-extender.yaml
type: FileOrCreate
name: extender
- hostPath:
path: /etc/kubernetes/scheduler-extender-policy.yaml
type: FileOrCreate
name: extender-policy
status: {}
注意: 当然我们这个地方是直接在系统默认的 kube-scheduler 上面配置的,我们也可以复制一个调度器的 YAML 文件然后更改下 schedulerName 来部署,这样就不会影响默认的调度器了,然后在需要使用这个测试的调度器的 Pod 上面指定 spec.schedulerName 即可。对于多调度器的使用可以查看官方文档 配置多个调度器。
kube-scheduler 重新配置后可以查看日志来验证是否重启成功,需要注意的是一定需要将 /etc/kubernetes/scheduler-extender.yaml 和 /etc/kubernetes/scheduler-extender-policy.yaml 两个文件挂载到 Pod 中去:
$ kubectl logs -f kube-scheduler-ydzs-master -n kube-system
I0102 15:17:38.824657 1 serving.go:319] Generated self-signed cert in-memory
I0102 15:17:39.472276 1 server.go:143] Version: v1.16.2
I0102 15:17:39.472674 1 defaults.go:91] TaintNodesByCondition is enabled, PodToleratesNodeTaints predicate is mandatory
W0102 15:17:39.479704 1 authorization.go:47] Authorization is disabled
W0102 15:17:39.479733 1 authentication.go:79] Authentication is disabled
I0102 15:17:39.479777 1 deprecated_insecure_serving.go:51] Serving healthz insecurely on [::]:10251
I0102 15:17:39.480559 1 secure_serving.go:123] Serving securely on 127.0.0.1:10259
I0102 15:17:39.682180 1 leaderelection.go:241] attempting to acquire leader lease kube-system/kube-scheduler...
I0102 15:17:56.500505 1 leaderelection.go:251] successfully acquired lease kube-system/kube-scheduler
到这里我们就创建并配置了一个非常简单的调度扩展程序,现在我们来运行一个 Deployment 查看其工作原理,我们准备一个包含20个副本的部署 Yaml:(test-scheduler.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: pause
spec:
replicas: 20
selector:
matchLabels:
app: pause
template:
metadata:
labels:
app: pause
spec:
containers:
- name: pause
image: gcr.azk8s.cn/google_containers/pause:3.1
直接创建上面的资源对象:
$ kuectl apply -f test-scheduler.yaml
deployment.apps/pause created
这个时候我们去查看下我们编写的调度器扩展程序日志:
$ ./app
......
2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is unlucky to fit on node ydzs-node1
2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is lucky to get score 7
2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is lucky to get score 9
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is unlucky to fit on node ydzs-node3
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is unlucky to fit on node ydzs-node4
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to fit on node ydzs-node1
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to fit on node ydzs-node2
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to get score 4
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to get score 8
......
我们可以看到 Pod 调度的过程,另外默认调度程序会定期重试失败的 Pod,因此它们将一次又一次地重新传递到我们的调度扩展程序上,我们的逻辑是检查随机数是否为偶数,所以最终所有 Pod 都将处于运行状态。
调度器扩展程序可能是在一些情况下可以满足我们的需求,但是他仍然有一些限制和缺点:
由于这些局限性,Kubernetes 调度小组就提出了上面第四种方法来进行更好的扩展,也就是调度框架(Scheduler Framework),它基本上可以解决我们遇到的所有难题,现在也已经成官方推荐的扩展方式,所以这将是以后扩展调度器的最主流的方式。
调度框架定义了一组扩展点,用户可以实现扩展点定义的接口来定义自己的调度逻辑(我们称之为扩展),并将扩展注册到扩展点上,调度框架在执行调度工作流时,遇到对应的扩展点时,将调用用户注册的扩展。调度框架在预留扩展点时,都是有特定的目的,有些扩展点上的扩展可以改变调度程序的决策方法,有些扩展点上的扩展只是发送一个通知。
我们知道每当调度一个 Pod 时,都会按照两个过程来执行:调度过程和绑定过程。
调度过程为 Pod 选择一个合适的节点,绑定过程则将调度过程的决策应用到集群中(也就是在被选定的节点上运行 Pod),将调度过程和绑定过程合在一起,称之为调度上下文(scheduling context)。需要注意的是调度过程是同步运行的(同一时间点只为一个 Pod 进行调度),绑定过程可异步运行(同一时间点可并发为多个 Pod 执行绑定)。
调度过程和绑定过程遇到如下情况时会中途退出:
这个时候,该 Pod 将被放回到 待调度队列,并等待下次重试。
下图展示了调度框架中的调度上下文及其中的扩展点,一个扩展可以注册多个扩展点,以便可以执行更复杂的有状态的任务。
如果我们要实现自己的插件,必须向调度框架注册插件并完成配置,另外还必须实现扩展点接口,对应的扩展点接口我们可以在源码 pkg/scheduler/framework/v1alpha1/interface.go 文件中找到,如下所示:
// Plugin is the parent type for all the scheduling framework plugins.
type Plugin interface {
Name() string
}
type QueueSortPlugin interface {
Plugin
Less(*PodInfo, *PodInfo) bool
}
// PreFilterPlugin is an interface that must be implemented by "prefilter" plugins.
// These plugins are called at the beginning of the scheduling cycle.
type PreFilterPlugin interface {
Plugin
PreFilter(pc *PluginContext, p *v1.Pod) *Status
}
// FilterPlugin is an interface for Filter plugins. These plugins are called at the
// filter extension point for filtering out hosts that cannot run a pod.
// This concept used to be called 'predicate' in the original scheduler.
// These plugins should return "Success", "Unschedulable" or "Error" in Status.code.
// However, the scheduler accepts other valid codes as well.
// Anything other than "Success" will lead to exclusion of the given host from
// running the pod.
type FilterPlugin interface {
Plugin
Filter(pc *PluginContext, pod *v1.Pod, nodeName string) *Status
}
// PostFilterPlugin is an interface for Post-filter plugin. Post-filter is an
// informational extension point. Plugins will be called with a list of nodes
// that passed the filtering phase. A plugin may use this data to update internal
// state or to generate logs/metrics.
type PostFilterPlugin interface {
Plugin
PostFilter(pc *PluginContext, pod *v1.Pod, nodes []*v1.Node, filteredNodesStatuses NodeToStatusMap) *Status
}
// ScorePlugin is an interface that must be implemented by "score" plugins to rank
// nodes that passed the filtering phase.
type ScorePlugin interface {
Plugin
Score(pc *PluginContext, p *v1.Pod, nodeName string) (int, *Status)
}
// ScoreWithNormalizePlugin is an interface that must be implemented by "score"
// plugins that also need to normalize the node scoring results produced by the same
// plugin's "Score" method.
type ScoreWithNormalizePlugin interface {
ScorePlugin
NormalizeScore(pc *PluginContext, p *v1.Pod, scores NodeScoreList) *Status
}
// ReservePlugin is an interface for Reserve plugins. These plugins are called
// at the reservation point. These are meant to update the state of the plugin.
// This concept used to be called 'assume' in the original scheduler.
// These plugins should return only Success or Error in Status.code. However,
// the scheduler accepts other valid codes as well. Anything other than Success
// will lead to rejection of the pod.
type ReservePlugin interface {
Plugin
Reserve(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}
// PreBindPlugin is an interface that must be implemented by "prebind" plugins.
// These plugins are called before a pod being scheduled.
type PreBindPlugin interface {
Plugin
PreBind(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}
// PostBindPlugin is an interface that must be implemented by "postbind" plugins.
// These plugins are called after a pod is successfully bound to a node.
type PostBindPlugin interface {
Plugin
PostBind(pc *PluginContext, p *v1.Pod, nodeName string)
}
// UnreservePlugin is an interface for Unreserve plugins. This is an informational
// extension point. If a pod was reserved and then rejected in a later phase, then
// un-reserve plugins will be notified. Un-reserve plugins should clean up state
// associated with the reserved Pod.
type UnreservePlugin interface {
Plugin
Unreserve(pc *PluginContext, p *v1.Pod, nodeName string)
}
// PermitPlugin is an interface that must be implemented by "permit" plugins.
// These plugins are called before a pod is bound to a node.
type PermitPlugin interface {
Plugin
Permit(pc *PluginContext, p *v1.Pod, nodeName string) (*Status, time.Duration)
}
// BindPlugin is an interface that must be implemented by "bind" plugins. Bind
// plugins are used to bind a pod to a Node.
type BindPlugin interface {
Plugin
Bind(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}
对于调度框架插件的启用或者禁用,我们同样可以使用上面的 KubeSchedulerConfiguration 资源对象来进行配置。下面的例子中的配置启用了一个实现了 reserve 和 preBind 扩展点的插件,并且禁用了另外一个插件,同时为插件 foo 提供了一些配置信息:
apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
...
plugins:
reserve:
enabled:
- name: foo
- name: bar
disabled:
- name: baz
preBind:
enabled:
- name: foo
disabled:
- name: baz
pluginConfig:
- name: foo
args: >
foo插件可以解析的任意内容
扩展的调用顺序如下:
假设默认插件 foo 实现了 reserve 扩展点,此时我们要添加一个插件 bar,想要在 foo 之前被调用,则应该先禁用 foo 再按照 bar foo 的顺序激活。示例配置如下所示:
apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
...
plugins:
reserve:
enabled:
- name: bar
- name: foo
disabled:
- name: foo
在源码目录 pkg/scheduler/framework/plugins/examples 中有几个示范插件,我们可以参照其实现方式。
其实要实现一个调度框架的插件,并不难,我们只要实现对应的扩展点,然后将插件注册到调度器中即可,下面是默认调度器在初始化的时候注册的插件:
func NewRegistry() Registry {
return Registry{
// FactoryMap:
// New plugins are registered here.
// example:
// {
// stateful_plugin.Name: stateful.NewStatefulMultipointExample,
// fooplugin.Name: fooplugin.New,
// }
}
}
但是可以看到默认并没有注册一些插件,所以要想让调度器能够识别我们的插件代码,就需要自己来实现一个调度器了,当然这个调度器我们完全没必要完全自己实现,直接调用默认的调度器,然后在上面的 NewRegistry() 函数中将我们的插件注册进去即可。在 kube-scheduler 的源码文件 kubernetes/cmd/kube-scheduler/app/server.go 中有一个 NewSchedulerCommand 入口函数,其中的参数是一个类型为 Option 的列表,而这个 Option 恰好就是一个插件配置的定义:
// Option configures a framework.Registry.
type Option func(framework.Registry) error
// NewSchedulerCommand creates a *cobra.Command object with default parameters and registryOptions
func NewSchedulerCommand(registryOptions ...Option) *cobra.Command {
......
}
所以我们完全就可以直接调用这个函数来作为我们的函数入口,并且传入我们自己实现的插件作为参数即可,而且该文件下面还有一个名为 WithPlugin 的函数可以来创建一个 Option 实例:
// WithPlugin creates an Option based on plugin name and factory.
func WithPlugin(name string, factory framework.PluginFactory) Option {
return func(registry framework.Registry) error {
return registry.Register(name, factory)
}
}
所以最终我们的入口函数如下所示:
func main() {
rand.Seed(time.Now().UTC().UnixNano())
command := app.NewSchedulerCommand(
app.WithPlugin(sample.Name, sample.New),
)
logs.InitLogs()
defer logs.FlushLogs()
if err := command.Execute(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
其中 app.WithPlugin(sample.Name, sample.New) 就是我们接下来要实现的插件,从 WithPlugin 函数的参数也可以看出我们这里的 sample.New 必须是一个 framework.PluginFactory 类型的值,而 PluginFactory 的定义就是一个函数:
type PluginFactory = func(configuration *runtime.Unknown, f FrameworkHandle) (Plugin, error)
所以 sample.New 实际上就是上面的这个函数,在这个函数中我们可以获取到插件中的一些数据然后进行逻辑处理即可,插件实现如下所示,我们这里只是简单获取下数据打印日志,如果你有实际需求的可以根据获取的数据就行处理即可,我们这里只是实现了 PreFilter、Filter、PreBind 三个扩展点,其他的可以用同样的方式来扩展即可:
// 插件名称
const Name = "sample-plugin"
type Args struct {
FavoriteColor string `json:"favorite_color,omitempty"`
FavoriteNumber int `json:"favorite_number,omitempty"`
ThanksTo string `json:"thanks_to,omitempty"`
}
type Sample struct {
args *Args
handle framework.FrameworkHandle
}
func (s *Sample) Name() string {
return Name
}
func (s *Sample) PreFilter(pc *framework.PluginContext, pod *v1.Pod) *framework.Status {
klog.V(3).Infof("prefilter pod: %v", pod.Name)
return framework.NewStatus(framework.Success, "")
}
func (s *Sample) Filter(pc *framework.PluginContext, pod *v1.Pod, nodeName string) *framework.Status {
klog.V(3).Infof("filter pod: %v, node: %v", pod.Name, nodeName)
return framework.NewStatus(framework.Success, "")
}
func (s *Sample) PreBind(pc *framework.PluginContext, pod *v1.Pod, nodeName string) *framework.Status {
if nodeInfo, ok := s.handle.NodeInfoSnapshot().NodeInfoMap[nodeName]; !ok {
return framework.NewStatus(framework.Error, fmt.Sprintf("prebind get node info error: %+v", nodeName))
} else {
klog.V(3).Infof("prebind node info: %+v", nodeInfo.Node())
return framework.NewStatus(framework.Success, "")
}
}
//type PluginFactory = func(configuration *runtime.Unknown, f FrameworkHandle) (Plugin, error)
func New(configuration *runtime.Unknown, f framework.FrameworkHandle) (framework.Plugin, error) {
args := &Args{}
if err := framework.DecodeInto(configuration, args); err != nil {
return nil, err
}
klog.V(3).Infof("get plugin config args: %+v", args)
return &Sample{
args: args,
handle: f,
}, nil
}
完整代码可以前往仓库 https://github.com/cnych/sample-scheduler-framework 获取。
实现完成后,编译打包成镜像即可,然后我们就可以当成普通的应用用一个 Deployment 控制器来部署即可,由于我们需要去获取集群中的一些资源对象,所以当然需要申请 RBAC 权限,然后同样通过 --config 参数来配置我们的调度器,同样还是使用一个 KubeSchedulerConfiguration 资源对象配置,可以通过 plugins 来启用或者禁用我们实现的插件,也可以通过 pluginConfig 来传递一些参数值给插件:
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
直接部署上面的资源对象即可,这样我们就部署了一个名为 sample-scheduler 的调度器了,接下来我们可以部署一个应用来使用这个调度器进行调度:
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-scheduler
spec:
replicas: 1
selector:
matchLabels:
app: test-scheduler
template:
metadata:
labels:
app: test-scheduler
spec:
schedulerName: sample-scheduler
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: nginx
ports:
- containerPort: 80
这里需要注意的是我们现在手动指定了一个 schedulerName 的字段,将其设置成上面我们自定义的调度器名称 sample-scheduler。
我们直接创建这个资源对象,创建完成后查看我们自定义调度器的日志信息:
$ kubectl get pods -n kube-system -l component=sample-scheduler
NAME READY STATUS RESTARTS AGE
sample-scheduler-7c469787f-rwhhd 1/1 Running 0 13m
$ kubectl logs -f sample-scheduler-7c469787f-rwhhd -n kube-system
I0104 08:24:22.087881 1 scheduler.go:530] Attempting to schedule pod: default/test-scheduler-6d779d9465-rq2bb
I0104 08:24:22.087992 1 plugins.go:23] prefilter pod: test-scheduler-6d779d9465-rq2bb
I0104 08:24:22.088657 1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-node1
I0104 08:24:22.088797 1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-node2
I0104 08:24:22.088871 1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-node3
I0104 08:24:22.088946 1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-node4
I0104 08:24:22.088992 1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-master
I0104 08:24:22.090653 1 plugins.go:36] prebind node info: &Node{ObjectMeta:{ydzs-node3 /api/v1/nodes/ydzs-node3 1ff6e228-4d98-4737-b6d3-30a5d55ccdc2 15466372 0 2019-11-10 09:05:09 +0000 UTC <nil> <nil> ......}
I0104 08:24:22.091761 1 factory.go:610] Attempting to bind test-scheduler-6d779d9465-rq2bb to ydzs-node3
I0104 08:24:22.104994 1 scheduler.go:667] pod default/test-scheduler-6d779d9465-rq2bb is bound successfully on node "ydzs-node3", 5 nodes evaluated, 4 nodes were found feasible. Bound node resource: "Capacity: CPU<4>|Memory<8008820Ki>|Pods<110>|StorageEphemeral<17921Mi>; Allocatable: CPU<4>|Memory<7906420Ki>|Pods<110>|StorageEphemeral<16912377419>.".
可以看到当我们创建完 Pod 后,在我们自定义的调度器中就出现了对应的日志,并且在我们定义的扩展点上面都出现了对应的日志,证明我们的示例成功了,也可以通过查看 Pod 的 schedulerName 来验证:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
test-scheduler-6d779d9465-rq2bb 1/1 Running 0 22m
$ kubectl get pod test-scheduler-6d779d9465-rq2bb -o yaml
......
restartPolicy: Always
schedulerName: sample-scheduler
securityContext: {}
serviceAccount: default
......
在最新的 Kubernetes v1.17 版本中,Scheduler Framework 内置的预选和优选函数已经全部插件化,所以要扩展调度器我们应该掌握并理解调度框架这种方式。
与前面所讲的调度优选策略中的优先级(Priorities)不同,前面所讲的优先级指的是节点优先级,而我们这里所说的优先级指的是 Pod 的优先级,高优先级的 Pod 会优先被调度,或者在资源不足低情况牺牲低优先级的 Pod,以便于重要的 Pod 能够得到资源部署。
要定义 Pod 优先级,就需要先定义 PriorityClass 对象,该对象没有 Namespace 的限制:
apiVersion: v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."
其中:
然后通过在 Pod 的 spec.priorityClassName 中指定已定义的 PriorityClass 名称即可:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
另外一个值得注意的是当节点没有足够的资源供调度器调度 Pod,导致 Pod 处于 pending 时,抢占(preemption)逻辑就会被触发,抢占会尝试从一个节点删除低优先级的 Pod,从而释放资源使高优先级的 Pod 得到节点资源进行部署。
现在我们通过下面的图再去回顾下 kubernetes 的调度过程就清晰很多了:
一般情况下我们部署的 Pod 是通过集群的自动调度策略来选择节点的,默认情况下调度器考虑的是资源足够,并且负载尽量平均,但是有的时候我们需要能够更加细粒度的去控制 Pod 的调度,比如我们希望一些机器学习的应用只跑在有 GPU 的节点上;但是有的时候我们的服务之间交流比较频繁,又希望能够将这服务的 Pod 都调度到同一个的节点上。这就需要使用一些调度方式来控制 Pod 的调度了,主要有两个概念:亲和性和反亲和性,亲和性又分成节点亲和性(nodeAffinity)和 Pod 亲和性(podAffinity)。
在了解亲和性之前,我们先来了解一个非常常用的调度方式:nodeSelector。我们知道 label 标签是 kubernetes 中一个非常重要的概念,用户可以非常灵活的利用 label 来管理集群中的资源,比如最常见的 Service 对象通过 label 去匹配 Pod 资源,而 Pod 的调度也可以根据节点的 label 来进行调度。
我们可以通过下面的命令查看我们的 node 的 label:
$ kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
ydzs-master Ready master 55d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-master,kubernetes.io/os=linux,node-role.kubernetes.io/master=
ydzs-node1 Ready <none> 55d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-node1,kubernetes.io/os=linux
ydzs-node2 Ready <none> 55d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-node2,kubernetes.io/os=linux
ydzs-node3 Ready <none> 53d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-node3,kubernetes.io/os=linux,monitor=prometheus
ydzs-node4 Ready <none> 53d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-node4,kubernetes.io/os=linux
现在我们先给节点 ydzs-node2 增加一个com=youdianzhishi的标签,命令如下:
$ kubectl label nodes ydzs-node2 com=youdianzhishi
node/ydzs-node2 labeled
我们可以通过上面的 --show-labels 参数可以查看上述标签是否生效。当节点被打上了相关标签后,在调度的时候就可以使用这些标签了,只需要在 Pod 的 spec 字段中添加 nodeSelector 字段,里面是我们需要被调度的节点的 label 标签,比如,下面的 Pod 我们要强制调度到 ydzs-node2 这个节点上去,我们就可以使用 nodeSelector 来表示了:(node-selector-demo.yaml)
apiVersion: v1
kind: Pod
metadata:
labels:
app: busybox-pod
name: test-busybox
spec:
containers:
- command:
- sleep
- "3600"
image: busybox
imagePullPolicy: Always
name: test-busybox
nodeSelector:
com: youdianzhishi
然后我们可以通过 describe 命令查看调度结果:
$ kubectl apply -f pod-selector-demo.yaml
pod/test-busybox created
$ kubectl describe pod test-busybox
Name: test-busybox
Namespace: default
Priority: 0
Node: ydzs-node2/10.151.30.23
......
Node-Selectors: com=youdianzhishi
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled <unknown> default-scheduler Successfully assigned default/test-busybox to ydzs-node2
Normal Pulling 13s kubelet, ydzs-node2 Pulling image "busybox"
Normal Pulled 10s kubelet, ydzs-node2 Successfully pulled image "busybox"
Normal Created 10s kubelet, ydzs-node2 Created container test-busybox
Normal Started 9s kubelet, ydzs-node2 Started container test-busybox
我们可以看到 Events 下面的信息,我们的 Pod 通过默认的 default-scheduler 调度器被绑定到了 ydzs-node2 节点。不过需要注意的是nodeSelector 属于强制性的,如果我们的目标节点没有可用的资源,我们的 Pod 就会一直处于 Pending 状态。
通过上面的例子我们可以感受到 nodeSelector 的方式比较直观,但是还够灵活,控制粒度偏大,接下来我们再和大家了解下更加灵活的方式:节点亲和性(nodeAffinity)。
前面我们了解了 kubernetes 调度器的调度流程,我们知道默认的调度器在使用的时候,经过了 predicates 和 priorities 两个阶段,但是在实际的生产环境中,往往我们需要根据自己的一些实际需求来控制 Pod 的调度,这就需要用到 nodeAffinity(节点亲和性)、podAffinity(pod 亲和性) 以及 podAntiAffinity(pod 反亲和性)。
亲和性调度可以分成软策略和硬策略两种方式:
对于亲和性和反亲和性都有这两种规则可以设置: preferredDuringSchedulingIgnoredDuringExecution 和requiredDuringSchedulingIgnoredDuringExecution,前面的就是软策略,后面的就是硬策略。
节点亲和性(nodeAffinity)主要是用来控制 Pod 要部署在哪些节点上,以及不能部署在哪些节点上的,它可以进行一些简单的逻辑组合了,不只是简单的相等匹配。
比如现在我们用一个 Deployment 来管理8个 Pod 副本,现在我们来控制下这些 Pod 的调度,如下例子:(node-affinity-demo.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-affinity
labels:
app: node-affinity
spec:
replicas: 8
selector:
matchLabels:
app: node-affinity
template:
metadata:
labels:
app: node-affinity
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
name: nginxweb
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬策略
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- ydzs-node3
preferredDuringSchedulingIgnoredDuringExecution: # 软策略
- weight: 1
preference:
matchExpressions:
- key: com
operator: In
values:
- youdianzhishi
上面这个 Pod 首先是要求不能运行在 ydzs-node3 这个节点上,如果有个节点满足 com=youdianzhishi 的话就优先调度到这个节点上。
由于上面 ydzs-node02 节点我们打上了 com=youdianzhishi 这样的 label 标签,所以按要求会优先调度到这个节点来的,现在我们来创建这个 Pod,然后查看具体的调度情况是否满足我们的要求。
$ kubectl apply -f node-affinty-demo.yaml
deployment.apps/node-affinity created
$ kubectl get pods -l app=node-affinity -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
node-affinity-cdd9d54d9-bgbbh 1/1 Running 0 2m28s 10.244.2.247 ydzs-node2 <none> <none>
node-affinity-cdd9d54d9-dlbck 1/1 Running 0 2m28s 10.244.4.16 ydzs-node4 <none> <none>
node-affinity-cdd9d54d9-g2jr6 1/1 Running 0 2m28s 10.244.4.17 ydzs-node4 <none> <none>
node-affinity-cdd9d54d9-gzr58 1/1 Running 0 2m28s 10.244.1.118 ydzs-node1 <none> <none>
node-affinity-cdd9d54d9-hcv7r 1/1 Running 0 2m28s 10.244.2.246 ydzs-node2 <none> <none>
node-affinity-cdd9d54d9-kvxw4 1/1 Running 0 2m28s 10.244.2.245 ydzs-node2 <none> <none>
node-affinity-cdd9d54d9-p4mmk 1/1 Running 0 2m28s 10.244.2.244 ydzs-node2 <none> <none>
node-affinity-cdd9d54d9-t5mff 1/1 Running 0 2m28s 10.244.1.117 ydzs-node1 <none> <none>
从结果可以看出有4个 Pod 被部署到了 ydzs-node2 节点上,但是可以看到并没有一个 Pod 被部署到 ydzs-node3 这个节点上,因为我们的硬策略就是不允许部署到该节点上,而 ydzs-node2 是软策略,所以会尽量满足。这里的匹配逻辑是 label 标签的值在某个列表中,现在 Kubernetes 提供的操作符有下面的几种:
但是需要注意的是如果 nodeSelectorTerms 下面有多个选项的话,满足任何一个条件就可以了;如果 matchExpressions有多个选项的话,则必须同时满足这些条件才能正常调度 Pod。
Pod 亲和性(podAffinity)主要解决 Pod 可以和哪些 Pod 部署在同一个拓扑域中的问题(其中拓扑域用主机标签实现,可以是单个主机,也可以是多个主机组成的 cluster、zone 等等),而 Pod 反亲和性主要是解决 Pod 不能和哪些 Pod 部署在同一个拓扑域中的问题,它们都是处理的 Pod 与 Pod 之间的关系,比如一个 Pod 在一个节点上了,那么我这个也得在这个节点,或者你这个 Pod 在节点上了,那么我就不想和你待在同一个节点上。
由于我们这里只有一个集群,并没有区域或者机房的概念,所以我们这里直接使用主机名来作为拓扑域,把 Pod 创建在同一个主机上面。
$ kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
ydzs-master Ready master 55d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-master,kubernetes.io/os=linux,node-role.kubernetes.io/master=
ydzs-node1 Ready <none> 55d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-node1,kubernetes.io/os=linux
ydzs-node2 Ready <none> 55d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,com=youdianzhishi,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-node2,kubernetes.io/os=linux
ydzs-node3 Ready <none> 53d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-node3,kubernetes.io/os=linux,monitor=prometheus
ydzs-node4 Ready <none> 53d v1.16.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ydzs-node4,kubernetes.io/os=linux
同样,还是针对上面的资源对象,我们来测试下 Pod 的亲和性:(pod-affinity-demo.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: pod-affinity
labels:
app: pod-affinity
spec:
replicas: 3
selector:
matchLabels:
app: pod-affinity
template:
metadata:
labels:
app: pod-affinity
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
name: nginxweb
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬策略
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- busybox-pod
topologyKey: kubernetes.io/hostname
上面这个例子中的 Pod 需要调度到某个指定的节点上,并且该节点上运行了一个带有 app=busybox-pod 标签的 Pod。我们可以查看有标签 app=busybox-pod 的 pod 列表:
$ kubectl get pods -l app=busybox-pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-busybox 1/1 Running 0 27m 10.244.2.242 ydzs-node2 <none> <none>
我们看到这个 Pod 运行在了 ydzs-node2 的节点上面,所以按照上面的亲和性来说,上面我们部署的3个 Pod 副本也应该运行在 ydzs-node2 节点上:
$ kubectl apply -f pod-affinity-demo.yaml
deployment.apps/pod-affinity created
$ kubectl get pods -o wide -l app=pod-affinity
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-affinity-587f9b5b58-5nxmf 1/1 Running 0 26s 10.244.2.249 ydzs-node2 <none> <none>
pod-affinity-587f9b5b58-m2j7s 1/1 Running 0 26s 10.244.2.248 ydzs-node2 <none> <none>
pod-affinity-587f9b5b58-vrd7b 1/1 Running 0 26s 10.244.2.250 ydzs-node2 <none> <none>
如果我们把上面的 test-busybox 和 pod-affinity 这个 Deployment 都删除,然后重新创建 pod-affinity 这个资源,看看能不能正常调度呢:
$ kubectl delete -f node-selector-demo.yaml
pod "test-busybox" deleted
$ kubectl delete -f pod-affinity-demo.yaml
deployment.apps "pod-affinity" deleted
$ kubectl apply -f pod-affinity-demo.yaml
deployment.apps/pod-affinity created
$ kubectl get pods -o wide -l app=pod-affinity
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-affinity-587f9b5b58-bbfgr 0/1 Pending 0 18s <none> <none> <none> <none>
pod-affinity-587f9b5b58-lwc8n 0/1 Pending 0 18s <none> <none> <none> <none>
pod-affinity-587f9b5b58-pc7ql 0/1 Pending 0 18s <none> <none> <none> <none>
我们可以看到都处于 Pending 状态了,这是因为现在没有一个节点上面拥有 app=busybox-pod 这个标签的 Pod,而上面我们的调度使用的是硬策略,所以就没办法进行调度了,大家可以去尝试下重新将 test-busybox 这个 Pod 调度到其他节点上,观察下上面的3个副本会不会也被调度到对应的节点上去。
我们这个地方使用的是 kubernetes.io/hostname 这个拓扑域,意思就是我们当前调度的 Pod 要和目标的 Pod 处于同一个主机上面,因为要处于同一个拓扑域下面,为了说明这个问题,我们把拓扑域改成 beta.kubernetes.io/os,同样的我们当前调度的 Pod 要和目标的 Pod 处于同一个拓扑域中,目标的 Pod 是拥有 beta.kubernetes.io/os=linux 的标签,而我们这里所有节点都有这样的标签,这也就意味着我们所有节点都在同一个拓扑域中,所以我们这里的 Pod 可以被调度到任何一个节点,重新运行上面的 app=busybox-pod 的 Pod,然后再更新下我们这里的资源对象:
$ kubectl get pods -o wide -l app=pod-affinity
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-affinity-76c56567c-792n4 1/1 Running 0 2m59s 10.244.2.254 ydzs-node2 <none> <none>
pod-affinity-76c56567c-8s2pd 1/1 Running 0 3m53s 10.244.4.18 ydzs-node4 <none> <none>
pod-affinity-76c56567c-hx7ck 1/1 Running 0 2m52s 10.244.3.23 ydzs-node3 <none> <none>
可以看到现在是分别运行在3个节点下面的,因为他们都属于 beta.kubernetes.io/os 这个拓扑域。
Pod 反亲和性(podAntiAffinity)则是反着来的,比如一个节点上运行了某个 Pod,那么我们的模板 Pod 则不希望被调度到这个节点上面去了。我们把上面的 podAffinity 直接改成 podAntiAffinity:(pod-antiaffinity-demo.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: pod-antiaffinity
labels:
app: pod-antiaffinity
spec:
replicas: 3
selector:
matchLabels:
app: pod-antiaffinity
template:
metadata:
labels:
app: pod-antiaffinity
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
name: nginxweb
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬策略
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- busybox-pod
topologyKey: kubernetes.io/hostname
这里的意思就是如果一个节点上面有一个 app=busybox-pod 这样的 Pod 的话,那么我们的 Pod 就别调度到这个节点上面来,上面我们把app=busybox-pod 这个 Pod 固定到了 ydzs-node2 这个节点上面的,所以正常来说我们这里的 Pod 不会出现在该节点上:
$ kubectl apply -f pod-antiaffinity-demo.yaml
deployment.apps/pod-antiaffinity created
$ kubectl get pods -l app=pod-antiaffinity -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-antiaffinity-84d5bf9df4-9c9qk 1/1 Running 0 73s 10.244.4.19 ydzs-node4 <none> <none>
pod-antiaffinity-84d5bf9df4-q6lkm 1/1 Running 0 67s 10.244.3.24 ydzs-node3 <none> <none>
pod-antiaffinity-84d5bf9df4-vk9tc 1/1 Running 0 57s 10.244.3.25 ydzs-node3 <none> <none>
我们可以看到没有被调度到 ydzs-node2 节点上,因为我们这里使用的是 Pod 反亲和性。大家可以思考下,如果这里我们将拓扑域更改成 beta.kubernetes.io/os 会怎么样呢?可以自己去测试下看看。
对于 nodeAffinity 无论是硬策略还是软策略方式,都是调度 Pod 到预期节点上,而污点(Taints)恰好与之相反,如果一个节点标记为 Taints ,除非 Pod 也被标识为可以容忍污点节点,否则该 Taints 节点不会被调度 Pod。
比如用户希望把 Master 节点保留给 Kubernetes 系统组件使用,或者把一组具有特殊资源预留给某些 Pod,则污点就很有用了,Pod 不会再被调度到 taint 标记过的节点。我们使用 kubeadm 搭建的集群默认就给 master 节点添加了一个污点标记,所以我们看到我们平时的 Pod 都没有被调度到 master 上去:
$ kubectl describe node ydzs-master
Name: ydzs-master
Roles: master
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
kubernetes.io/arch=amd64
kubernetes.io/hostname=ydzs-master
kubernetes.io/os=linux
node-role.kubernetes.io/master=
......
Taints: node-role.kubernetes.io/master:NoSchedule
Unschedulable: false
......
我们可以使用上面的命令查看 master 节点的信息,其中有一条关于 Taints 的信息:node-role.kubernetes.io/master:NoSchedule,就表示master 节点打了一个污点的标记,其中影响的参数是 NoSchedule,表示 Pod 不会被调度到标记为 taints 的节点,除了 NoSchedule 外,还有另外两个选项:
污点 taint 标记节点的命令如下:
$ kubectl taint nodes ydzs-node2 test=node2:NoSchedule
node "ydzs-node2" tainted
上面的命名将 ydzs-node2 节点标记为了污点,影响策略是 NoSchedule,只会影响新的 Pod 调度,如果仍然希望某个 Pod 调度到 taint 节点上,则必须在 Spec 中做出 Toleration 定义,才能调度到该节点,比如现在我们想要将一个 Pod 调度到 master 节点:(taint-demo.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: taint
labels:
app: taint
spec:
replicas: 3
selector:
matchLabels:
app: taint
template:
metadata:
labels:
app: taint
spec:
containers:
- name: nginx
image: nginx
ports:
- name: http
containerPort: 80
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
由于 master 节点被标记为了污点,所以我们这里要想 Pod 能够调度到改节点去,就需要增加容忍的声明:
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
然后创建上面的资源,查看结果:
$ kubectl apply -f taint-demo.yaml
deployment.apps "taint" created
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
......
taint-845d8bb4fb-57mhm 1/1 Running 0 1m 10.244.4.247 ydzs-node2
taint-845d8bb4fb-bbvmp 1/1 Running 0 1m 10.244.0.33 ydzs-master
taint-845d8bb4fb-zb78x 1/1 Running 0 1m 10.244.4.246 ydzs-node2
......
我们可以看到有一个 Pod 副本被调度到了 master 节点,这就是容忍的使用方法。
对于 tolerations 属性的写法,其中的 key、value、effect 与 Node 的 Taint 设置需保持一致, 还有以下几点说明:
另外,还有两个特殊值:
最后如果我们要取消节点的污点标记,可以使用下面的命令:
$ kubectl taint nodes ydzs-node2 test-
node "ydzs-node2" untainted
手机扫一扫
移动阅读更方便
你可能感兴趣的文章