这次我们来看一个关于 Kubernetes 集群资源利用率优化的实战案例。核心议题是:仅仅通过调整 Pod 的分配顺序,就能将集群整体资源利用率提升 33 个百分点。这听起来像是一个简单的调度策略调整,但其背后涉及的是对 Kubernetes 调度器默认行为、资源碎片化问题以及实际工作负载模式的深刻理解。对于任何运行着大规模 K8s 集群、深受资源浪费和成本困扰的团队来说,这个思路都极具参考价值。
本文将深入拆解这一优化策略的原理、实现路径和具体效果。我们会先厘清 Kubernetes 默认调度器kube-scheduler在资源分配时的“最佳适应”策略及其可能带来的问题,然后引出“最差适应”策略如何成为破局关键。接着,我们将通过模拟实验和概念验证,展示如何通过自定义调度器插件或修改调度策略来实现这一改变,并分析其对 CPU、内存利用率提升的量化影响。最后,我们会探讨这种策略的适用边界、潜在风险以及在生产环境中落地的注意事项。
无论你是运维工程师、SRE 还是云原生开发者,这篇文章都将为你提供一个全新的视角来审视集群资源管理,并可能为你节省下可观的云资源成本。
1. 核心能力速览:优化策略一览
在深入技术细节前,我们先通过一个表格快速了解这次优化策略的核心要点、技术门槛和预期收益。
| 能力项 | 说明 |
|---|---|
| 优化目标 | 提升 Kubernetes 集群整体资源利用率,减少资源碎片。 |
| 核心原理 | 将默认调度器的“最佳适应”分配策略改为“最差适应”,优先填满已有负载的节点。 |
| 技术实现 | 通过实现自定义调度器插件(Scheduler Plugin)或配置调度策略(Scheduler Policy)来干预打分过程。 |
| 主要影响 | 提高节点资源装箱密度,降低空闲节点数量,从而提升集群整体利用率。 |
| 硬件/环境门槛 | 需要一个正在运行的 Kubernetes 集群(v1.19+ 支持框架更完善),具备开发或配置调度器的权限。无需额外硬件。 |
| 启动/部署方式 | 1. 编译并部署自定义调度器。 2. 或,配置并启用kube-scheduler的扩展点。 |
| 是否支持 API | 优化本身不提供额外 API,但可通过 Kubernetes 标准 API 或监控系统(如 Prometheus)观察效果。 |
| 是否支持“批量” | 策略一旦生效,将影响集群内所有新 Pod 的调度决策,属于全局性批量优化。 |
| 适合场景 | 拥有大量节点、工作负载波动大、资源碎片化严重、成本敏感的生产集群。 |
| 不适合场景 | 小规模集群、节点异构性极强、对 Pod 启动延迟有极端要求、或已深度使用基于 bin packing 的其他调度策略。 |
2. 适用场景与使用边界
2.1 谁需要这个优化?
- 中大型 Kubernetes 集群运维团队:节点数量多(例如数十到上百台),经常发现集群总体资源请求量不高,但却需要频繁扩容节点,资源利用率报表“不好看”。
- 成本控制团队:在公有云或私有云上,资源利用率直接关联成本。提升利用率意味着用更少的节点承载相同的工作负载,直接降低基础设施支出。
- 追求极致效率的技术团队:不满足于默认配置,希望通过深入理解并调整系统底层行为来获得性能与效率的提升。
2.2 能解决什么问题?
- 资源碎片化:默认调度策略可能导致每个节点都剩余少量无法被新 Pod 利用的资源(如每个节点剩 0.5 核 CPU,100MiB 内存),这些碎片累积起来就是巨大的浪费。
- 节点利用率不均衡:部分节点被塞满,部分节点却非常空闲,整体利用率被拉低。
- 不必要的节点扩容:由于碎片存在,当需要部署一个资源需求中等的 Pod 时,调度器可能因为所有现存节点都无法满足其“最小空闲资源”要求,而触发集群自动扩容,增加新节点,但实际上集群总空闲资源是足够的。
2.3 不适合什么场景?
- 高可用性优先的场景:将 Pod 密集地调度到少数节点,会提高单节点故障的影响范围。如果业务对可用性要求极高,需要更均匀的分布,则此策略需谨慎评估。
- 节点异构集群:如果集群节点规格差异巨大(如混布了 CPU 密集型、内存密集型、GPU 节点),简单的“最差适应”可能不是最优解,需要更复杂的调度策略。
- 对延迟极度敏感的业务:更密集的装箱可能加剧节点资源竞争(特别是 CPU 和网络),可能对某些延迟敏感型 Pod 的性能产生负面影响。
2.4 合规与风险边界
- 稳定性风险:修改核心调度策略是高风险操作。必须在测试环境中充分验证,并制定详尽的回滚方案。
- 公平性考量:此策略可能不利于需要快速扩容或对资源有突发需求的命名空间/团队,因为它倾向于“填坑”而非“铺新摊”。可能需要结合配额(ResourceQuota)和优先级(PriorityClass)进行综合管理。
- 监控与告警:实施后,必须加强对目标节点(那些被填满的节点)的监控,包括 CPU/内存压力、磁盘 IO、网络带宽等,防止因过度拥挤导致节点不稳定。
3. 环境准备与前置条件
在尝试实现这一优化策略前,请确保你的环境满足以下条件。
3.1 基础环境要求
- Kubernetes 集群:版本建议在 1.19 及以上。低版本可能对调度器扩展框架的支持不完善。你可以通过
kubectl version命令确认。 - kubectl 配置:已正确配置
kubeconfig文件,能够管理目标集群。 - 集群权限:需要在目标集群中拥有部署 Pod、创建 ConfigMap、修改调度器配置等权限,通常是
cluster-admin或等效角色。
3.2 开发与构建环境(如果选择自定义插件)
如果计划通过编写 Go 插件来实现,则需要:
- Go 语言环境:版本需要与你的 Kubernetes 版本相匹配(可参考 Kubernetes 官方发布说明)。
- 必要的依赖包:如
k8s.io/client-go,k8s.io/kubernetes等,版本需严格对齐。 - 代码仓库:从 Kubernetes 源码分叉或建立独立的插件项目。
3.3 监控与评估工具
为了量化优化效果,你必须有能力测量集群利用率。建议提前部署:
- Prometheus + Grafana:用于收集和可视化节点资源使用率、分配率、Pod 数量等指标。
- kube-state-metrics:提供关于 Pod、节点、资源请求和限制的丰富指标。
- 或使用云服务商提供的原生监控仪表盘。
4. 原理剖析:从“最佳适应”到“最差适应”
要理解如何改变分配顺序,首先要理解默认调度器kube-scheduler是如何工作的。
4.1 默认调度流程简述
对于一个待调度的 Pod,kube-scheduler的工作流程主要分为两步:
- 过滤(Filtering):根据 Pod 的资源请求、节点选择器、亲和性、污点容忍等条件,过滤掉所有不满足条件的节点。剩下的节点称为“可调度节点”。
- 打分(Scoring):对每一个“可调度节点”进行评分。评分基于一系列打分插件,如
NodeResourcesFit(检查资源适配度)、NodeAffinity(节点亲和性)、InterPodAffinity(Pod间亲和性)等。最后,调度器选择总分最高的节点。
4.2 问题的根源:NodeResourcesFit的 “LeastAllocated” 策略
在打分阶段,NodeResourcesFit插件默认使用LeastAllocated策略。这个策略的名字容易误解,它的目标是将 Pod 调度到资源分配最少的节点上,即“最佳适应”(Best Fit)。其评分函数大致是:得分 = (节点总容量 - Pod请求资源后节点已分配量) / 节点总容量这个值越高,说明节点分配后剩余资源越多,节点“越空闲”,得分也就越高。调度器会选择得分最高的节点。
这导致了什么?假设集群有 3 个节点:
- Node1: 已分配 16核/32核, 剩余 16核。
- Node2: 已分配 8核/32核, 剩余 24核。
- Node3: 已分配 24核/32核, 剩余 8核。
现在有一个请求 4 核 CPU 的 Pod。按照LeastAllocated策略计算分配后的空闲率:
- Node1: (32 - (16+4)) / 32 = 12/32 = 0.375
- Node2: (32 - (8+4)) / 32 = 20/32 = 0.625
- Node3: (32 - (24+4)) / 32 = 4/32 = 0.125
Node2 得分最高(0.625),因此 Pod 会被调度到Node2。这看起来合理,但它倾向于把新的、小的 Pod 放到还比较空的节点上,而不是去填满那些已经比较满的节点(如 Node3)。长期运行后,容易导致每个节点都有剩余,但每个节点的剩余都不够运行一个稍大的 Pod,从而产生碎片。
4.3 解决方案:切换到 “MostAllocated” 策略
“最差适应”(Worst Fit)策略,在NodeResourcesFit插件中对应MostAllocated策略。它的目标是将 Pod 调度到资源分配最多的节点上(前提是该节点仍能满足 Pod 请求)。其评分函数与LeastAllocated相反,追求分配后剩余资源最少。
继续上面的例子,使用MostAllocated策略(得分计算方式可能不同,但理念是选择最满的可行节点): 调度器会更倾向于选择 Node3,因为把它填满(从24核到28核)能最大化该节点的利用率,让 Node2 保持相对空闲以容纳未来可能的需求更大的 Pod。这样,集群的节点负载会呈现“部分节点高负载,部分节点低负载”的两极分化,但整体资源碎片更少,平均利用率更高。
5. 实现方式一:配置 kube-scheduler 使用新策略
对于较新版本的 Kubernetes(如 1.23+),你可以通过修改调度器配置文件来启用MostAllocated策略,而无需编写代码。
5.1 创建调度器配置文件
首先,创建一个名为scheduler-config.yaml的配置文件:
apiVersion: kubescheduler.config.k8s.io/v1beta3 kind: KubeSchedulerConfiguration leaderElection: leaderElect: false profiles: - schedulerName: default-scheduler plugins: score: disabled: - name: NodeResourcesFit # 先禁用默认的 enabled: - name: NodeResourcesFit weight: 1 # 权重保持为1 pluginConfig: - name: NodeResourcesFit args: scoringStrategy: type: MostAllocated # 关键修改:将策略类型改为 MostAllocated resources: - name: cpu weight: 1 - name: memory weight: 15.2 以配置文件启动自定义调度器
你可以运行一个独立的调度器 Pod,使用这个配置。首先将上述配置保存为 ConfigMap:
kubectl create configmap scheduler-config --from-file=scheduler-config.yaml --namespace=kube-system然后,部署一个自定义调度器的 Deployment。以下是一个简化的示例:
# custom-scheduler-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: custom-kube-scheduler namespace: kube-system labels: component: custom-scheduler spec: replicas: 1 # 生产环境应考虑高可用,部署多个副本 selector: matchLabels: component: custom-scheduler template: metadata: labels: component: custom-scheduler spec: serviceAccountName: custom-kube-scheduler-sa containers: - name: custom-kube-scheduler image: registry.k8s.io/kube-scheduler:v1.27.0 # 使用与集群兼容的版本 command: - /usr/local/bin/kube-scheduler - --config=/etc/kubernetes/scheduler-config.yaml - --v=2 volumeMounts: - name: scheduler-config mountPath: /etc/kubernetes volumes: - name: scheduler-config configMap: name: scheduler-config你需要创建一个拥有相应 RBAC 权限的 ServiceAccount (custom-kube-scheduler-sa)。部署完成后,新的 Pod 如果要使用这个调度器,需要在 Pod Spec 中指定schedulerName: default-scheduler(与配置文件中profiles[0].schedulerName一致)。
注意:此方法部署的是另一个调度器,与系统原有的kube-scheduler并存。你可以通过指定schedulerName来让部分 Pod 接受新策略调度,进行灰度测试。
6. 实现方式二:开发自定义调度器插件(进阶)
如果内置策略不满足需求,或者你想实现更复杂的打分逻辑(如结合实际负载而非请求量),可以开发自定义插件。
6.1 插件框架简介
Kubernetes 调度框架(Scheduling Framework)提供了一组扩展点,允许开发者以插件形式注入自定义逻辑。我们关注的是Score扩展点。
6.2 一个简单的“最差适应”打分插件示例
以下是一个高度简化的 Go 代码示例,展示插件的基本结构:
package main import ( "context" "fmt" "k8s.io/kubernetes/pkg/scheduler/framework" ) // 插件名称 const MyWorstFitPluginName = "MyWorstFitPlugin" type MyWorstFitPlugin struct { handle framework.Handle } // 实现 Score 扩展点接口 func (pl *MyWorstFitPlugin) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) { // 1. 获取节点信息 nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName) if err != nil { return 0, framework.AsStatus(err) } // 2. 获取节点可分配资源总量 allocatable := nodeInfo.Allocatable // 3. 获取节点已分配资源总量(包括待调度的Pod) requested := nodeInfo.Requested // 这里需要加上当前待调度Pod的资源请求,但框架可能已在state中处理。 // 简化处理:我们仅基于当前已分配量来打分。 // 4. 计算“已分配比例”作为得分依据。比例越高,得分越高。 // 注意:这里只考虑CPU和内存,且做了简化加权平均。 cpuRatio := float64(requested.MilliCPU) / float64(allocatable.MilliCPU) memRatio := float64(requested.Memory) / float64(allocatable.Memory) avgRatio := (cpuRatio + memRatio) / 2.0 // 5. 将比例映射到框架的得分范围 (0, framework.MaxNodeScore) score := int64(avgRatio * float64(framework.MaxNodeScore)) // 打印日志便于调试 klog.V(3).Infof("Node %s: allocatable CPU=%dm, Memory=%dMi, requested CPU=%dm, Memory=%dMi, ratio=%.2f, score=%d", nodeName, allocatable.MilliCPU, allocatable.Memory>>20, requested.MilliCPU, requested.Memory>>20, avgRatio, score) return score, nil } // 必须实现 ScoreExtensions 接口,用于标准化分数 func (pl *MyWorstFitPlugin) ScoreExtensions() framework.ScoreExtensions { return pl } func (pl *MyWorstFitPlugin) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList) *framework.Status { // 这里可以实现分数的标准化,例如让最高分是100,最低分是0。 // 简化处理,直接返回。 return nil } // 插件工厂函数,用于初始化插件 func NewMyWorstFitPlugin(_ runtime.Object, h framework.Handle) (framework.Plugin, error) { return &MyWorstFitPlugin{handle: h}, nil }6.3 编译与部署插件
- 编译:将你的插件代码编译成
.so共享库文件。go build -buildmode=plugin -o myworstfit.so myworstfit.go - 配置调度器:修改 kube-scheduler 的配置文件,指定插件目录并启用你的插件。
apiVersion: kubescheduler.config.k8s.io/v1beta3 kind: KubeSchedulerConfiguration leaderElection: leaderElect: false profiles: - schedulerName: default-scheduler pluginConfig: - name: MyWorstFitPlugin args: # 可以传递自定义参数 plugins: score: enabled: - name: MyWorstFitPlugin weight: 1 # 设置插件权重 disabled: - name: NodeResourcesFit # 可以选择禁用默认插件 - 启动调度器:将编译好的
.so文件放到指定目录,并在启动命令中通过--config指定上述配置文件。
注意:自定义插件开发、编译和部署流程复杂,且严重依赖于 Kubernetes 版本,需严格参考对应版本的官方文档和示例。
7. 功能测试与效果验证
在将新策略应用于生产环境前,必须在测试集群进行充分的验证。
7.1 测试环境搭建
- 准备测试集群:可以使用 Kind、Minikube 或一个小型生产镜像集群。
- 部署监控:确保 Prometheus 和 Grafana 已就位,并配置好关于节点资源请求/使用率的仪表盘。
- 部署工作负载模拟工具:例如,使用
kubectl批量创建一系列具有不同资源请求的 Deployment 或 Job。
7.2 测试步骤与对比
我们将进行 A/B 测试,对比默认策略和新策略下的集群状态。
步骤 1:基准测试(默认策略)
- 确保调度器使用默认配置。
- 使用脚本批量创建一批 Pod。例如,创建 100 个 Pod,CPU 请求在 100m 到 1000m 之间随机,内存请求在 100Mi 到 512Mi 之间随机。
# 示例脚本片段 for i in {1..100}; do cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Pod metadata: name: test-pod-default-$i spec: containers: - name: busybox image: busybox command: ["sleep", "3600"] resources: requests: cpu: $(shuf -i 100-1000 -n 1)m memory: $(shuf -i 100-512 -n 1)Mi restartPolicy: Never EOF done - 等待所有 Pod 调度完成(
Running状态)。 - 在 Grafana 中记录:集群总 CPU/内存请求量、各节点资源分配率、节点间分配率的方差(衡量均衡度)、无法调度的 Pod 数量(如有)。
步骤 2:实验测试(最差适应策略)
- 将调度器切换到配置了
MostAllocated策略的自定义调度器,或启用你的自定义插件。 - 删除步骤 1 中创建的所有 Pod。
- 使用相同的脚本,创建另一批名称不同的 Pod(如
test-pod-worstfit-$i)。 - 等待调度完成。
- 再次在 Grafana 中记录相同指标。
7.3 预期结果与成功标准
- 成功标准 1:整体利用率提升。在总请求资源量相同的情况下,使用“最差适应”策略后,集群的平均节点资源分配率应该显著高于默认策略。这正是“提升 33 个百分点”可能出现的场景——例如,从平均 40% 的分配率提升到 73%。
- 成功标准 2:资源碎片减少。观察“拥有充足空闲资源(例如 >2 核 CPU)的节点数量”。最差适应策略下,这类节点应该更少,因为资源被集中到了部分节点上。
- 成功标准 3:调度结果符合预期。通过
kubectl describe node命令查看节点详情,确认新的 Pod 确实被更多地调度到了那些已分配率较高的节点上。 - 需要关注的负面现象:
- 节点压力:高分配率的节点,其实际资源使用率(而不仅仅是请求量)是否过高?需监控节点负载。
- Pending Pod:是否出现了因节点资源碎片化减少但节点数量不足导致的 Pod 无法调度?这可能需要结合集群自动伸缩器(CA)来观察。
8. 资源占用与性能观察
改变调度策略本身几乎不消耗额外的系统资源。核心的观察点在于策略改变后,集群整体的资源分布状态。
8.1 关键监控指标
在 Grafana 中,你应该关注以下面板:
- 节点资源分配率(Request):
sum(kube_pod_container_resource_requests{node="<node_name>", resource="cpu"}) by (node) / sum(kube_node_status_capacity_cpu_cores) by (node) * 100 - 节点资源使用率(Usage):
(1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (node)) * 100(CPU),(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100(内存) - 集群平均分配率:所有节点分配率的平均值。
- 分配率分布直方图:查看节点分配率的分布情况,是均匀分布还是两极分化。
- Pending Pod 数量:
count(kube_pod_status_phase{phase="Pending"}) - 节点系统负载:
node_load1,node_load5,node_load15
8.2 性能影响分析
- 调度器性能:打分逻辑的改变对调度器性能影响微乎其微。最差适应策略的计算复杂度与默认策略相同。
- 节点性能:这是主要关注点。节点负载过高可能导致:
- CPU 竞争:Pod 的 CPU 使用受到限制,进程调度延迟增加。
- 内存压力:可能触发内核 OOM Killer,杀死进程。
- 网络与存储 IO 竞争:物理带宽成为瓶颈。
- 应对措施:
- 为节点设置合理的资源预留(System Reserved, Kube Reserved),确保系统进程和 K8s 组件有足够资源。
- 使用LimitRange和ResourceQuota约束 Pod 的资源请求与限制,避免单个 Pod 过度占用。
- 结合Vertical Pod Autoscaler (VPA)动态调整 Pod 的资源请求,但 VPA 与调度器联动复杂,需谨慎。
- 实施Pod 中断预算(PDB)和优雅的节点排水(Drain)策略,以便安全地维护高负载节点。
9. 常见问题与排查方法
在实施过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 自定义调度器 Pod 无法启动 | RBAC 权限不足、镜像拉取失败、配置错误 | kubectl logs -f <scheduler-pod> -n kube-system查看日志;kubectl describe pod查看事件。 | 检查 ServiceAccount 的 ClusterRoleBinding;确保镜像版本与集群兼容;检查 ConfigMap 挂载和配置文件语法。 |
| Pod 一直处于 Pending 状态,事件显示调度失败 | 自定义调度器名称不匹配、调度器插件冲突、节点过滤条件不满足 | kubectl describe pod <pending-pod>查看事件详情;检查 Pod 的schedulerName。 | 确保 Pod 的spec.schedulerName与自定义调度器配置的profiles[*].schedulerName一致;检查插件打分是否导致所有节点得分为0。 |
| 调度策略未生效,Pod 分布无变化 | 配置文件未正确加载、插件未启用、权重设置过低 | 检查调度器日志,确认配置已加载且插件被调用;检查插件打分日志。 | 确认调度器启动命令包含--config参数;在配置文件中显式禁用默认的NodeResourcesFit插件(如果冲突);提高自定义插件的权重。 |
| 节点负载过高,系统不稳定 | 最差适应策略导致节点过于拥挤,实际使用量超过承受能力。 | 监控节点系统负载、内存使用率、网络丢包率。检查是否有 Pod 被 OOMKilled。 | 引入基于实际使用率的调度策略(需更复杂插件);设置更保守的资源预留;对节点设置最大可分配比例阈值(通过 Taint/Toleration 或自定义插件实现)。 |
| 集群自动伸缩器(CA)频繁伸缩 | 调度策略改变后,资源碎片减少,但可能使 CA 基于剩余可调度资源计算的逻辑发生变化。 | 观察 CA 日志,看其触发缩容/扩容的条件是否被意外满足。 | 可能需要调整 CA 的扩展阈值或冷却时间,以适应新的调度密度。理解 CA 与调度器的协作逻辑。 |
10. 最佳实践与使用建议
- 灰度发布:绝对不要一次性将所有流量切换到新调度策略。可以先创建一个新的调度器,并让部分非关键业务或测试命名空间的 Pod 使用它(通过
schedulerName指定),观察一段时间。 - 结合多种策略:“最差适应”并非银弹。可以考虑混合策略,例如:
- 对 CPU 密集型应用使用
MostAllocated。 - 对内存密集型应用使用
LeastAllocated。 - 或开发一个综合考量 CPU、内存、GPU 甚至节点实际负载(使用 metrics-server 数据)的复合打分插件。
- 对 CPU 密集型应用使用
- 设置安全边界:在节点层面,可以通过设置 Taint 来防止节点被过度调度。例如,给节点打上
node.kubernetes.io/memory-pressure类似的污点(需配合监控系统自动打标),并让 Pod 谨慎容忍这些污点。 - 强化监控与告警:实施后,必须加强对节点压力指标的监控,并设置告警。例如,当节点内存使用率超过 85% 或 CPU 负载持续高于某个阈值时触发告警。
- 定期评估与回滚预案:定期(如每周)分析调度策略的效果,对比成本节省与可能带来的稳定性风险。始终准备好一键回滚到默认调度器的方案。
改变 Pod 的分配顺序,从“最佳适应”转向“最差适应”,是一个从“追求均衡”到“追求利用率”的思维转变。它通过牺牲一定程度的负载均衡性,来换取集群整体资源利用率的显著提升,对于成本敏感且业务弹性较强的场景价值巨大。
这项优化最直接的价值在于,它可能让你在不增加任何硬件投入的情况下,凭空多出三分之一的可用计算资源。实现路径是清晰的:无论是通过配置内置策略,还是开发自定义插件,核心都在于干预kube-scheduler的打分过程。
在动手之前,请务必在测试环境中完成全链路的验证,从策略变更、工作负载模拟到监控告警。重点关注节点在高负载下的真实表现,而不仅仅是分配率数字。同时,牢记这一策略的边界,它不适合所有场景,尤其是那些对单点故障零容忍或节点异构性复杂的集群。
对于已经深陷资源碎片化困扰的团队,不妨从一个小型测试集群开始,迈出这改变分配顺序的第一步。它或许就是你提升集群效率、降低云成本的关键一着。