crane-scheduler基于真实负载进行k8s调度

原生 kubernetes 调度器只能基于资源的 resource request 进行调度,然而 Pod 的真实资源使用率,往往与其所申请资源的 request/limit 差异很大,导致集群负载不均的问题。

背景

将服务部署在Kubernetes集群上是当今许多企业的首选方案,其能帮助企业自动化部署、弹性伸缩以及容错处理等工作,减少了人工操作和维护工作量,提高了服务的可靠性和稳定性,有效实现了降本增效。但kubernetes 的原生调度器只能通过资源请求来调度 pod,这很容易造成一系列负载不均的问题:

  1. 集群中的部分节点,资源的真实使用率远低于 resource request,却没有被调度更多的 Pod,这造成了比较大的资源浪费。
  2. 而集群中的另外一些节点,其资源的真实使用率事实上已经过载,却无法为调度器所感知到,这极大可能影响到业务的稳定性。

这些无疑都与企业上云的最初目的相悖,为业务投入了足够的资源,却没有达到理想的效果。crane-scheduler打破了资源 resource request 与真实使用率之间的鸿沟,着力于调度层面,让调度器直接基于真实使用率进行调度。

crane-scheduler基于集群的真实负载数据构造了一个简单却有效的模型,作用于调度过程中的 Filter 与 Score 阶段,并提供了一种灵活的调度策略配置方式,从而有效缓解集群中资源负载不均问题,真正实现将本增效。

调度框架

Kubernetes 调度框架

Kubernetes官方提供了可插拔架构的调度框架,能够进一步扩展Kubernetes调度器,下图展示了调度框架中的调度上下文及其中的扩展点,一个扩展可以注册多个扩展点,以便可以执行更复杂的有状态的任务。

如上图,Pod 调度流程:

1
2
3
4
5
6
7
8
9
10
11
12
Sort - 用于对 Pod 的待调度队列进行排序,以决定先调度哪个 Pod
Pre-filter - 用于对 Pod 的信息进行预处理
Filter - 用于排除那些不能运行该 Pod 的节点
Post-filter - 一个通知类型的扩展点,更新内部状态,或者产生日志
Scoring - 用于为所有可选节点进行打分
Normalize scoring - 在调度器对节点进行最终排序之前修改每个节点的评分结果
Reserve - 使用该扩展点获得节点上为 Pod 预留的资源,该事件发生在调度器将 Pod 绑定到节点前
Permit - 用于阻止或者延迟 Pod 与节点的绑定
Pre-bind - 用于在 Pod 绑定之前执行某些逻辑
Bind - 用于将 Pod 绑定到节点上
Post-bind - 是一个通知性质的扩展
Unreserve - 如果为 Pod 预留资源,又在被绑定过程中被拒绝绑定,则将被调用

crane-scheduler调度框架

Crane-scheduler 总体架构:

动态调度器总体架构如上图所示,主要有两个组件组成:

  1. Node-annotator定期从 Prometheus 拉取数据,并以注释的形式在节点上用时间戳标记它们。
  2. Dynamic plugin直接从节点的注释中读取负载数据,过滤并基于简单的算法对候选节点进行评分。

同时动态调度器提供了一个默认值调度策略并支持用户自定义策略。默认策略依赖于以下指标:

1
2
3
4
5
6
cpu_usage_avg_5m
cpu_usage_max_avg_1h
cpu_usage_max_avg_1d
mem_usage_avg_5m
mem_usage_max_avg_1h
mem_usage_max_avg_1d

在调度的Filter阶段,如果该节点的实际使用率大于上述任一指标的阈值,则该节点将被过滤。而在Score阶段,最终得分是这些指标值的加权和。

在生产集群中,可能会频繁出现调度热点,因为创建 Pod 后节点的负载不能立即增加。因此定义了一个额外的指标,名为Hot Value,表示节点最近几次的调度频率。并且节点的最终优先级是最终得分减去Hot Value

github项目地址:GitHub - gocrane/crane-scheduler: Crane scheduler is a Kubernetes scheduler which can schedule pod based on actual node load.

方案一:部署crane-scheduler

配置Prometheus Rules

