节点池不是云厂商才有的概念。即使所有节点都来自本地虚拟化平台,也应该用标签、污点和亲和性把不同故障域分开。
系列导航:Kubernetes 重建迁移教程系列。本篇在基础组件验收通过后执行。
这篇文章从一份节点清单开始,把规划真正落实到 Kubernetes 调度规则中。
第 1 步:先按职责分池
一个通用的起点是:
| 节点池 | 典型工作负载 | 是否建议独占 |
|---|---|---|
| control-plane | API Server、etcd | 是 |
| ingress | Ingress Controller、四层入口 | 是 |
| platform | Nacos、DevOps、核心平台 | 视容量而定 |
| app-* | 各业务环境 | 生产建议 |
| data | MySQL、Redis、Kafka、RabbitMQ、ZooKeeper | 是 |
| observability | Prometheus、日志、追踪 | 建议 |
| shared | 小型无状态服务 | 否 |
| reserve | 故障和滚动升级预留 | 不部署常规业务 |
先给每个节点分配一个主池,不要让同一台机器同时出现在两个独占池。
第 2 步:把规划写成 CSV 并校验
hostname,pool,cpu,memory_gib,disk_type,failure_domain
worker-a,app-test,8,32,ssd,host-a
worker-b,app-test,8,32,ssd,host-b
worker-c,app-test,8,32,ssd,host-c
检查重复:
cut -d, -f1 inventory/node-pools.csv | tail -n +2 | sort | uniq -d
检查每个池的节点数:
cut -d, -f2 inventory/node-pools.csv | tail -n +2 | sort | uniq -c
规划高可用三副本时,要同时考虑 Kubernetes 节点和虚拟化宿主机两个层面。三个 Pod 落在三台虚拟机,但若三台虚拟机位于同一台物理宿主机上,仍是单点。
第 3 步:先添加描述性标签
标签描述“节点是什么”,不产生排他效果:
kubectl label node worker-a workload.yj/pool=app-test
kubectl label node worker-a topology.yj/failure-domain=host-a
kubectl label node worker-a hardware.yj/disk=ssd
批量完成后检查:
kubectl get nodes -L workload.yj/pool,topology.yj/failure-domain,hardware.yj/disk
自定义标签应使用自己的 DNS 前缀,避免与 Kubernetes 保留标签冲突。
第 4 步:再决定哪些池需要污点
污点表示“默认不允许谁来”。对于独占数据池,可以添加:
kubectl taint node data-a workload.yj/pool=data:NoSchedule
kubectl taint node data-b workload.yj/pool=data:NoSchedule
kubectl taint node data-c workload.yj/pool=data:NoSchedule
检查:
kubectl describe node data-a | sed -n '/Taints:/,/Unschedulable:/p'
不要先给几十个节点加污点,再回头修改 Deployment。正确顺序是:
准备应用 toleration 和 nodeAffinity
→ server-side dry-run
→ 给第一台节点加污点
→ 验证调度
→ 批量推广
第 5 步:应用同时声明容忍和目标节点
仅配置 toleration 表示“允许进入”,并不保证 Pod 一定进入数据池,应同时使用 nodeAffinity:
spec:
template:
spec:
tolerations:
- key: workload.yj/pool
operator: Equal
value: data
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload.yj/pool
operator: In
values: [data]
先做 dry-run:
kubectl apply --server-side --dry-run=server -f workload.yaml
第 6 步:让有状态副本分散
Kafka、RabbitMQ、ZooKeeper、MySQL 副本不能落在同一节点。使用 Pod 反亲和:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: zookeeper
topologyKey: kubernetes.io/hostname
如果还要跨虚拟化宿主机,可再使用自定义故障域标签:
topologyKey: topology.yj/failure-domain
节点不足时,required 会让 Pod 保持 Pending。这比把三个副本挤到同一节点上更诚实;应扩容节点或调整容量规划,而不是默默降级故障域。
第 7 步:为滚动升级保留容量
假设某业务有两个 Pod,每个 request 为 2 核 6 GiB。滚动升级使用默认 maxSurge: 25% 时,至少需要临时容纳第三个 Pod。
可用资源要按 request 计算,而不是按实际瞬时使用量:
kubectl describe node <node> | sed -n '/Allocated resources:/,$p'
kubectl top node <node>
两者用途不同:
describe的 Allocated resources 是调度器已经承诺的 request/limit。top是当前实际使用量。
不要让低优先级业务长期占用预留池,否则故障迁移和滚动发布都会卡住。
第 8 步:验证调度结果
部署后执行:
kubectl get pods -A -o custom-columns='NS:.metadata.namespace,POD:.metadata.name,NODE:.spec.nodeName' \
| sort
kubectl get pods -n <namespace> -l app.kubernetes.io/name=<app> -o wide
kubectl get events -A --field-selector reason=FailedScheduling --sort-by=.lastTimestamp
重点检查:
- 独占池是否混入无关 Pod。
- 三副本是否落在不同节点和故障域。
- DaemonSet 是否因缺少 toleration 而无法调度到带污点的节点。
- 预留节点是否仍为空闲。
- 小规格节点是否承载了不适合的重型服务。
实战复盘:0/69 nodes are available 应该怎么读
一次双副本服务发布时,事件持续报告:
0/69 nodes are available:
several nodes had untolerated taint,
some nodes didn't match Pod's node affinity/selector,
some nodes had insufficient memory
这不表示 69 台机器全部内存不足。调度器会依次过滤:
flowchart LR
A["69 个节点"] --> B["Taint / Toleration"]
B --> C["nodeSelector / nodeAffinity"]
C --> D["CPU / Memory request"]
D --> E["Topology spread / anti-affinity"]
E --> F["可调度节点"]
应先把 Pod 的约束完整打印出来:
kubectl -n <namespace> get pod <pending-pod> -o jsonpath='
nodeSelector={.spec.nodeSelector}{"\n"}
affinity={.spec.affinity}{"\n"}
tolerations={.spec.tolerations}{"\n"}
requests={.spec.containers[*].resources.requests}{"\n"}
spread={.spec.topologySpreadConstraints}{"\n"}'
再逐层检查候选节点:
kubectl get node --show-labels
kubectl describe node <candidate-node>
kubectl get events -n <namespace> \
--field-selector reason=FailedScheduling \
--sort-by=.lastTimestamp
为什么两个 Pod 实际只用 2G,第三个仍提示内存不足
调度器依据的不是“两个现有 Pod 当前合计用了多少”,而是新 Pod 的 memory request 与节点上剩余可用的 allocatable:
可调度
⇔ node allocatable
- 已调度 Pod requests
≥ incoming Pod request
如果每个 Pod request 为 3.5 GiB,即使两个现有实例当前各用约 1 GiB,新 revision 的 surge Pod 仍要完整申请 3.5 GiB。
这也是资源治理后来把普通服务、仓储服务和重型服务分档的原因:request 应覆盖稳态负载与可解释的峰值,但不能用一个过大的通用值占满所有专属节点。
为什么看起来有空节点,Pod 仍然不能使用
“节点没有业务 Pod”不等于“这个 Pod 可以调度进去”。它可能:
- 带有当前 Pod 不容忍的
NoSchedule污点。 - 缺少 required nodeSelector 标签。
- 不属于 topology spread 的 eligible domain。
- 已经被其他 Pod 的 request 预留。
- 是故障或滚动升级预留节点。
因此不能仅凭节点列表推断“还有 9 台服务器空闲”。先把污点、标签和 allocated requests 一起统计。
火山引擎为什么把手工删除的标签和污点写回来
托管节点池的配置是控制面的期望状态。若节点池中仍声明:
app-type=<dedicated-service>
taint app-type=<dedicated-service>:NoSchedule
只执行:
kubectl label node <node> app-type-
kubectl taint node <node> app-type-
可能只是临时改变 Kubernetes Node 对象。节点池控制器再次同步时会恢复这些字段。
火山引擎 VKE 节点池文档明确说明,节点池中新增或修改标签、污点,会自动更新新建节点和存量节点。
要把专属节点永久迁入公共池,应:
- 先确认该节点不再承担独占业务和特殊硬件职责。
- 在 VKE 节点池配置中删除或修改目标标签、污点。
- 等控制器完成同步。
- 再检查 Kubernetes Node 对象。
- 迁移一个低风险 Pod 验证公共池调度。
- 确认重启、扩缩容或节点替换后配置仍不会恢复。
如果原节点池的所有成员都必须共享同一配置,单台机器不应继续留在该节点池;需要将它迁移到公共节点池,或新建一个没有专属污点的节点池。
实战复盘:两个副本为什么仍在同一节点
只有 preferred node affinity 或 ScheduleAnyway 时,调度器可能会为了更好的资源评分把两个副本放到同一节点上。这属于合法行为,但无法抵御单节点故障。
在 Kubernetes 1.30 的 Deployment 中,可以按当前 ReplicaSet revision 强制分散:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
matchLabelKeys:
- pod-template-hash
labelSelector:
matchLabels:
app: <application>
version: <version>
matchLabelKeys: [pod-template-hash] 让新旧 ReplicaSet 分开计算。这样在 maxUnavailable: 0、maxSurge: 1 的滚动发布中,旧 revision 不会因为参与 skew 计算而阻塞新 revision。
Kubernetes topology spread 文档也以 pod-template-hash 作为按 Deployment revision 分散的典型示例。
强制分散前必须保证:
- 至少有两个节点同时匹配 nodeSelector/affinity。
- 两个节点都能被 Pod 的 toleration 容忍。
- 每个节点都能容纳一个副本的 request。
- 发布期间仍有容纳 surge Pod 的余量。
如果只有一个合格节点,DoNotSchedule 会让第二个 Pod 保持 Pending。这是故障域约束下的正常结果,不应通过删除 spread 规则悄悄恢复同机部署。
如何安全撤销
删除污点:
kubectl taint node data-a workload.yj/pool=data:NoSchedule-
删除标签:
kubectl label node data-a workload.yj/pool-
撤销前先修改工作负载,避免它仍要求一个已经不存在的标签。
完成定义
- 每个节点只有一个明确主池。
- 独占池同时配置标签和污点。
- 业务清单同时配置 toleration 与 nodeAffinity。
- 有状态三副本跨节点,关键服务还要跨物理故障域。
- 滚动升级时至少能容纳一个 surge Pod。
- 监控、CNI、CSI 等 DaemonSet 已验证能容忍必要污点。
- 预留节点没有被常规业务长期占用。
- 托管节点池与 Kubernetes Node 上的标签、污点保持一致。
FailedScheduling已按污点、亲和性、request 和 spread 分层解释。- 双副本使用 revision-aware topology spread 强制跨节点。
DISCUSSION
讨论与反馈