查看源文件

K8s 标签与选择器避坑:5 条规则与生产案例

部署一个应用时,Deployment 和 Service 都创建成功,Pod 也处于 Running 状态,访问服务却没有响应;给 Pod 配上节点选择条件后,新副本又一直 Pending。遇到这些现象,可以先检查一组容易被忽略的配置:标签和选择器。

在 Kubernetes 中,一个应用可能有多个副本,发布时还会不断创建新 Pod、替换旧 Pod。Pod 名称会变化,Service 需要持续找到符合条件的后端,调度器也需要知道哪些节点适合承载工作负载。标签为对象提供分类信息,选择器则把这些分类信息变成可执行的筛选条件。

标签一旦被选择器使用,改动它就可能影响资源之间的关联。设计时需要区分哪些字段用于稳定地识别一组资源,哪些字段会随版本或组织变化,避免把需要经常调整的信息写进固定的选择条件。

一、让标签清楚地表达资源身份

标签描述对象,选择器筛选对象

标签(Label)是附加在 Kubernetes 对象上的键值对,保存在 metadata.labels 中。一个对象可以有多个标签,同一个标签也可以出现在多个对象上,因此标签适合表达应用、环境、团队等分类信息,并不要求全局唯一。

例如,app=nginx 表示应用名称,environment=production 表示环境;Node 上的 node-type=gpu 则可以表示节点类型。选择器(Selector)根据这些键和值筛选对象。

不同位置的选择器,筛选目标也不同:

配置位置筛选目标用途
Deployment 的 spec.selectorPod定义控制器管理的 Pod 范围,Deployment 通过 ReplicaSet 管理副本
Service 的 spec.selector同一命名空间内的 Pod为服务发现后端
Pod 的 spec.nodeSelectorNode限制 Pod 可以调度到哪些节点

排查标签问题时,先确认选择器正在筛选哪一种对象。Service 读取的是 Pod 的标签,nodeSelector 匹配的是 Node 的标签,两者不会互相替代。

注解(Annotation)保存在 metadata.annotations 中,适合记录构建详情、联系人、工具配置等信息。工具和控制器也会读取注解并据此工作,但标签选择器不按注解筛选对象。把“是否需要参与分类和选择”作为区分依据,比把注解理解成“只给人看”更准确。Kubernetes 官方对注解的说明也明确区分了这两种用途。

用前缀区分标签的管理来源

标签键由可选的前缀和必需的名称组成,中间用 / 分隔,例如 company.com/team。没有前缀的 app、team 也合法,可以正常参与选择。

官方约定把无前缀的键视为用户私有;自动化系统组件向用户对象添加标签时,应使用前缀。团队维护的 CI/CD 系统或控制器可以使用自己的域名,减少与其他工具重名的机会:

# 推荐
company.com/team: payments
company.com/environment: production
 
# 也能用,但人多了容易重名
team: payments
env: prod

前缀用于划分命名空间,不会自动产生权限隔离。kubernetes.io/ 和 k8s.io/ 保留给 Kubernetes 核心组件;使用已有标准标签时遵循它们的定义,自定义业务标签使用自己的前缀。

环境值也需要统一。prod 和 production 都合法,但选择器会把它们视为不同的值,团队应选定一种写法。

写入前,先确认键和值是否合法

标签的格式限制可以归纳为以下几项:

  • 键的名称部分最长 63 个字符,必须以 ASCII 字母或数字开头、结尾,中间可以包含 -、_ 和 .。
  • 可选前缀必须符合 DNS 子域名格式,总长度不超过 253 个字符。
  • 值最长 63 个字符,可以是空字符串;非空值同样要求以 ASCII 字母或数字开头、结尾,中间允许 -、_ 和 .。
  • 键和值都按字符串处理。在 YAML 中,布尔值、数字形式的内容应加引号,例如 enabled: "true"。

如果只需要表达某个标记是否存在,可以写 critical-service: "",再按键是否存在进行筛选。空字符串是合法值,YAML 中省略值产生的 null 则需要另行处理。