生成新的查询表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: example-record
labels:
prometheus: k8s
role: alert-rules
spec:
groups:
- name: cpu_mem_usage_active
interval: 30s
rules:
- record: cpu_usage_active
expr: 100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[30s])) * 100)
- record: mem_usage_active
expr: 100*(1-node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes)
- name: cpu-usage-5m
interval: 5m
rules:
- record: cpu_usage_max_avg_1h
expr: max_over_time(cpu_usage_avg_5m[1h])
- record: cpu_usage_max_avg_1d
expr: max_over_time(cpu_usage_avg_5m[1d])
- name: cpu-usage-1m
interval: 1m
rules:
- record: cpu_usage_avg_5m
expr: avg_over_time(cpu_usage_active[5m])
- name: mem-usage-5m
interval: 5m
rules:
- record: mem_usage_max_avg_1h
expr: max_over_time(mem_usage_avg_5m[1h])
- record: mem_usage_max_avg_1d
expr: max_over_time(mem_usage_avg_5m[1d])
- name: mem-usage-1m
interval: 1m
rules:
- record: mem_usage_avg_5m
expr: avg_over_time(mem_usage_active[5m])

注:Prometheus 的采样间隔必须小于30秒,不然可能会导致规则无法正常生效。如:cpu_usage_active

或者把采集时间加大:

1
2
3
4
5
- name: cpu_mem_usage_active
interval: 1m
rules:
- record: cpu_usage_active
expr: 100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[2m])) * 100)

安装 Crane-scheduler

1
2
helm repo add crane https://gocrane.github.io/helm-charts
helm install scheduler -n crane-system --create-namespace --set global.prometheusAddr="http://prometheus-k8s.kubesphere-monitoring-system.svc:9090" crane/scheduler

安装完成后会创建两个deploy:

调度规则:

1
2
3
4
syncPolicy: 用户可以自定义负载数据的类型与拉取周期;
predicate: Filter 策略,若候选节点的当前负载数据超过了任一所配置的指标阈值,则这个节点将会被过滤;
priority:在 Score 策略中配置相关指标的权重,候选节点的最终得分为不同指标得分的加权和;
hotValue:定义调度热点规则,最终节点的 Priority 为上一小节中的 Score 减去 Hot Value

使用Crane-scheduler

这里有两种方式可供选择:

  • 作为k8s原生调度器之外的第二个调度器
  • 替代k8s原生调度器成为默认的调度器
作为k8s原生调度器之外的第二个调度器

在 pod spec.schedulerName 指定 crane-scheduler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
name: cpu-stress
spec:
selector:
matchLabels:
app: cpu-stress
replicas: 1
template:
metadata:
labels:
app: cpu-stress
spec:
schedulerName: crane-scheduler
hostNetwork: true
tolerations:
- key: node.kubernetes.io/network-unavailable
operator: Exists
effect: NoSchedule
containers:
- name: stress
image: docker.io/gocrane/stress:latest
command: ["stress", "-c", "1"]
resources:
requests:
memory: "1Gi"
cpu: "1"
limits:
memory: "1Gi"
cpu: "1"
替代k8s原生调度器成为默认的调度器

1.修改kube调度器的配置文件(scheduler config.yaml)以启用动态调度器插件并配置插件参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: kubescheduler.config.k8s.io/v1beta2
kind: KubeSchedulerConfiguration
...
profiles:
- schedulerName: default-scheduler
plugins:
filter:
enabled:
- name: Dynamic
score:
enabled:
- name: Dynamic
weight: 3
pluginConfig:
- name: Dynamic
args:
policyConfigPath: /etc/kubernetes/policy.yaml
...

2.修改kube-scheduler.yaml,并将kube调度器映像替换为Crane schedule:

1
2
3
...
image: docker.io/gocrane/crane-scheduler:0.0.23
...

3.安装crane-scheduler-controller:

1
kubectl apply ./deploy/controller/rbac.yaml && kubectl apply -f ./deploy/controller/deployment.yaml

这里使用k8s原生调度器之外的第二个调度器,在yaml文件指定schedulerName: crane-scheduler。

crane-sheduler 会将监控指标数据写在 node annotation 上,可以通过kubectl describe nodes 查看:

创建一个pod,看是否调度成功:

看实际效果:

之前没使用crane-sheduler之前,内存使用不均衡,有的node内存使用率五十,有的node已经到了快九十(忘记截图了),使用之后会趋向均衡:

问题

