第七章:资源的限制与调度

Kubernetes不仅仅是一个应用部署平台,更是一个强大的资源管理器。在一个共享的集群环境中,如果不对资源进行管理,一个行为不端的“坏邻居”应用(例如,因内存泄漏而耗尽节点内存)就可能影响到同一节点上所有其他应用的正常运行。同样,如果不能将需要特定硬件(如GPU)的应用调度到正确的节点上,那么应用也无法正常工作。

本章将聚焦于两个核心问题:

  1. 资源量化与约束:如何为应用定义其所需的CPU和内存,并设置使用上限?
  2. 智能调度与放置:如何控制Pod应该被调度到哪些(或不应该被调度到哪些)Node节点上?

掌握这些内容,是实现资源隔离、成本控制、高可用部署以及优化集群资源利用率的关键。

7.1 Requests & Limits:为Pod中的容器设置CPU和内存

Kubernetes允许你为Pod中的每一个容器指定两种类型的资源约束:requests(请求值)limits(限制值)

  • requests:

    • 含义: 容器保证可以获得的最小资源量。
    • 作用: 主要由**调度器(Scheduler)**在调度时使用。调度器在为Pod选择节点时,会检查节点的可用资源是否能满足Pod中所有容器的requests总和。如果找不到任何一个节点能满足,Pod将一直处于Pending状态。
    • 可以看作是“预订”。你为你的应用预订了这部分资源,集群承诺会为你保留。
  • limits:

    • 含义: 容器在运行时绝对不能超过的资源使用上限。
    • 作用: 主要由Kubelet在容器运行时强制执行。
    • 可以看作是“天花板”。无论节点上有多么空闲的资源,你的容器使用量都不能突破这个硬性上限。
CPU和内存的单位与特性
  • CPU:

    • 单位: CPU在Kubernetes中是一个绝对量,而不是相对比例。1 CPU单位等同于云提供商的一个vCPU或物理机上的一个超线程核心。你可以使用毫核(millicores)来表示更小的值,例如500m代表0.5个CPU。
    • 特性: CPU是一种可压缩(Compressible)资源。如果一个容器尝试使用的CPU超过了它的limit,它不会被杀死,而是会被节流(Throttled),即它的性能会被强制降低,运行速度变慢。
  • 内存 (Memory):

    • 单位: 内存的单位是字节,你可以使用E, P, T, G, M, KEi, Pi, Ti, Gi, Mi, Ki等后缀。例如128Mi(128 Mebibytes)、1Gi(1 Gibibyte)。
    • 特性: 内存是一种不可压缩(Incompressible)资源。如果一个容器尝试分配的内存超过了它的limit,它会立即被终止,并被操作系统以**OOMKilled(Out of Memory Killed)**的错误杀死。如果这个Pod由Deployment等控制器管理,它会被重新启动。
示例:在Pod中定义资源
# resources-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: resource-demo-pod
spec:
  containers:
  - name: my-app-container
    image: nginx
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"  # 请求 0.25 个CPU核心
      limits:
        memory: "128Mi"
        cpu: "500m"  # 最多使用 0.5 个CPU核心
服务质量(QoS)等级:Pod的“优先级”

Kubernetes根据你为Pod设置的requestslimits,自动将其划分为三种服务质量(QoS)等级。这个QoS等级非常重要,它决定了Pod的调度优先级和在节点资源紧张时被驱逐(Evicted)的优先级

QoS等级 设置条件 行为和优先级
Guaranteed (有保证的)
  • Pod中的每一个容器都必须同时设置了CPU和内存的requestslimits
  • 并且,对于每一个容器,其CPU的requests必须严格等于limits,内存的requests也必须严格等于limits
  • 最高优先级
  • 这些Pod的资源需求是完全确定的,性能最可预测。
  • 当节点资源耗尽时,它们是最后被驱逐的。
  • 非常适合数据库、消息队列等关键的有状态应用。
Burstable (可突发的)
  • Pod不满足Guaranteed的条件。
  • 但Pod中至少有一个容器设置了CPU或内存的requests
(例如 requests < limits,或只设置了requests)
  • 中等优先级
  • 这些Pod被保证了requests的资源量,但如果节点上有空闲资源,它们可以“突发”地使用更多资源,直到达到limits
  • 当节点资源耗尽时,它们会在BestEffort类型的Pod被驱逐之后才被驱逐。
  • 这是最常见的QoS等级,适用于绝大多数Web应用和微服务。
BestEffort (尽力而为的) Pod中的所有容器都没有设置任何requestslimits
  • 最低优先级
  • 这些Pod没有任何资源保证,它们只能使用节点上剩余的“残羹冷饭”。
  • 当节点资源紧张时,它们是最先被驱逐的。
  • 适合开发测试、不重要的批处理任务等。

核心思想:通过为关键应用设置GuaranteedBurstable的QoS,你可以确保在集群负载高时,这些关键应用不会被那些不重要的BestEffort应用抢占资源,从而保障了整个系统的稳定性。

