Kubernetes调度优化:从最佳适应到最差适应,提升集群资源利用率33%
这次我们来看一个关于 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 核 CPU100MiB 内存这些碎片累积起来就是巨大的浪费。节点利用率不均衡部分节点被塞满部分节点却非常空闲整体利用率被拉低。不必要的节点扩容由于碎片存在当需要部署一个资源需求中等的 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 默认调度流程简述对于一个待调度的 Podkube-scheduler的工作流程主要分为两步过滤Filtering根据 Pod 的资源请求、节点选择器、亲和性、污点容忍等条件过滤掉所有不满足条件的节点。剩下的节点称为“可调度节点”。打分Scoring对每一个“可调度节点”进行评分。评分基于一系列打分插件如NodeResourcesFit检查资源适配度、NodeAffinity节点亲和性、InterPodAffinityPod间亲和性等。最后调度器选择总分最高的节点。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 - (164)) / 32 12/32 0.375Node2: (32 - (84)) / 32 20/32 0.625Node3: (32 - (244)) / 32 4/32 0.125Node2 得分最高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使用这个配置。首先将上述配置保存为 ConfigMapkubectl create configmap scheduler-config --from-filescheduler-config.yaml --namespacekube-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 - --v2 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.Memory20, requested.MilliCPU, requested.Memory20, 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 -buildmodeplugin -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 个 PodCPU 请求在 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 中你应该关注以下面板节点资源分配率Requestsum(kube_pod_container_resource_requests{nodenode_name, resourcecpu}) by (node) / sum(kube_node_status_capacity_cpu_cores) by (node) * 100节点资源使用率Usage(1 - avg(rate(node_cpu_seconds_total{modeidle}[5m])) by (node)) * 100(CPU),(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100(内存)集群平均分配率所有节点分配率的平均值。分配率分布直方图查看节点分配率的分布情况是均匀分布还是两极分化。Pending Pod 数量count(kube_pod_status_phase{phasePending})节点系统负载node_load1,node_load5,node_load158.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 数据的复合打分插件。设置安全边界在节点层面可以通过设置 Taint 来防止节点被过度调度。例如给节点打上node.kubernetes.io/memory-pressure类似的污点需配合监控系统自动打标并让 Pod 谨慎容忍这些污点。强化监控与告警实施后必须加强对节点压力指标的监控并设置告警。例如当节点内存使用率超过 85% 或 CPU 负载持续高于某个阈值时触发告警。定期评估与回滚预案定期如每周分析调度策略的效果对比成本节省与可能带来的稳定性风险。始终准备好一键回滚到默认调度器的方案。改变 Pod 的分配顺序从“最佳适应”转向“最差适应”是一个从“追求均衡”到“追求利用率”的思维转变。它通过牺牲一定程度的负载均衡性来换取集群整体资源利用率的显著提升对于成本敏感且业务弹性较强的场景价值巨大。这项优化最直接的价值在于它可能让你在不增加任何硬件投入的情况下凭空多出三分之一的可用计算资源。实现路径是清晰的无论是通过配置内置策略还是开发自定义插件核心都在于干预kube-scheduler的打分过程。在动手之前请务必在测试环境中完成全链路的验证从策略变更、工作负载模拟到监控告警。重点关注节点在高负载下的真实表现而不仅仅是分配率数字。同时牢记这一策略的边界它不适合所有场景尤其是那些对单点故障零容忍或节点异构性复杂的集群。对于已经深陷资源碎片化困扰的团队不妨从一个小型测试集群开始迈出这改变分配顺序的第一步。它或许就是你提升集群效率、降低云成本的关键一着。
