第1章:Kubernetes调度器详解

调度器这东西,说白了就是Kubernetes的「大脑」。我刚开始接触K8s时,总觉得调度就是个简单的分配任务——哪个节点空闲就扔上去呗。后来线上出了几次事故,我才意识到,调度器的设计远比想象中复杂。

今天咱们就来聊聊调度器的核心机制。我会结合自己踩过的坑,把调度流程、算法、自定义开发、亲和性策略,还有Taints与Tolerations这些硬核内容,掰开揉碎了讲清楚。

1.1 调度器的工作流程

调度器到底在干什么?简单说,它负责把Pod分配到最合适的Node上。但这个过程不是一拍脑袋就决定的,它有一套严谨的流水线。

我习惯把调度流程分成三个阶段:

  • 预选阶段(Predicates):先过滤掉不满足条件的节点。比如节点资源不够、端口冲突、节点挂了——这些节点直接淘汰。
  • 优选阶段(Priorities):在剩下的节点里打分。分数高的节点优先被选中。
  • 绑定阶段(Bind):把Pod和选中的Node绑定,写入etcd。

你想想看,如果跳过预选直接打分,那得浪费多少计算资源?我见过有人把调度器调优搞反了,先打分再过滤,结果Pod老是调度到不健康的节点上。嗯,这里要注意,顺序不能乱。

核心要点:调度器默认是异步的,它会监听未调度的Pod,然后逐个执行上述流程。如果调度失败,Pod会重新进入队列等待下一次尝试。

1.2 Predicates:硬性过滤条件

Predicates就是「硬门槛」。不满足条件的节点,直接Pass。我整理了几种最常用的Predicates:

Predicate名称 作用 我踩过的坑
PodFitsResources 检查节点CPU、内存是否够用 曾经忽略了GPU资源检查,导致AI训练Pod被调度到无GPU节点
PodFitsHostPorts 检查端口是否冲突 两个Pod抢同一个宿主机端口,调度器直接报错
MatchNodeSelector 检查节点标签是否匹配Pod的nodeSelector 标签写错了,Pod一直Pending,排查了半天
NoDiskConflict 检查存储卷是否冲突 多个Pod挂载同一个读写卷,数据全乱了

我个人习惯在写Pod时,先把资源请求(requests)和限制(limits)写清楚。否则Predicates阶段根本没法准确判断节点是否够用。你想想看,如果requests没写,调度器默认认为Pod只需要0.1核CPU,结果跑起来直接占满节点——这不就出事了?

避坑指南:我曾经在生产环境遇到过一个问题——所有节点都通过了Predicates,但Pod就是调度不上去。后来发现是节点上的kubelet挂了,但调度器没有及时更新节点状态。所以,监控节点健康状态非常重要。

1.3 Priorities:打分机制

Predicates过滤完后,剩下的节点进入优选阶段。调度器会给每个节点打分,分数越高越优先。常用的打分策略有:

  • LeastRequestedPriority:优先选择资源使用率低的节点。说白了就是「负载均衡」。
  • BalancedResourceAllocation:优先选择CPU和内存使用率均衡的节点。避免一个节点CPU爆满但内存空闲。
  • NodeAffinityPriority:根据节点亲和性规则打分。这个后面会细讲。
  • TaintTolerationPriority:根据污点和容忍度打分。Pod能容忍的污点越多,节点得分越高。

我记得有一次,线上服务突然变慢,排查后发现调度器把所有Pod都扔到了同一个节点上。为什么?因为那个节点资源最空闲,LeastRequestedPriority给它打了最高分。但其他节点其实也够用,只是分数低了一点。后来我调整了权重,让BalancedResourceAllocation占更大比例,问题就解决了。

小技巧:你可以通过修改kube-scheduler的配置文件,自定义每个Priorities的权重。比如让NodeAffinityPriority的权重翻倍,这样亲和性规则的影响会更大。

1.4 自定义调度器开发

默认调度器虽然强大,但总有满足不了需求的时候。比如我遇到过这样一个场景:需要把Pod调度到离用户最近的节点上,但默认调度器没有「地理距离」这个概念。怎么办?自己写一个调度器。

自定义调度器有两种方式:

  • 扩展默认调度器:通过调度器扩展(Scheduler Extender)机制,在Predicates或Priorities阶段插入自定义逻辑。
  • 完全替换调度器:自己写一个调度器,监听Pod事件,执行自定义调度逻辑。