7.2 ResourceQuota: 在命名空间级别限制资源使用

requestslimits是在Pod级别对资源进行约束。而ResourceQuota则是集群管理员的工具,用于在**命名空间(Namespace)**的宏观层面上对资源进行限制。这对于多租户环境至关重要,可以防止某个团队或项目过度消耗集群资源。

ResourceQuota可以限制两大类资源:

  1. 计算资源: 如requests.cpu, limits.memory, requests.storage(对PVC的总容量请求)等。
  2. 对象数量: 如pods, services, configmaps, secrets, persistentvolumeclaims等的最大数量。
示例:为一个命名空间设置配额
# my-namespace-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: default-quota
  namespace: team-a # 这个配额将作用于 team-a 命名空间
spec:
  hard:
    # 计算资源配额
    requests.cpu: "10"      # 该命名空间下所有Pod的CPU requests总和不能超过10核
    requests.memory: 20Gi   # ...内存requests总和不能超过20GiB
    limits.cpu: "20"
    limits.memory: 40Gi
    # 对象数量配额
    pods: "50"
    services: "20"

kubectl apply -f my-namespace-quota.yaml

重要行为:如果在某个命名空间中为一个计算资源(如requests.cpu)设置了配额,那么在该命名空间中创建的每一个容器都必须明确指定该资源的requestslimits。否则,Pod的创建请求将被API Server拒绝。这是为了让ResourceQuota能够准确地追踪资源使用情况。

7.3 Pod调度策略

Kubernetes的默认调度器会综合考虑节点资源、Pod的QoS等级等因素,努力将Pod均匀地分布在集群中。但在很多场景下,我们需要更精细的控制,例如:

  • 硬件依赖: 将需要GPU的Pod调度到装有GPU卡的节点上。
  • 高可用: 将一个服务的多个副本分散到不同的可用区或物理机架上,避免单点故障。
  • 数据局部性: 将计算Pod调度到离其所需数据(如存储或缓存)最近的节点上,以降低网络延迟。
  • 隔离: 将某个特定租户或应用的所有Pod都限制在一组专用的节点上。

Kubernetes提供了多种强大的机制来实现这些高级调度策略,核心是亲和性(Affinity)污点/容忍(Taints/Tolerations)

节点亲和性(Node Affinity)

节点亲和性是Pod的一个属性,它使Pod被吸引到一类特定的Node节点上。这是通过匹配Node上的**标签(Labels)**来实现的。

它有两种类型:

  1. requiredDuringSchedulingIgnoredDuringExecution (硬亲和性):

    • 含义: 必须满足。调度器只会将Pod调度到拥有匹配标签的节点上。如果集群中不存在这样的节点,Pod将永远处于Pending状态。
    • IgnoredDuringExecution意味着:一旦Pod被成功调度,即使后来该节点的标签发生了变化(不再满足规则),Pod也不会被驱逐
  2. preferredDuringSchedulingIgnoredDuringExecution (软亲和性):

    • 含义: 偏好满足。调度器会尽量将Pod调度到拥有匹配标签的节点上,但如果找不到,也可以将其调度到其他不匹配的节点上。
    • 你可以为每个偏好规则设置一个weight(权重,1-100),调度器会计算每个节点的总权重,并将Pod调度到得分最高的节点上。

示例:将Pod调度到拥有SSD硬盘的节点

# 1. 首先为节点打上标签
# kubectl label nodes <your-node-name> disktype=ssd

# 2. 在Pod Spec中定义Node Affinity
# node-affinity-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: ssd-pod
spec:
  containers:
  - name: my-container
    image: nginx
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 50
        preference:
          matchExpressions:
          - key: availability-zone
            operator: In
            values:
            - zone-a
Pod亲和性/反亲和性(Pod Affinity/Anti-Affinity)

Pod亲和性/反亲和性是更高级的调度策略。它不是根据Node的标签,而是根据已经运行在Node上的其他Pod的标签来决定新Pod的调度位置。

  • Pod亲和性: “物以类聚”。将Pod调度到与某些特定Pod“在一起”的地方。

    • 用例: 将一个Web应用Pod和一个缓存Pod(如Redis)调度到同一个节点或同一个可用区,以减少它们之间的网络延迟。
  • Pod反亲和性: “保持距离”。避免将Pod调度到与某些特定Pod“在一起”的地方。

    • 用例 (极其重要): 部署一个高可用的服务(如replicas: 3的Deployment),使用Pod反亲和性来确保这3个副本不会被调度到同一个Node节点上。这样,即使某个节点宕机,也最多只会损失一个副本,服务依然可用。

这两种策略同样也分为required...(硬性)和preferred...(软性)两种。

一个关键的字段是**topologyKey。它定义了“在一起”或“分开”的拓扑域**。

  • topologyKey: "kubernetes.io/hostname": 拓扑域是单个Node节点
  • topologyKey: "topology.kubernetes.io/zone": 拓扑域是可用区
  • topologyKey: "topology.kubernetes.io/region": 拓扑域是区域