以上的部署是在内网环境测试的,k8s版本是1.22,在生产环境部署的时候就报错了,生产环境的k8s版本是1.27

crane-sheduler报错日志:

1
2
3
W1017 08:34:15.712878       1 reflector.go:324] pkg/mod/k8s.io/client-go@v0.23.3/tools/cache/reflector.go:167: failed to list *v1beta1.CSIStorageCapacity: the server could not find the requested resource

E1017 08:34:15.712901 1 reflector.go:138] pkg/mod/k8s.io/client-go@v0.23.3/tools/cache/reflector.go:167: Failed to watch *v1beta1.CSIStorageCapacity: failed to list *v1beta1.CSIStorageCapacity: the server could not find the requested resource

新创建的pod会一直卡在pending状态。

这里把crane的镜像地址改一下就行:

1
2
3
4
5
crane-scheduler:
ghcr.io/gocrane/crane/crane-scheduler:pr-43-b871212

crane-scheduler-controller:
ghcr.io/gocrane/crane/crane-scheduler-controller:pr-43-b871212

方案二:通过脚本定时监控node内存使用率

为了实现这个解决方案,可以使用 Kubernetes 中的污点(Taint)和容忍度(Toleration)机制。首先添加一个污点到这个节点,以标识它当前无法容纳高内存需求的 Pods。然后,在这些 Pods 的 YAML 文件中添加容忍度字段,以允许它们在具有更充足内存资源的其他节点上运行。最后,设置 SchedulingDisabled 标志,以确保后续的 Pods 不会被调度到这个节点上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash

# 功能:控制节点内存
# 如果节点内存占用率超过 90%,则禁止在该节点上调度 Pod
# 如果节点内存占用率低于平均值且平均值小于 90%,则允许在该节点上调度 Pod

# 使用 kubectl top 命令获取节点的内存占用率,并忽略掉一些特定的节点
data=$(kubectl top node | grep -v "MEMORY" | grep -v "cn-shenzhen" | sed "s/%//g")
# 计算所有节点的内存占用率的平均值
avg=$(echo "$data" | awk '{ sum += $NF } END { print sum / NR }' | awk -F. '{ print $1 }')
# 逐行读取每个节点的信息(节点名称和内存占用率)
echo "$data" | awk '{ print $1, $NF }' | while read line
do
# 获取节点名称和内存占用率
n=$(echo $line | awk '{ print $1 }')
m=$(echo $line | awk '{ print $2 }')
# 如果内存占用率超过 90%,并且该节点没有被设置为不可调度状态,则禁止在该节点上调度 Pod
if [ "$m" -ge "90" ];then
if kubectl get node | grep $n | grep -q "SchedulingDisabled";then
continue
else
kubectl cordon $n
fi
# 如果内存占用率低于平均值且平均值小于 90%,并且该节点已被设置为不可调度状态,则允许在该节点上调度 Pod
elif [ "$m" -lt "$avg" ] && [ "$avg" -lt "90" ];then
if kubectl get node | grep $n | grep -q "SchedulingDisabled";then
kubectl uncordon $n
fi
fi
done

优化:

实现了以下功能:

1
2
3
4
5
6
使用 kubectl top 命令获取节点的内存占用率,并忽略掉特定节点(例如 “MEMORY” 和 “cn-shenzhen”)的数据。
计算所有节点的内存占用率的平均值。
遍历每个节点的内存占用率,并根据阈值动态调整节点的调度状态。
如果某个节点的内存占用率超过 90%,并且该节点没有被设置为不可调度状态,则禁止在该节点上调度 Pod(使用 kubectl cordon 命令)。
如果某个节点的内存占用率低于下限阈值,并且该节点已被设置为不可调度状态,则允许在该节点上调度 Pod(使用 kubectl uncordon 命令)。
这样,脚本会根据节点的内存占用率动态调整节点的调度状态,以确保每台机器资源使用率基本一样,并在内存占用率达到 90% 时禁止调度 Pod。

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#/**********************************************************
# * Description : 根据节点的内存占用率动态调整节点的调度状态,以确保每台机器资源使用率基本一样,并在内存占用率达到 90% 时禁止调度 Pod。
# * *******************************************************/
#/bin/bash


# 日志函数,接收时间、日志级别和日志内容作为参数
log() {
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local level=$1
local message=$2
echo "[$timestamp] [$level] $message"
}

