第 37 篇 k8s之调度进阶:亲和性、污点与容忍

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。


在第 36 篇中,我们学会了用 Requests 和 Limits 控制 Pod 的资源使用,Scheduler 会依据资源请求自动将 Pod 调度到合适的节点。但资源充足只是调度的最低要求。在生产环境中,你往往需要更精细地决定 Pod 应该落在哪些节点上------比如,Redis 这类磁盘敏感的服务,能否强制调度到挂载了 SSD 的节点?多个 Flask 副本,能否让它们分布在不同的节点以避免单点故障?某些节点是否为 GPU 计算专用,普通业务 Pod 不得进入?

Docker Compose 完全不需要考虑这些问题,因为它只在一台机器上运行。而 Kubernetes 管理的是一个节点池,调度策略直接影响资源利用率、容灾能力和运维复杂度。今天这篇,我们就把 K8s 调度的三大利器------亲和性、污点与容忍------彻底搞懂,并将它们应用到贯穿案例的 Flask + Redis 应用中。

一、节点亲和性(Node Affinity):吸引 Pod 到指定节点

1.1 为什么需要节点亲和性?

默认情况下,Scheduler 会根据 Requests 与节点的可分配资源进行匹配,Pod 可能落在任何符合条件的节点上。但如果你需要"让 Redis 始终使用 SSD 磁盘的节点",或者"让某些服务只在指定的 GPU 节点上运行",就需要节点亲和性

节点亲和性像是一种吸引力规则 ------节点上带有特定的标签,Pod 则通过 affinity 声明"我喜欢带有这些标签的节点"。

1.2 节点标签

首先,你需要给节点打上标签:

bash 复制代码
kubectl label node minikube disktype=ssd

查看标签:

bash 复制代码
kubectl get node minikube --show-labels
# NAME       STATUS   ROLES           AGE   VERSION   LABELS
# minikube   Ready    control-plane   1d    v1.31.0   ...,disktype=ssd

1.3 硬亲和(required)与软亲和(preferred)

节点亲和性分为两种:

  • requiredDuringSchedulingIgnoredDuringExecution (硬亲和):必须满足。如果没有任何节点符合条件,Pod 将无法调度,一直处于 Pending。

  • preferredDuringSchedulingIgnoredDuringExecution (软亲和):倾向满足。Scheduler 会优先调度到符合条件的节点,但如果没有,也可以落在其他节点上。

两者的名字中都有 IgnoredDuringExecution,意思是"一旦 Pod 已经运行,节点标签的变化不会驱逐正在运行的 Pod"。如果需要在标签变化时强制驱逐,需要更复杂的机制(如自定义控制器)。

1.4 示例:将 Redis 强制调度到 SSD 节点

bash 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: redis-ssd
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: disktype
                operator: In
                values:
                  - ssd
  containers:
    - name: redis
      image: redis:alpine

解释:

  • matchExpressions:可以有多个条件,它们之间是 AND 关系。

  • operator: In:标签的值必须在 values 列表中。还有 NotInExistsDoesNotExistGt(大于)、Lt(小于)等。

  • 如果 Minikube 只有单节点并已打上 disktype=ssd,Pod 会正常调度。

部署后查看调度:

bash 复制代码
kubectl get pod redis-ssd -o wide
# NAME        READY   STATUS    RESTARTS   AGE   IP            NODE
# redis-ssd   1/1     Running   0          10s   10.244.1.15   minikube

如果没有匹配节点,Pod 会 Pending:

bash 复制代码
kubectl describe pod redis-ssd | grep -A5 Events
# Warning  FailedScheduling  ...  0/1 nodes are available: 1 node(s) didn't match node selector.

1.5 软亲和示例

bash 复制代码
preferredDuringSchedulingIgnoredDuringExecution:
  - weight: 100
    preference:
      matchExpressions:
        - key: disktype
          operator: In
          values:
            - ssd

weight 是权重(1-100),Scheduler 会根据权重对节点打分。如果集群中有多个节点,权重越高的条件越优先被满足。

二、Pod 亲和性与反亲和性(Pod Affinity/Anti-affinity):Pod 之间的拓扑关系

2.1 什么是 Pod 亲和性?

节点亲和性解决了"Pod 与节点的关系",但实际中更常见的是"Pod 与 Pod 的关系"。例如:

  • Pod 亲和性:把 Web 应用调度到缓存所在节点的附近,降低延迟。

  • Pod 反亲和性:将同一服务的多个副本分散到不同节点上,避免单节点故障导致服务全部不可用。

Pod 亲和性与反亲和性通过 podAffinitypodAntiAffinity 实现,规则基于 Pod 的标签,并且可以根据拓扑域 (topologyKey)来控制亲和性的粒度。常见的 topologyKeykubernetes.io/hostname(节点级别)、topology.kubernetes.io/zone(可用区级别)。