这些是 Labels and Selectors 官方文档定义的格式要求。团队内部可以进一步约定键名和值域,例如只允许 environment 取 development、staging、production。

优先复用应用标签,再补充业务维度

Kubernetes 推荐了 app.kubernetes.io/* 这一组应用标签。它们为工具提供共同的描述方式,适合在相关资源上保持一致:

标签键说明示例
app.kubernetes.io/name应用名称payment-gateway
app.kubernetes.io/instance应用实例标识payment-gateway-prod-01
app.kubernetes.io/version应用版本2.3.1
app.kubernetes.io/component应用中的组件api、database、cache
app.kubernetes.io/part-of所属的上层应用trade-system
app.kubernetes.io/managed-by管理工具helm、argocd、terraform

这套标签是推荐约定,核心工具不强制要求使用,也不能保证每个工具都会自动处理全部标签。实际采用时,先确定各字段的含义,再让模板和工具沿用同一套规则。详细定义见官方推荐标签。

二、先验证选择结果,再把选择器写进配置

标签设计好以后,可以先用 kubectl get 验证筛选结果。这样既能熟悉语法,也能在创建 Service 或修改配置前,确认条件有没有选多、选少。

从等值和不等值查询开始

查询 default 命名空间中属于 Nginx、环境为生产的 Pod:

kubectl get pods -n default -l 'app=nginx,environment=production' --show-labels

这里的逗号表示逻辑 AND,两个条件必须同时满足。Pod 可以带有其他标签,无须与选择器逐项完全相同。

等值选择器支持 =、== 和 !=。例如,排除 staging 环境:

kubectl get pods -n default -l 'environment!=staging'

这条命令还会选中没有 environment 标签的 Pod。如果要求标签必须存在,再排除 staging,需要同时写上存在条件:

kubectl get pods -n default -l 'environment,environment!=staging'

用集合条件表达多个候选值

想同时查询 production 和 staging,可以使用 in:

kubectl get pods -n default -l 'app=nginx,environment in (production,staging)'

常用的集合条件如下:

查询条件含义
environment in (production,staging)值属于给定集合
environment notin (development,test)值不属于给定集合,也包括没有该键的对象
app存在 app 这个键,不限制值
!app不存在 app 这个键

多个要求之间仍是 AND。in 可以给同一个键列出多个候选值,但通用标签选择器没有把任意两个条件用 || 连接的语法。命令中的表达式建议用单引号包住,避免括号、感叹号等字符被 Shell 解释。

YAML 写法取决于资源字段

命令行中的选择器表达式,不能直接照搬成每一种资源的 YAML。Service 的 spec.selector 是键值映射,只支持等值匹配:

# Service 配置片段
spec:
  selector:
    app: nginx
    environment: production

写成 environment: "!= staging",含义是把 != staging 当成标签值,不会执行“不等于”判断,而且这个值不符合标签格式,会被校验拒绝。Service 的这个字段也不支持 matchExpressions。

Deployment 等资源使用的标签选择器结构,可以组合 matchLabels 和 matchExpressions:

# Deployment 的选择器片段
spec:
  selector:
    matchLabels:
      app: nginx
    matchExpressions:
      - key: environment
        operator: In
        values: ["production", "staging"]
      - key: company.com/team
        operator: Exists

这里要求 Pod 同时满足:app=nginx,环境属于给定集合,并且存在 company.com/team 标签。matchLabels 和全部 matchExpressions 共同生效。

matchExpressions 支持 In、NotIn、Exists、DoesNotExist。前两者的 values 必须非空,后两者不填写 values。以上都是字段片段,部署时还需要补齐对应资源的其他必需字段。

三、固定控制器的选择条件,保持 Service 与 Pod 对应

用一份完整配置看清标签放在哪里

理解了选择器,再看一个 Nginx 应用。下面使用简短的 app 标签,便于观察 Deployment、Pod 和 Service 的对应关系;实际项目可以统一换成前面介绍的推荐标签。

将以下内容保存为 nginx-label-demo.yaml,在测试集群的 default 命名空间中使用:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
      environment: production
  template:
    metadata:
      labels:
        app: nginx
        environment: production
        app.kubernetes.io/version: "1.28"
    spec:
      containers:
        - name: nginx
          image: nginx:1.28
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: http
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
spec:
  selector:
    app: nginx
    environment: production
  ports:
    - name: http
      port: 80
      targetPort: http
kubectl apply -n default -f nginx-label-demo.yaml
kubectl rollout status deployment/nginx-deploy -n default

这份配置中,有三处容易混淆:

位置实际作用
Deployment 的 metadata.labels标记 Deployment 对象本身
Deployment 的 spec.template.metadata.labels写入新建 Pod 的标签
Service 的 spec.selector根据实际 Pod 标签发现后端

只给 Deployment 自身添加标签,Pod 不会自动继承。Service 也不会根据 Deployment 的名称或顶层标签寻找后端。

示例中的 Pod 比 Service 的选择条件多了一个版本标签,这完全合法。选择器只要求指定条件全部满足,Pod 上的其他标签不影响匹配。

把稳定身份留在 selector 中

在 apps/v1 中,Deployment 的 spec.selector 创建后不可修改,且必须与 Pod 模板标签匹配。尝试更新这个字段,API 会拒绝请求,常见报错包含 field is immutable。Deployment 官方文档明确说明了这一限制。

因此,selector 应使用生命周期内保持稳定的标签,例如应用名、实例标识;通常也应让不同 Deployment 的选择范围保持区分。示例中的应用和环境都不会随每次发布变化,版本标签则放在 Pod 模板中。

如果确实要更换整套选择条件,可以创建名称和选择范围不同的新 Deployment,确认新副本就绪后再调整 Service 的流量指向,并安排旧工作负载退出。直接删除原 Deployment 再重建,可能造成服务中断,不应作为默认的修复步骤。

版本标签可以用于查询、监控和审计,也可以在有意设计的蓝绿发布中参与 Service 的版本选择。需要避免的是:把每次发布都会变化的版本值放进同一个 Deployment 的固定 selector 中。

还要分清标签所在的层级。修改 Deployment 顶层的 metadata.labels 不会触发滚动更新;修改 spec.template.metadata.labels 会改变 Pod 模板,从而触发新一轮 rollout。更新模板里的容器镜像也会触发 rollout。Helm chart 版本号本身并不是 Deployment 的触发条件,最终要看渲染后的 Pod 模板是否变化。

Service 没有可用后端时,按顺序检查

Service 创建成功,只能说明资源已被接受,不能说明它已经有可用后端。可以依次查看选择器、匹配的 Pod 和 EndpointSlice:

# 查看 Service 的选择条件和端口配置
kubectl describe service nginx-svc -n default
 
# 用相同条件直接查询 Pod
kubectl get pods -n default -l 'app=nginx,environment=production' -o wide --show-labels
 
# 查看这个 Service 对应的后端地址和就绪状态
kubectl get endpointslices -n default -l 'kubernetes.io/service-name=nginx-svc' -o yaml

根据结果继续排查:

  1. 如果查不到 Pod,先检查命名空间、标签键和值,以及 Deployment 是否成功创建了副本。
  2. 如果能查到 Pod,检查它是否取得 Pod IP、是否 Ready,再查看 EndpointSlice 中的地址和 conditions.ready。Running 状态并不等于通过了就绪检查。
  3. 如果后端存在且就绪,再检查 targetPort、容器实际监听端口和网络策略等因素。标签匹配正确,只解决了后端选择这一环。

在默认配置下,未就绪的 Pod 不会作为普通的就绪后端参与转发;设置 publishNotReadyAddresses 会改变相关行为。排查时应结合 Service 配置和 EndpointSlice 条件判断,不能仅凭“没有可用地址”就认定 selector 写错了。

旧资料常使用 kubectl get endpoints nginx-svc -n default 检查后端。Endpoints API 从 Kubernetes v1.33 起已弃用,新排查流程优先使用 EndpointSlice,见官方 Service 说明。

如果最后确认需要修正标签,要回到配置来源处理。普通 Pod 的标签可以原地修改,无须因为改标签就重建;对于 Deployment 管理的 Pod,应维护 Pod 模板,使后续副本也获得正确标签。模板标签变化会触发 rollout,同时仍须满足原有 selector。只手工修补某一个 Pod,下一次重建时可能再次丢失。

四、用节点标签约束调度,分清选择与隔离

nodeSelector 匹配的是 Node

Service 根据 Pod 标签选择后端,调度器则根据节点标签寻找可用节点。例如,希望 Pod 只运行在同时具备 GPU 类型标记和 SSD 标记的节点上:

# Pod 配置片段;在 Deployment 中应放到 spec.template.spec 下
spec:
  nodeSelector:
    node-type: gpu
    disk-type: ssd

两个条件必须同时满足,可以先查询集群中有没有这样的节点:

kubectl get nodes --show-labels
kubectl get nodes -l 'node-type=gpu,disk-type=ssd' --show-labels

nodeSelector 是硬性筛选条件。没有节点满足时,Pod 会保持 Pending。即使有节点满足标签条件,调度还需要考虑节点状态、资源请求、污点和其他约束;node-type=gpu 这个标签本身也不会替应用申请 GPU 设备。

如果需要“优先使用 SSD,条件不满足时允许去其他节点”,可以考虑节点亲和性中的软性偏好;如果只接受指定节点,nodeSelector 就能表达简单的等值条件。两类配置的区别见官方节点分配文档。

nodeSelector 与污点解决不同方向的问题

为 Pod 设置 nodeSelector,只限制这个 Pod 的候选节点,其他没有此限制的 Pod 仍可能被调度到这些节点。专用节点池还需要考虑如何限制其他工作负载进入。

需求适用配置需要理解的边界
让某个 Pod 只去指定节点nodeSelector 或必需的节点亲和性节点还要满足资源等其他条件
不让缺少相应容忍的 Pod 新调度进来NoSchedule 污点与 Toleration不会驱逐已经运行的 Pod
需要对不容忍该污点的已有 Pod 产生驱逐效果NoExecute 污点是否驱逐及何时驱逐取决于相应容忍配置

Toleration 允许 Pod 容忍对应污点,并不保证它一定调度到该节点。需要专用节点池时,可以组合污点与节点选择条件:前者限制哪些 Pod 能进入,后者限制目标 Pod 去哪里。具体语义见官方污点与容忍文档。

维护节点、使用 Spot 节点时,也需要按目标选择措施。例如,NoSchedule 只能阻止不容忍该污点的新 Pod 进入,不能完成已有工作负载的迁移,也不能阻止云平台回收实例。

Pending 和节点标签漂移要分开定位

Pod 一直 Pending 时,先查看调度事件:

kubectl describe pod <pod-name> -n <namespace>

如果事件包含 didn't match Pod's node affinity/selector 或类似的节点选择失败提示,检查 nodeSelector 和节点亲和性中的实际要求,再与 Node 标签逐项比较。如果事件指向资源不足或不容忍的污点,应沿对应原因继续排查。

仅改变 Node 标签,不会因为原有 nodeSelector 不再匹配,就自动驱逐或重新调度已经运行的 Pod;新建、扩容或替换出来的 Pod 却要重新经过筛选。因此,修改节点池标签前,要评估下一次扩容、发布时是否还有合适节点。

标签被反复覆盖时,需要查清节点池配置、基础设施代码或控制器中,谁在维护该字段。可以先查看对象信息,在确认预期值后做临时修复:

kubectl get node <node-name> -o yaml --show-managed-fields
kubectl label nodes <node-name> <key>=<value> --overwrite

managedFields 可以提供字段管理线索;需要确认具体变更行为时还应结合审计日志。长期修复应落在配置来源,避免手工改回后又被自动化覆盖。已有的 node-role.kubernetes.io/<role> 按集群约定使用;自定义节点池属性可以写成 mycompany.com/node-pool: high-memory,不必另造一个保留前缀下的角色名。

五、让标签能长期维护,服务于查询与成本归因

一个标签保存一个值,多个维度分别表达

标签值是字符串,选择器不会把它拆成数组。下面这段配置既不能表达环境集合,其中的逗号也不符合标签值的字符规则,会被 API 校验拒绝:

# 错误示范,不要应用
labels:
  environments: "prod,staging,dev"

一个对象属于哪个环境,就写对应值。不同维度分别使用独立标签,布尔含义也写成字符串:

labels:
  environment: production
  feature-canary: "true"
  internet-facing: "false"

如果需要查询多个环境,应让各对象分别带有自己的环境标签,再使用 environment in (production,staging) 查询。In 匹配的是完整标签值。

成本和监控需要事先约定归属字段

在共享集群中,节点账单通常无法直接回答某个团队、项目消耗了多少资源。可以在应用标签之外补充归属维度,并让采集和成本工具使用相同定义:

labels:
  company.com/team: payments          # 哪个团队
  company.com/cost-center: fintech    # 哪个成本中心
  company.com/environment: production # 哪个环境
  company.com/project: wallet-v2      # 哪个项目

这些字段应进入需要归因的对象。若成本工具按 Pod 统计,就要检查 Deployment 的 Pod 模板是否包含它们;只标记 Deployment 顶层,不会让 Pod 自动得到这些字段。监控系统是否采集并保留这些标签,也取决于具体集成配置。

以 AWS EKS 为例,Split Cost Allocation Data 可以将 Pod 标签导入成本分配标签,但需要启用相应功能,并在管理账户激活相关成本标签。该集成每个 Pod 最多导入 50 个 Kubernetes 标签,按字母顺序选取;这是 AWS 成本导入的限制,不能套用成 Kubernetes 的标签数量上限。具体前提和限制见 AWS 官方说明。

模板统一注入,也要检查注入的位置

使用 Helm 或 Kustomize 维护公共标签,可以减少逐个文件手写造成的遗漏。不过,合并后的标签究竟进入资源顶层、Pod 模板还是 selector,需要检查最终渲染结果。

版本、团队等可能变化的字段,不应因为“统一注入”就被带进 Deployment 的固定 selector。建议在配置审查时确认:

  • 稳定的选择标签与可变化的描述标签分别维护。
  • 应用、环境和归属字段进入了预期的对象层级。
  • 模板渲染后的标签值仍是字符串,没有意外产生 null。
  • 新旧工作负载的选择范围符合预期,不会让 Service 选入其他实例。

标签数量以实际查询和管理需求为准,避免保留无人使用的重复字段。对象大小还受 API 和存储层约束,例如 etcd 默认限制单个请求为 1.5 MiB,这并不对应固定的标签数量配额。长说明、构建详情适合放进注解,整个对象仍需遵守相应大小限制。

遇到 null,先检查渲染结果和筛选条件

在 YAML 中,team: 会被解析成 null,而 team: "" 才是明确的空字符串。需要表达存在标记时,应选择后者;字段没有实际意义时,可以让模板省略该键。

还要留意带 -l 的 apply 操作。Kubernetes 的官方问题记录报告过一个案例:某个无关标签被渲染成 null,在 kubectl v1.30.5 中,即使筛选的是另一个有值的标签,也会出现 error: no objects passed to apply。

# 问题记录中的标签片段
metadata:
  labels:
    label_a: null
    label_b: "bar"
kubectl apply -f test_file.yaml -l 'label_b=bar'

报告者在 v1.33.0 中复测通过,并观察到该案例中客户端 apply 省略了空值标签,服务端 apply 将其保存为空字符串。这是该案例的复测结果,不能据此把所有 no objects passed to apply 都归因于版本问题。

排查时可以按以下顺序处理:

  1. 查看 Helm 或 Kustomize 的最终渲染结果,确认输入里确实有资源,并检查是否存在空文档、labels: null 或标签成员为 null。
  2. 对照 -l 条件检查资源自身的 metadata.labels。例如,筛选 Deployment 时,不会因为 Pod 模板内有相同标签就选中它。
  3. 记录 kubectl 客户端版本,必要时在与 API Server 兼容的范围内升级并复测,同时修正产生空值的模板。显式使用空字符串或省略无用字段,比依赖客户端替你转换更容易维护。