log "info" "开始执行脚本"

# 使用 kubectl top 命令获取节点的内存占用率,并忽略掉一些特定的节点
data=$(kubectl top node | grep -v "MEMORY" | grep -v "cn-shenzhen" | sed "s/%//g")

# 计算所有节点的内存占用率的平均值
avg=$(echo "$data" | awk '{ sum += $NF } END { print sum / NR }' | awk -F. '{ print $1 }')

# 逐行读取每个节点的信息(节点名称和内存占用率)
echo "$data" | awk '{ print $1, $NF }' | while read line
do
# 获取节点名称和内存占用率
n=$(echo $line | awk '{ print $1 }')
m=$(echo $line | awk '{ print $2 }')

# 输出节点名称和内存占用率日志
log "info" "节点 $n 的内存占用率为 $m%"

# 如果内存占用率超过90%,并且该节点没有被设置为不可调度状态,则禁止在该节点上调度 Pod
if [ "$m" -ge 90 ];then
if kubectl get node $n | grep -q "SchedulingDisabled";then
log "info" "节点 $n 内存占用率超过90%,已经禁止调度 Pod"
else
log "info" "节点 $n 内存占用率超过90%,禁止调度 Pod"
kubectl cordon $n
fi
else
# 根据平均阈值动态调整阈值范围(比如将平均阈值的上限和下限设置为平均值的上下10%)
threshold=$(echo "$avg * 0.1" | bc)
upper_threshold=$(printf "%.0f" $(echo "$avg + $threshold" | bc))
lower_threshold=$(printf "%.0f" $(echo "$avg - $threshold" | bc))

# 如果内存占用率超过上限阈值,并且该节点没有被设置为不可调度状态,则禁止在该节点上调度 Pod
if [ "$m" -ge "$upper_threshold" ];then
if kubectl get node $n | grep -q "SchedulingDisabled";then
log "info" "节点 $n 内存占用率过高,已经禁止调度 Pod"
else
log "info" "节点 $n 内存占用率过高,禁止调度 Pod"
kubectl cordon $n
fi
# 如果内存占用率低于下限阈值,并且该节点已被设置为不可调度状态,则允许在该节点上调度 Pod
elif [ "$m" -lt "$lower_threshold" ];then
if kubectl get node $n | grep -q "SchedulingDisabled";then
log "info" "节点 $n 内存占用率较低,允许调度 Pod"
kubectl uncordon $n
fi
fi
fi
done

log "info" "脚本执行完成"

原理

该脚本的原理是通过获取节点的内存占用率,并根据预设的阈值进行判断和操作。具体流程如下:

1
2
3
4
5
1. 使用 kubectl top 命令获取节点的内存占用率,并忽略掉特定的节点。
2.计算所有节点的内存占用率的平均值。
3.逐行读取每个节点的信息,包括节点名称和内存占用率。
4.如果节点内存占用率超过设定的阈值(例如 90%),并且该节点没有被设置为不可调度状态,则禁止在该节点上调度 Pod。
5.如果节点内存占用率低于平均值且平均值小于阈值,并且该节点已被设置为不可调度状态,则允许在该节点上调度 Pod。

通过这样的原理,我们可以在集群中实现对节点内存的动态管理,确保节点资源的合理利用和容器应用的稳定运行。

优势

1
2
3
自动化管理: 该脚本实现了自动化的节点内存管理,无需手动干预,减轻了运维人员的负担。
实时监测:通过定期执行脚本,可以实时监测节点的内存占用情况,及时做出调整,提高了容器应用的性能和可用性。
智能决策:根据设定的阈值和平均值,脚本能够智能地决策是否禁止或允许在节点上调度 Pod,确保资源的合理分配。

缺点

1
2
依赖性: 该脚本依赖于 Kubernetes 命令行工具 kubectl 和集群的配置,因此需要保证环境的正确配置和可用性。
单一维度: 该脚本仅基于节点的内存占用率进行管理,没有考虑其他资源(如 CPU、存储)的情况,因此在综合资源管理方面还有待完善。

由于配置了systemReserved,kubeReserved以及硬驱逐等,kubectl top nodes监控到的数据和node实际的使用率对不上,所以没使用这个方案。

Thank you for your accept. mua!
-------------本文结束感谢您的阅读-------------