主题
K8s 核心概念与调度
一句话:Kubernetes 把「期望状态」写进 etcd,再让一组控制器不断把实际状态拉回期望;换来的是故障自愈和统一编排,代价是控制面组件多、每个抽象都有语义陷阱。
适用版本:Kubernetes 1.29+。默认值以官方文档为准。
为什么需要
裸容器只能解决「怎么跑一个进程」,解决不了「跑在哪台机器、挂了谁来拉、扩缩容听谁的、滚动发布怎么不中断」。这些如果靠脚本和人工,每加一种服务就要重写一遍,故障时也没有统一入口。
K8s 的做法是把运维动作抽象成资源对象:Deployment 描述副本数和镜像,Service 描述访问入口,HPA 描述扩缩规则。用户只提交期望状态,控制器负责逼近它;节点故障、Pod 崩溃、副本偏离都由控制循环处理,不需要外部监控告警再触发人工脚本。
核心机制
控制平面与节点各管一段
| 组件 | 职责 | 关键点 |
|---|---|---|
| API Server | 所有操作的统一入口 | 唯一直接读写 etcd 的组件 |
| etcd | 保存集群全部状态 | Raft 共识,丢了它集群就失忆 |
| Scheduler | 给未调度的 Pod 选节点 | 只处理 nodeName 为空的 Pod |
| Controller Manager | 运行 Deployment、ReplicaSet、HPA 等控制器 | 每个控制器只做「比较期望与实际」 |
| kubelet | 节点上的代理 | 拉起容器、执行探针、上报状态 |
| kube-proxy / CNI | Service 转发与 Pod 网络 | kube-proxy 维护 iptables/IPVS 规则 |
声明式 API:调谐而不是执行
命令式是「创建三个 Pod」,执行完就结束;声明式是「我要三个副本」,控制器持续比较实际副本数和期望副本数。少一个就补一个,多一个就删一个。这个循环是 level-triggered 的:它不关心中间发生过什么,只看当前状态和期望状态的差。
这就是为什么删掉 Deployment 的 Pod 会自动补回来,也是为什么手动 kubectl edit 改副本数会被 HPA 或 GitOps 改回去——它们都在维护自己认为的期望状态。
调度:先过滤,再打分,最后绑定
未调度 Pod → Filter(资源、污点、亲和性、端口)→ Score(资源均衡、拓扑偏好)→ Bind(写 nodeName)Filter 是布尔判断,可以把绝大多数节点直接排除;Score 只在剩下的节点上算分。kubectl describe pod 的 Events 会直接写出被哪个条件挡下,例如 0/3 nodes are available: 3 Insufficient memory。
调度只看 requests,不看 limits。节点可分配资源减去已调度 Pod 的 requests 总和,才是新 Pod 能用的空间;limits 只影响运行时的 CPU 限流和内存 OOM,不参与调度决策。requests 设得太小会让节点超卖,设得太大则浪费容量。
| 配置 | 影响调度 | 影响运行 |
|---|---|---|
requests.cpu | 是,按值预留 | 作为 CPU 分配权重 |
requests.memory | 是,按值预留 | OOM 评分参考 |
limits.cpu | 否 | 超过被 CFS 限流,延迟升高 |
limits.memory | 否 | 超过被 OOM Kill,退出码 137 |
QoS 由 requests 和 limits 的关系决定:两者相等是 Guaranteed,只设一部分是 Burstable,完全不设是 BestEffort。节点内存紧张时,BestEffort 最先被驱逐。
HPA:按指标算副本数,再用冷却窗口防抖
期望副本数 = ceil(当前副本数 × 当前指标值 / 目标指标值)例如 3 副本、CPU 80%、目标 50%,得到 ceil(4.8) = 5。HPA 默认每 15 秒同步一次(--horizontal-pod-autoscaler-sync-period,以官方文档为准),指标在目标值 ±10% 以内不动作(容忍度默认 0.1)。
冷却由 behavior 控制:扩容的 stabilizationWindowSeconds 默认 0s,缩容默认 300s(K8s 1.29+,以官方文档为准)。扩容不等待是为了尽快接住流量,但仍受 15 秒同步周期和默认扩缩策略限制;缩容等 5 分钟是为了排除流量毛刺,避免「刚缩完又来一波」。
HPA 依赖 metrics-server 提供资源指标,并且 Pod 必须设置 requests,否则 CPU 利用率没有分母,TARGETS 会长期显示 <unknown>。
实战场景
一、requests 报小了三分之一,节点内存压力把核心服务一起驱逐
现象:一次“节省成本”的容量调整把 Java 服务的 requests.memory 从 2Gi 改成 1Gi(limits.memory 仍 2Gi)。两周内出现四次“无变更但服务集体重启”:kubectl get events 里是 Evicted: The node was low on resource: memory,被驱逐的不只是自己,还有同节点的其他 BestEffort/Burstable Pod。
排查:调度只看 requests,不看 limits。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 节点超卖程度 | kubectl describe node 里 Allocated resources 的 requests 占可分配比例 | 接近或超过 100% 即超卖 |
| 谁触发了驱逐 | 节点 Conditions:MemoryPressure=True 与 kubelet 日志 evicting pod | 压力阈值触发,不是 OOM 单点 |
| 实际用量 | container_memory_working_set_bytes 与 requests 的差 | 长期超出 requests 即上报值不可信 |
| QoS 等级 | kubectl get pod -o jsonpath 看 qosClass | BestEffort 最先被驱逐 |
| 驱逐顺序 | status.phase=Failed + reason=Evicted 的时间序列 | 与 Pressure 时间点吻合 |
| limits 的真实作用 | CPU 被 CFS 限流(延迟升高)、内存超限被 OOM Kill(退出码 137) | limits 不参与调度决策 |
对症做法:
yaml
# 1. 内存 requests = limits(避免内存超卖),CPU 可以留 oversubscription
resources:
requests: { cpu: "500m", memory: "2Gi" }
limits: { cpu: "2000m", memory: "2Gi" }
# 内存不设弹性:超出 requests 而节点紧张时就会进驱逐名单;
# CPU 不同,超卖只造成限流与延迟抖动,不会直接杀进程
# 2. 核心服务用 Guaranteed(requests 与 limits 全相等)+ 高 PriorityClass
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata: { name: core-service }
value: 100000
preemptionPolicy: Never # 不允许抢占他人,只保护自己不被轻易驱逐text
3. requests 的取值来源是实测:
取过去 30 天 P99 工作集(working_set),再上浮 20%~30%(经验值,需按负载形态校准),
不是拍一个“看着够用”的数。
4. 节点保留:kubelet 的 system-reserved / kube-reserved 要覆盖系统与守护进程,
否则业务 requests 占满后,节点组件先撑不住。
5. 驱逐要有告警且按 reason 分类:Evicted 属于容量问题,
CrashLoopBackOff 属于代码/配置问题,混成一个“Pod 重启”会误导排障。复盘要点:判据是“节点 requests 分配率长期 ≤ 85%”与“每个 Pod 的内存 P99 working_set 不超过其 requests”。这两个数应该出现在容量周报里,而不是等驱逐事件。把“内存 requests=limits”写进准入策略(例如 OPA/Kyverno 规则),比培训“记得设对”可靠。
二、Pod 一直 Pending:事件里那行 0/9 nodes are available
现象:新版本发布后 Pod 卡在 Pending 十几分钟,kubectl get pods 只显示 Pending 没有更多信息,运维反复删重建。真正的原因是这套模板新加了 nodeAffinity(限定 disktype=ssd),而带该标签的 3 台节点都已被其他服务占满。
排查:调度是 Filter → Score → Bind,失败原因一定写在 Events 里。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 被哪个条件挡下 | kubectl describe pod 的 Events:0/9 nodes are available: 3 Insufficient memory, 6 node(s) didn't match Pod's node affinity/selector | 数字分解直接给结论 |
| 资源不足 | 各节点 requests 汇总 vs 可分配 | 见上一场景 |
| 污点与容忍 | 节点 NoSchedule 污点与 Pod tolerations | 缺容忍会全节点不匹配 |
| 亲和与拓扑 | nodeAffinity、topologySpreadConstraints、podAntiAffinity | 过严的组合会把可选节点压到 0 |
| PVC 绑定 | Pending 原因是否为卷未绑定(存储类拓扑) | 卷的可用区限制同样阻止调度 |
| 端口冲突 | hostPort 是否已被占用 | Filter 阶段直接排除 |
| 抢占与优先级 | PriorityClass 是否允许抢占 | 不允许则只能等容量释放 |
bash
kubectl describe pod <pending-pod> | sed -n '/Events:/,$p'
kubectl get nodes -o wide --show-labels | grep disktype
# 看节点上已占用的 requests,确认是“真不够”还是“标签不对”
kubectl describe node <node> | sed -n '/Allocated resources:/,$p'对症做法:
yaml
# 1. 亲和性用“偏好”而不是“硬性要求”,给调度器退路
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference: { matchExpressions: [ { key: disktype, operator: In, values: [ssd] } ] }
# 必须硬约束时用 requiredDuringScheduling...,但要保证满足的节点数 ≥ 副本数 + 1
# 2. 容量真不足时:扩节点、降低 requests、或允许抢占(谨慎)
# 3. 打散用拓扑约束,而不是把节点写死:
topologySpreadConstraints:
- { maxSkew: 1, topologyKey: topology.kubernetes.io/zone,
whenUnsatisfiable: ScheduleAnyway, labelSelector: { matchLabels: { app: order-service } } }text
4. 上线前用 dry-run 评估可调度性:
临时把副本数调大做一次“容量预演”,或用 scheduler 的 metrics 看打分分布。
5. 别用“删了重建”解决 Pending:Pending 是调度约束问题,重建只会重新排队。复盘要点:判据是“Pending 超过 5 分钟的 Pod 数量”为告警项,并且值班手册要求先看 Events 那一行而不是先删 Pod。发布流水线里加一条“容量与标签校验”:模板新增硬亲和/污点容忍变更时必须给出满足条件的节点数,否则不允许合并。
三、手动 kubectl edit 改副本数,十分钟后被改回去
现象:故障处理时运维手动把副本数从 3 调到 10 顶流量,几分钟后自动回到 3,流量再次超时。同时 GitOps 工具持续报“集群状态与仓库不一致”,开始反复覆盖。另一类冲突是有人给 Deployment 加了 HPA 后又手工设 replicas,两边互相覆盖。
排查:声明式 API 是调谐,不是执行;多个写入方会互相覆盖。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 谁在写这个字段 | kubectl get deploy -o jsonpath='{.metadata.managedFields}' 看 manager 与 operation | 出现多个 manager 即多写入方 |
| 期望值来源 | GitOps 仓库里的 replicas、HPA 的 minReplicas | 控制器会持续拉回自己的期望 |
| HPA 与 Deployment 冲突 | HPA 存在时手工设 replicas 会被覆盖 | HPA 管副本数,Deployment 不该再管 |
| 调谐循环类型 | level-triggered:只看当前与期望之差 | 中间手动改动作废是预期行为 |
| 时间线 | 变更记录与“被改回”时间差 | 与同步周期一致即确认 |
删掉 Deployment 的 Pod 会自动补回来、手动改副本数会被改回去,是同一个机制的两个表现。
对症做法:
yaml
# 1. 容量调整走唯一入口:改 GitOps 仓库或 HPA 参数,不改线上对象
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: order-service }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: order-service }
minReplicas: 6
maxReplicas: 30
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }yaml
# 2. 有 HPA 时 Deployment 不要固定 replicas(交给 HPA)
# GitOps 侧同理:仓库里不写死副本数,避免同步时把弹性结果覆盖回去text
3. 紧急扩容的正确做法:
改 HPA 的 minReplicas 或仓库里的期望值并同步;
如果必须现场应急,同步冻结 GitOps 的 reconcile,事后回写仓库,
否则“应急操作被自动回滚”会造成二次事故。
4. 权限分层:生产集群对 Deployment 的 edit 权限收紧,
扩容走专用工具(改 HPA/改仓库),避免随手改线上状态。
5. 每个字段只能有一个权威写入方:副本数归 HPA,
镜像归发布流水线,资源配额归平台基线。复盘要点:判据是“线上关键对象的 managedFields 里只有一个业务 manager”,可以定期扫描发现“多个控制器抢同一字段”。应急流程要写清“先冻结同步,事后回写仓库”,这是 GitOps 环境里最容易被忽略的一步;HPA 的行为边界与弹性策略见 容器平台弹性、任务与迁移。
最小上手
bash
# Deployment 必须带 requests,HPA 才有计算基准
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: hpa-demo
spec:
replicas: 2
selector:
matchLabels: {app: hpa-demo}
template:
metadata:
labels: {app: hpa-demo}
spec:
containers:
- name: nginx
image: nginx:1.27
resources:
requests: {cpu: 100m, memory: 64Mi}
limits: {cpu: 500m, memory: 128Mi}
EOF
kubectl autoscale deployment hpa-demo --cpu-percent=50 --min=2 --max=5
kubectl get hpa hpa-demo -w
# TARGETS 从 <unknown>/50% 变成实际值后,REPLICAS 才会随负载变化验证调度失败的原因:
bash
kubectl run sched-fail --image=nginx:1.27 --overrides='
{"spec":{"nodeSelector":{"disktype":"nonexistent"}}}'
kubectl describe pod sched-fail | grep -A3 Events
# → 0/N nodes are available: ... didn't match Pod's node affinity/selector
kubectl delete pod sched-fail生产动作
- 所有 Pod 设 requests:否则调度器按 0 计算,节点会被超卖到 OOM;HPA 也无法工作。
- 核心服务 requests = limits:拿到 Guaranteed,节点内存紧张时最后被驱逐;代价是失去突发能力。
- 扩缩容参数:
scaleUp.stabilizationWindowSeconds默认 0s,scaleDown.stabilizationWindowSeconds默认 300s(以官方文档为准);不要为了「更快」把缩容窗口调成 0。 - 排障命令:
kubectl describe pod看 Events;kubectl get pods -o wide看落到哪个节点;kubectl describe node看 Allocated resources;kubectl top pods看实际用量(需要 metrics-server)。 - 节点隔离:用 taint 标记专用节点,用
nodeAffinity做硬约束,用topologySpreadConstraints打散副本;只写nodeSelector容易在扩容后失效。
边界
- 调度器不迁移运行中的 Pod:节点资源变紧张后,已运行的 Pod 不会自动搬走;要靠驱逐、descheduler 或重建。
- HPA 不适合秒级尖刺:指标采集和同步有延迟,突发流量先靠足够的 minReplicas 和入口限流兜住。
- HPA 不能缩到 0:需要缩容到零用 KEDA 一类方案;有状态服务也不适合直接套 HPA。
- 节点满了 HPA 也扩不出来:Pod 会 Pending,需要 Cluster Autoscaler 或预留容量。
- 声明式不是自动正确:控制器只保证你写的期望状态,selector 写错、探针配错,它照样忠实执行错误配置。
讲清楚
K8s 的核心是一个控制循环:用户把期望状态写进 API Server,etcd 保存,控制器不断比较实际和期望并修正。声明式的价值不是「写法高级」,而是把「检测偏离 → 修复」内置到系统里,不再依赖外部告警和人工脚本。
调度分三步:Filter 用硬条件排除节点,Score 在剩余节点里打分,Bind 写入 nodeName。调度只看 requests,limits 不参与,所以 requests 设小了节点会被超卖。
HPA 用「当前副本 × 当前指标 / 目标指标」算副本数,扩容冷却默认 0s,缩容冷却默认 300s。它依赖 metrics-server 和 requests,两个缺一个,TARGETS 就会是 unknown。