一次线上雪崩:所有Pod挤在同一台机器上
2024年3月,我们一个微服务集群在晚高峰突然崩溃。排查发现:16个Pod全部调度到了同一台32核64G的节点上,其他3台节点几乎空载。这台机器CPU飙到95%,内存耗尽,OOM killer开始屠杀进程。
根因:我们用了默认调度策略,没有配置任何亲和性或反亲和性。K8s调度器只看资源水位,不会主动分散Pod。当第一个Pod调度到node1后,后续Pod因为node1资源剩余最多,继续往node1堆。
这次事故后,我花了2周时间系统研究了K8s调度策略,特别是亲和性与反亲和性。这篇文章把实战经验全写出来,包括配置、压测、踩坑。
K8s调度策略全景
K8s v1.28调度器(kube-scheduler)的调度流程分两步:
- 过滤(Predicates):选出满足Pod资源请求的节点
- 打分(Priorities):对过滤后的节点打分,选最高分
亲和性与反亲和性在过滤阶段生效。主要分三类:
- nodeSelector:最简单的节点选择,匹配标签
- nodeAffinity:节点亲和性,支持硬约束(required)和软约束(preferred)
- podAffinity / podAntiAffinity:Pod间亲和/反亲和,控制Pod之间的分布
方案对比:3种调度策略实测
测试环境:K8s v1.28.3,4节点集群(2台4C8G,2台8C16G),部署一个Nginx服务,副本数8。
| 策略 | 配置复杂度 | Pod分布均匀度 | 调度耗时(ms) | 适用场景 |
|---|---|---|---|---|
| 默认调度 | 低 | 差(集中) | 12 | 测试环境 |
| nodeSelector | 低 | 中(按标签分组) | 15 | 固定节点部署 |
| nodeAffinity + podAntiAffinity | 中 | 高(均匀分布) | 28 | 生产环境 |
压测数据:用wrk压测30分钟,QPS 5000。默认调度下,单节点CPU 92%,其他节点<20%;使用亲和性策略后,4节点CPU均摊在45-55%。
完整代码实现
1. nodeSelector:简单粗暴
apiVersion: v1
kind: Pod
metadata:
name: nginx-selector
spec:
nodeSelector:
disktype: ssd
containers:
- name: nginx
image: nginx:1.25-alpine
resources:
requests:
cpu: "500m"
memory: "512Mi"
给节点打标签:kubectl label node node1 disktype=ssd。Pod只会调度到有ssd标签的节点。
2. nodeAffinity:更灵活的节点选择
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-affinity
spec:
replicas: 8
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node1
- node2
- node3
- node4
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
preference:
matchExpressions:
- key: disktype
operator: In
values:
- ssd
containers:
- name: nginx
image: nginx:1.25-alpine
resources:
requests:
cpu: "500m"
memory: "512Mi"
requiredDuringSchedulingIgnoredDuringExecution:硬约束,Pod必须调度到指定节点。preferredDuringSchedulingIgnoredDuringExecution:软约束,尽量调度到ssd节点,权重50。
3. podAntiAffinity:解决Pod扎堆
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-anti
spec:
replicas: 8
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx:1.25-alpine
resources:
requests:
cpu: "500m"
memory: "512Mi"
topologyKey: kubernetes.io/hostname 表示按节点维度反亲和,每个节点最多一个Pod。如果副本数超过节点数,多余的Pod会Pending。
4. 组合拳:nodeAffinity + podAntiAffinity + podAffinity
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-combo
spec:
replicas: 8
selector:
matchLabels:
app: nginx
tier: frontend
template:
metadata:
labels:
app: nginx
tier: frontend
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/worker
operator: Exists
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx
topologyKey: kubernetes.io/hostname
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: tier
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx:1.25-alpine
resources:
requests:
cpu: "500m"
memory: "512Mi"
这个配置做了三件事:
- 只调度到worker节点
- 尽量让nginx Pod分散到不同节点(软约束)
- 必须和tier=cache的Pod同节点(硬约束)
5. 用脚本验证调度结果
#!/bin/bash
# 验证Pod分布
echo "=== Pod分布情况 ==="
kubectl get pods -o wide | grep nginx-combo
echo ""
echo "=== 每个节点的Pod数 ==="
kubectl get pods -o wide | grep nginx-combo | awk '{print $7}' | sort | uniq -c | sort -rn
echo ""
echo "=== 节点资源使用 ==="
kubectl top nodes
echo ""
echo "=== 检查Pending Pod ==="
kubectl get pods | grep Pending
效果数据:压测对比
测试工具:wrk,参数:-t4 -c100 -d120s http://nginx-svc
| 策略 | 平均延迟(ms) | P99延迟(ms) | QPS | 错误率 |
|---|---|---|---|---|
| 默认调度 | 245 | 890 | 4200 | 2.3% |
| nodeSelector | 180 | 520 | 5100 | 0.5% |
| nodeAffinity + podAntiAffinity | 95 | 210 | 6800 | 0.0% |
组合策略比默认调度QPS提升62%,P99延迟降低76%。核心原因:Pod均匀分布,没有单点瓶颈。
避坑指南(我踩过的3个坑)
坑1:topologyKey用错导致调度失败
一开始我把podAntiAffinity的topologyKey写成kubernetes.io/hostname,但集群节点标签不统一。有的节点是kubernetes.io/hostname,有的是hostname。结果Pod一直Pending。
解决方法:统一节点标签。用kubectl label node --all kubernetes.io/hostname=$(hostname)修复。或者用topologyKey: kubernetes.io/hostname,这是K8s内置标签,所有节点都有。
坑2:requiredDuringScheduling + 副本数超过节点数
配置了required的podAntiAffinity,副本数8,节点数4。结果4个Pod Running,4个Pending。因为硬约束要求每个节点最多一个Pod,节点不够了。
解决方法:用preferredDuringScheduling代替required,或者增加节点。生产环境建议用preferred,允许一定程度的集中。
坑3:podAffinity导致级联调度失败
配置了podAffinity要求与cache Pod同节点。但cache Pod先被调度到node1,nginx Pod跟着去node1。node1资源不够,nginx Pod Pending。而其他节点有资源,但因为亲和性约束,不能调度。
解决方法:用preferredDuringScheduling代替required,或者给cache Pod预留资源。更推荐用topologyKey: kubernetes.io/zone做区域亲和,而不是节点级。
原理深入:调度器如何打分
kube-scheduler v1.28的调度流程:
- Filter阶段:检查Pod是否满足节点约束。亲和性规则在这里检查。如果required规则不满足,节点被过滤掉。
- Score阶段:对剩余节点打分。preferred规则在这里生效,权重越高分数越高。
打分算法:
- NodeAffinity:匹配的preferred规则权重累加
- PodAffinity:计算节点上匹配的Pod数量,乘以权重
- PodAntiAffinity:计算节点上匹配的Pod数量,反向打分(越多分越低)
最终得分 = 各策略得分之和。调度器选择最高分节点。
生产建议
- 优先用preferred(软约束),避免调度失败
- topologyKey统一用K8s内置标签:kubernetes.io/hostname、topology.kubernetes.io/zone
- 组合使用nodeAffinity + podAntiAffinity,效果最好
- 监控Pending Pod,及时调整策略
- 用descheduler定期重平衡Pod分布
总结
Pod调度策略是K8s生产化必须掌握的技能。从一次雪崩事故出发,我们对比了3种方案,给出了完整YAML配置和压测数据。组合使用nodeAffinity和podAntiAffinity,QPS提升62%,P99延迟降低76%。避坑部分记录了3个真实踩坑案例,希望你能避开。