跳到正文

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 / CNIService 转发与 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 看 qosClassBestEffort 最先被驱逐
驱逐顺序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。

把一门技术讲透到既懂也会用