2.2 示例:让 Flask 靠近 Redis(Pod 亲和性)

假设你有一个 Redis Pod 带着标签 app=redis,你希望 Flask Pod 尽量与它在同一个节点上。

首先创建 Redis Pod 并打标签(如果还未创建):

bash 复制代码
kubectl run redis --image=redis:alpine --labels=app=redis

现在创建一个 Flask Deployment,声明 Pod 亲和性:

bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-affinity
spec:
  replicas: 2
  selector:
    matchLabels:
      app: flask-counter
  template:
    metadata:
      labels:
        app: flask-counter
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - redis
              topologyKey: kubernetes.io/hostname
      containers:
        - name: flask
          image: flask-redis-counter:3.0
          ports:
            - containerPort: 5000

这里 requiredDuringSchedulingIgnoredDuringExecution 要求 Flask Pod 必须与拥有标签 app=redis 的 Pod 调度在同一个 hostname 上。如果 Redis Pod 不存在或没有相同拓扑的节点,Flask Pod 无法调度。

在 Minikube 单节点中,这始终成立。在多节点集群中,你会看到 Flask Pod 被拉到 Redis 所在的节点。

2.3 示例:让 Flask 副本分散部署(Pod 反亲和性)

为了高可用,我们希望多个 Flask 副本不要挤在同一个节点上。使用 Pod 反亲和性:

bash 复制代码
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
            - key: app
              operator: In
              values:
                - flask-counter
        topologyKey: kubernetes.io/hostname

这段配置要求:如果某节点上已存在带有 app=flask-counter 标签的 Pod,则新 Pod 不能再调度到同一 hostname 上。这就强制了每个节点最多只运行一个副本

  • 优点:彻底的故障隔离,一个节点宕机只会影响一个副本。

  • 缺点 :如果你的副本数超过节点数,多余的 Pod 将无法调度,处于 Pending 状态。此时应考虑使用软反亲和性(preferredDuringSchedulingIgnoredDuringExecution),允许在必要时放宽限制。

2.4 topologyKey 的选择

  • kubernetes.io/hostname:节点级分布。

  • topology.kubernetes.io/zone:可用区级分布(需要云平台支持),防止整个可用区故障。

  • 自定义标签:如 rackfailure-domain

在多副本有状态应用(如数据库集群)中,反亲和性结合 topologyKey: hostname 可以确保每个副本都独占一个节点,避免资源竞争和单点故障。

三、污点与容忍(Taints and Tolerations):排斥与准入

3.1 污点的概念

如果说亲和性是"吸引",那么污点就是"排斥"。节点上的污点(Taint)会阻止没有相应容忍(Toleration)的 Pod 被调度到该节点上,甚至驱逐已在运行的 Pod。

污点有三个要素:keyvalueeffect

effect 有三种:

  • NoSchedule:不调度新的 Pod,但不影响已在运行的 Pod。

  • PreferNoSchedule:尽量不调度新的 Pod,但不强制。

  • NoExecute :不仅不调度新的 Pod,还会驱逐已经运行的、没有相应容忍的 Pod。

3.2 给节点添加污点

bash 复制代码
# 给节点 minikube 添加污点:专用 GPU
kubectl taint node minikube gpu=true:NoSchedule

查看污点:

bash 复制代码
kubectl describe node minikube | grep Taints
# Taints:             gpu=true:NoSchedule

现在,普通 Pod 将无法调度到这个节点上(除非它们有容忍)。这可以用来保护专用节点。

3.3 Pod 容忍污点

如果想让某个 Pod 能够调度到有污点的节点,需要在 Pod 中配置容忍:

bash 复制代码
tolerations:
  - key: "gpu"
    operator: "Equal"
    value: "true"
    effect: "NoSchedule"

你也可以容忍所有污点:

bash 复制代码
tolerations:
  - operator: "Exists"

但谨慎使用,可能让 Pod 跑到不合适的节点上。

3.4 污点的典型场景

  • GPU 专用节点 :给节点打上 gpu=true:NoSchedule,只有请求了 GPU 资源的 Pod 配置了对应容忍。

  • 控制平面隔离 :默认 master 节点有污点 node-role.kubernetes.io/control-plane:NoSchedule,防止普通业务 Pod 调度上去。

  • 节点维护 :在维护前给节点添加 NoExecute 污点,Pod 会自动被驱逐迁移。

3.5 移除污点

bash 复制代码
kubectl taint node minikube gpu=true:NoSchedule-

注意末尾的 - 表示移除污点。

四、实战:为 Flask + Redis 配置调度策略