我比较推荐第一种方式,因为改动小、风险低。下面是一个简单的Scheduler Extender示例:

// 自定义调度器扩展
type CustomExtender struct {
    // 你的自定义逻辑
}

func (e *CustomExtender) Filter(args schedulerapi.ExtenderArgs) (*schedulerapi.ExtenderFilterResult, error) {
    // 自定义过滤逻辑
    nodes := args.Nodes.Items
    var filteredNodes []v1.Node
    for _, node := range nodes {
        // 比如:只选择有SSD磁盘的节点
        if node.Labels["disk-type"] == "ssd" {
            filteredNodes = append(filteredNodes, node)
        }
    }
    return &schedulerapi.ExtenderFilterResult{
        Nodes: &v1.NodeList{Items: filteredNodes},
    }, nil
}

嗯,这里要注意,自定义调度器需要注册到kube-scheduler的配置文件中。我曾经因为配置文件格式写错了,调度器启动失败,排查了半天才发现是YAML缩进问题。

1.5 节点亲和性与反亲和性

亲和性(Affinity)是调度器的高级玩法。它让你能精细控制Pod应该调度到哪些节点上。

节点亲和性有两种:

  • requiredDuringSchedulingIgnoredDuringExecution:硬性要求。Pod必须调度到满足条件的节点上,否则Pending。
  • preferredDuringSchedulingIgnoredDuringExecution:软性要求。调度器尽量满足,但不强制。

举个例子,假设你的AI训练任务需要GPU节点:

apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: gpu-type
            operator: In
            values:
            - "nvidia-tesla-v100"
  containers:
  - name: gpu-container
    image: nvidia/cuda:11.0

反亲和性则相反,它让Pod避开某些节点。比如,你可以让两个相同的服务不要调度到同一个节点上,避免单点故障。

实战经验:我曾在生产环境用反亲和性来分散关键服务。比如数据库主从节点,我要求它们不能调度到同一个物理机上。这样即使一台机器挂了,另一台还能继续服务。

1.6 Taints与Tolerations实战

Taints(污点)和Tolerations(容忍度)是调度器的「门禁系统」。节点可以打上污点,只有能容忍这些污点的Pod才能调度上去。

污点有三个效果:

  • NoSchedule:不调度。Pod不能调度到这个节点上。
  • PreferNoSchedule:尽量不调度。调度器会尽量避免,但不强制。
  • NoExecute:驱逐。已经在节点上运行的Pod,如果不容忍这个污点,会被驱逐。

我举个例子。假设你有一个节点专门跑监控服务,不想让其他Pod占用资源:

# 给节点打上污点
kubectl taint nodes node-monitor monitor=true:NoSchedule

# Pod声明容忍度
apiVersion: v1
kind: Pod
metadata:
  name: monitoring-pod
spec:
  tolerations:
  - key: "monitor"
    operator: "Equal"
    value: "true"
    effect: "NoSchedule"
  containers:
  - name: monitor
    image: prometheus:latest

这样,只有声明了容忍度的Pod才能调度到node-monitor上。其他Pod会被自动过滤掉。

避坑指南:我曾经在测试环境给所有节点都打上了污点,结果系统组件(如kube-proxy、coredns)调度不上去,整个集群瘫痪了。后来才知道,系统组件默认有容忍度,但如果你改了污点策略,一定要检查系统组件的容忍度配置。

1.7 调度器调优建议

最后,分享几个我常用的调度器调优技巧:

  • 调整调度队列的优先级:关键Pod可以设置更高的优先级,让调度器优先处理。
  • 开启抢占调度:高优先级Pod可以抢占低优先级Pod的资源。
  • 监控调度延迟:通过Prometheus监控调度器的调度延迟,如果超过100ms,说明需要优化。
  • 合理设置资源请求:requests和limits写清楚,调度器才能准确判断资源使用情况。

我个人习惯在集群上线前,先做一轮调度压力测试。模拟大量Pod同时调度,看看调度器能不能扛住。如果调度队列积压严重,可以考虑增加调度器的副本数。

好了,这一章的内容就到这里。调度器是Kubernetes的核心组件,理解它的工作原理,能帮你避免很多线上问题。下一章我们会聊聊Kubernetes的网络模型,包括CNI插件、Service和Ingress的实现原理。到时候见。