示例:高可用部署,将Nginx副本分散到不同节点

# pod-antiaffinity-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ha
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-ha
  template:
    metadata:
      labels:
        app: nginx-ha # Pod必须有标签供反亲和性规则匹配
    spec:
      containers:
      - name: nginx
        image: nginx
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - nginx-ha
            topologyKey: "kubernetes.io/hostname" # 确保在同一主机名(Node)上,不会有多个app=nginx-ha的Pod

这个配置是实现服务高可用的黄金法则。

Taints(污点)与Tolerations(容忍)

污点和容忍是另一种强大的调度控制机制,它与亲和性逻辑相反。

  • Taint (污点): 应用于Node。一个带有污点的Node会“排斥”Pod。默认情况下,任何Pod都不会被调度到带有污点的Node上。
  • Toleration (容忍): 应用于Pod。Pod通过声明对某个污点的“容忍”,来表示自己可以被调度到带有该污点的Node上。

污点的组成: key=value:Effect

三种Effect:

  1. NoSchedule: 只影响新调度的Pod。调度器不会将不能容忍此污点的Pod调度到该节点上,但不会影响已经运行在该节点上的Pod。
  2. PreferNoSchedule: NoSchedule的软性版本。调度器会尽量避免将Pod调度到该节点上。
  3. NoExecute: 最强效果。不仅会阻止新Pod的调度,还会驱逐节点上已经运行的、但不能容忍此污点的Pod。

核心用例:

  • 专用节点: 管理员可以为一个节点打上污点,例如kubectl taint nodes node1 gpu=true:NoSchedule。这样,该节点就成为了GPU专用节点。只有那些明确声明了可以容忍gpu=true这个污点的Pod(通常是需要GPU的应用)才能被调度上去。
  • Master节点保护: Kubernetes集群的Master节点默认就被打上了NoSchedule污点,以防止用户的应用Pod被错误地调度到控制平面上,从而保证控制平面的稳定性。
  • 基于节点状况的自动驱逐: Kubernetes自身会使用NoExecute污点来处理节点故障。例如,当一个节点网络不通时,Node Controller会为该节点自动添加一个node.kubernetes.io/unreachableNoExecute污点。默认情况下,Pod对这个污点有300秒(5分钟)的容忍时间,如果节点在5分钟内没有恢复,所有Pod就会被自动驱逐到其他健康节点上。

示例:为Pod添加容忍,使其可以调度到Master节点(仅为演示)

# toleration-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: toleration-demo
spec:
  containers:
  - name: nginx
    image: nginx
  tolerations:
  - key: "node-role.kubernetes.io/master"
    operator: "Exists" # Exists 操作符匹配key存在即可,不关心value
    effect: "NoSchedule"

【重难点】 理解Requests和Limits对调度和节点资源分配的影响,灵活运用调度策略实现资源隔离和高可用部署

综合理解与应用:

  • Requests是调度的基石,Limits是运行时的护栏。调度器只关心Requests,它用这个值来做“数学题”,计算节点的剩余容量。而Kubelet则同时关心RequestsLimits,它用Limits来设置Cgroup,防止容器失控。一个常见的错误是只设置Limits而不设置Requests,这会导致Pod的QoS等级为BestEffort,并且调度器无法为其预留资源,极不稳定。

  • 亲和性是“拉力”,污点是“推力”。亲和性是Pod主动去寻找符合条件的Node。而污点/容忍是Node被动地排斥Pod,只有“有通行证”(Toleration)的Pod才能进入。两者结合可以实现非常复杂的调度逻辑。例如,你可以为一个节点池打上污点tenant=team-a:NoSchedule,同时,只给team-a的Pod添加对应的容忍,并使用Node亲和性规则,让这些Pod优先选择该节点池中性能最好的机器。这就实现了租户的硬隔离

  • Pod反亲和性是高可用的核心。在设计任何无状态服务的Deployment或有状态服务的StatefulSet时,都应该默认为其添加基于kubernetes.io/hostname的Pod反亲和性规则。这是用声明式的方式,以极低的成本,实现跨节点容灾的最简单、最有效的方法。可以进一步使用topology.kubernetes.io/zone来实现跨可用区容灾。

  • QoS等级决定了“牺牲”顺序。在资源紧张时,Kubelet需要驱逐Pod来回收资源。它会严格按照**BestEffort -> Burstable -> Guaranteed**的顺序进行驱逐。因此,为你的核心组件(如数据库、监控系统)设置Guaranteed的QoS,是保护它们在极端情况下不被“误杀”的生命线。

通过将资源量化(Requests/Limits/Quotas)与调度策略(Affinity/Taints)结合起来,你就从一个简单的应用部署者,转变为一个能够精细化管理和优化整个Kubernetes集群资源的架构师。

K8s 详细学习笔记 目录

以下是整个系列的10章目录,点击章节标题即可跳转阅读:

更多推荐