现在将亲和性和反亲和性应用到贯穿案例中。我们需要:

  • 确保 Redis 调度到有标签 tier=backend 的节点(节点亲和性)。

  • 确保 Flask 副本尽量与 Redis 在同一可用区(Pod 亲和性),但副本之间要分散在节点上(Pod 反亲和性)。

  • 为 Redis 节点添加污点,只允许 Redis 类 Pod 调度(可选)。

4.1 准备节点

Minikube 单节点做不了完整的多节点演示,但我们可以练习配置。先给节点加标签和污点(用于学习,不要影响其他 Pod,可使用 PreferNoSchedule)。

bash 复制代码
kubectl label node minikube tier=backend
kubectl taint node minikube tier=backend:PreferNoSchedule

4.2 创建 Redis Deployment 带节点亲和性和容忍

bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
      tier: backend
  template:
    metadata:
      labels:
        app: redis
        tier: backend
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: tier
                    operator: In
                    values:
                      - backend
      tolerations:
        - key: "tier"
          operator: "Equal"
          value: "backend"
          effect: "PreferNoSchedule"
      containers:
        - name: redis
          image: redis:alpine

4.3 创建 Flask Deployment 带 Pod 亲和性和反亲和性

bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: flask-counter
  template:
    metadata:
      labels:
        app: flask-counter
    spec:
      affinity:
        # 亲和性:靠近 Redis
        podAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: tier
                      operator: In
                      values:
                        - backend
                topologyKey: kubernetes.io/hostname
        # 反亲和性:副本分散
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - flask-counter
                topologyKey: kubernetes.io/hostname
      containers:
        - name: flask
          image: flask-redis-counter:3.0
          ports:
            - containerPort: 5000
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"

我们使用了 preferredDuringSchedulingIgnoredDuringExecution(软亲和)来避免单节点环境中因为满足不了硬条件而无法调度。在生产多节点环境中,可以切换为 required

部署后查看 Pod 分布(单节点全部在 minikube):

4.4 验证亲和性和容忍效果

检查事件,确认调度器考虑了亲和性:

bash 复制代码
kubectl describe pod <flask-pod> | grep -A5 "Affinity"

由于 Minikube 单节点,我们难以看到分散效果,但配置可以无缝迁移到多节点集群。

4.5 清理污点

bash 复制代码
kubectl taint node minikube tier=backend:PreferNoSchedule-

五、与 Docker Compose 的对比

Compose 没有调度概念,所有容器都在同一台宿主机上,你可以通过 depends_on 控制启动顺序,但不能控制容器在主机内的分布(单机不存在分布问题)。K8s 的亲和性和污点机制为多节点集群提供了细粒度的调度控制,这是编排平台走向大规模生产的必然需求。

六、命令速查表

七、本篇总结

  • 节点亲和性 :通过 nodeAffinity 将 Pod 调度到带有特定标签的节点上,支持硬性和软性两种策略。

  • Pod 亲和性与反亲和性 :通过 podAffinitypodAntiAffinity 控制 Pod 之间的拓扑关系,实现拉近或分散部署,提高性能或可用性。

  • 污点与容忍 :通过 TaintToleration 实现节点的专属访问控制,防止无关 Pod 调度,支持 NoSchedulePreferNoScheduleNoExecute 效果。

  • 调度策略的实际价值:合理的亲和性和反亲和性配置是实现高可用部署、资源隔离和性能优化的关键手段,也是 K8s 集群运维中常被忽略的细节。

通过本篇,你获得了控制 Pod 落点的完整工具集。下一篇文章------第 38 篇:安全:RBAC 与 ServiceAccount 实战------我们将进入 K8s 安全领域,学习如何通过基于角色的访问控制来保护集群资源。

想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !

相关推荐
EntyIU1 小时前
DOCKER_CHEATSHEET
运维·docker·容器
SilentSamsara1 小时前
Python 与 Docker:多阶段构建、最小镜像与健康检查
运维·开发语言·python·docker·中间件·容器
小猿姐2 小时前
三种 MongoDB Operator 实测对比:Community、Percona 与 KubeBlocks,谁更适合团队落地?
运维·mongodb·kubernetes
IT策士2 小时前
第 38 篇 k8s之RBAC 与 ServiceAccount 实战
云原生·容器·kubernetes
IT策士3 小时前
第 36 篇 k8s之资源管理:Requests、Limits 与 QoS
云原生·容器·kubernetes
_可乐无糖4 小时前
踩完坑之后的总结:Windows安装docker
运维·windows·docker·容器
zhangfeng11334 小时前
,在slurm中也能安装ubundu了,Singularity(现叫 Apptainer)不需要root权限的容器方案,对比docker
运维·人工智能·机器学习·docker·容器
章老师说4 小时前
B站网关事故背后:OpenResty 与 Lua 的稳定性代价
nginx·云原生·负载均衡·lua·openresty
IDIOT___IDIOT5 小时前
Docker 集群运行 Spark 的一些记录
docker·容器·spark