上一篇讲清了 Kubernetes 的核心对象——Pod、Deployment、Service,以及声明式 reconcile 的心智。但那里我们跳过了一个关键问题:scheduler 到底凭什么把一个 Pod 放到某台机器上? 更要命的是:你申请了多少 CPU/内存?超了会怎样?流量涨了谁来自动扩容?容器”活着”和”能干活”怎么区分?
这一篇专啃 K8s 里最影响稳定性和成本的三块:资源模型(requests/limits/QoS)、调度约束、弹性伸缩与探针。这三块配错,不是慢,而是Pod 被 OOMKilled、被 CPU throttle、发布瞬间大量报错、扩容扩不动——广告这种流量尖峰剧烈的场景尤其致命。
本文是容器化与 K8s 部署系列的第 3 篇。 全系列 5 篇:
一句话定位:requests 决定”调度到哪、保底多少”,limits 决定”最多用多少、超了怎么罚”;scheduler 按约束 Filter+Score 选节点;HPA/VPA/CA 三层弹性按指标自动伸缩;三种探针分别管”起没起来、能不能接流量、还活着吗”。 配对了才有稳定和成本双赢。
TL;DR
- requests vs limits:
requests是调度依据 + 资源保底(scheduler 按它找有足够余量的节点);limits是使用上限(cgroup 强制,超了要挨罚)。两者可以不等,差值就是”超卖空间”。 - 超限的两种罚则截然不同:内存超 limit → 直接被内核 OOMKilled(进程被杀、容器重启);CPU 超 limit → 被 throttle 限速(不杀,只是变慢、延迟飙升)。内存是硬墙,CPU 是软刹车。
- QoS 三级决定被驱逐的顺序:
Guaranteed(requests==limits,最稳,最后被赶)>Burstable(部分设置)>BestEffort(啥都没设,节点资源紧张时第一个被驱逐)。核心服务必须 Guaranteed。 - scheduler 两阶段:Filter 预选(排除资源不够、不容忍污点、硬亲和不满足的节点)→ Score 优选(给候选节点打分:资源均衡、亲和偏好、拓扑分散、镜像本地性)→ 绑定最高分节点。
- 调度约束工具箱:
nodeSelector/nodeAffinity(选哪类节点)、podAffinity/antiAffinity(和哪些 Pod 靠近/远离)、taint+toleration(节点”拒绝”+ Pod”豁免”)、topologySpreadConstraints(跨可用区/节点均匀分散)。 - 弹性三层:HPA(横向:按指标增减 Pod 数)、VPA(纵向:调单 Pod 的 requests/limits)、Cluster Autoscaler(节点级:Pod 没处放时加节点)。
- HPA 是负反馈环:
desiredReplicas = ceil(当前副本 × 当前指标/目标指标);要设稳定窗口防抖动、扩快缩慢、优先用业务指标(QPS/队列长度) 而非只看 CPU。 - 三种探针各司其职:
startupProbe(慢启动宽限期,保护应用不被误杀)、readinessProbe(能否接流量,失败只摘出 Endpoints 不重启)、livenessProbe(是否还活着,连续失败 kill 重启)。 - AdTech 血泪:竞价服务内存 limit 拍脑袋设小了,流量高峰堆内存触到
memory.max→ 批量 OOMKilled 重启 → 存活 Pod 承接更多流量 → 更快 OOM → 雪崩;同时没设合理探针导致重启风暴。
Table of contents
Open Table of contents
1. 资源模型:requests 与 limits
一切稳定性和成本问题的起点,是你在 Pod 里写的这几行:
resources:
requests: # 调度依据 + 保底
cpu: "500m" # 0.5 核
memory: "512Mi"
limits: # 使用上限(cgroup 强制)
cpu: "1000m" # 1 核
memory: "1Gi"
requests 和 limits 是两个完全不同的东西,混淆它们是新手最大的坑:
requests(申请量):告诉 scheduler”我至少需要这么多”。scheduler 只看 requests 做调度——它找一个”剩余可分配资源 ≥ 所有已调度 Pod 的 requests 之和 + 本 Pod requests”的节点。requests 也是资源的保底:这部分是给你预留的。limits(上限):告诉内核”我最多用这么多”。它落成 cgroup 的cpu.max/memory.max,超了要挨罚。
requests < limits 时,中间的差值就是超卖(overcommit)空间——节点可以在总 requests 内塞下更多 Pod,赌它们不会同时冲到 limits。超卖能提升资源利用率和密度,但赌输了(大家同时飙高)就会资源争抢甚至节点崩溃。
1.1 超限的两种罚则:内存是硬墙,CPU 是软刹车
这是必须刻进肌肉记忆的一点,因为两种资源超限的后果天差地别:
| 资源 | 超过 limit 时 | 后果 |
|---|---|---|
| 内存 | 触到 memory.max,内核 OOM Killer 直接杀进程 | 容器被 OOMKilled、重启(RestartCount 增加) |
| CPU | 被 cgroup throttle 限速(按周期掐掉超出的时间片) | 进程不死,但变慢、延迟飙升、吞吐下降 |
内存超限 = 猝死,CPU 超限 = 卡顿。 内存是不可压缩资源,没有”用少点”的中间态——要么够,要么被杀;CPU 是可压缩资源,不够就慢慢来。所以:内存 limit 要留足够 headroom(宁可略大),CPU limit 更多是防止个别 Pod 抢爆节点。这条直接对应 §6 的 war story——竞价服务就是内存 limit 拍脑袋设小了被批量 OOMKilled。
CPU throttle 特别隐蔽:应用没崩、日志没错,但 p99 延迟莫名其妙很高——一查是 container_cpu_cfs_throttled_periods 高企,limit 卡住了。很多”上云后变慢”的谜题都是 CPU throttle。
1.2 QoS 三级:节点告急时谁先被赶走
根据 requests/limits 的设置,K8s 自动给 Pod 打一个 QoS(服务质量)等级,它决定节点内存紧张时的驱逐顺序:
| QoS 等级 | 条件 | 节点告急时 |
|---|---|---|
| Guaranteed | 每个容器都设了 requests,且 requests == limits | 最稳,最后被驱逐 |
| Burstable | 设了 requests,但 requests < limits(或部分设置) | 居中 |
| BestEffort | 完全不设 requests/limits | 第一个被驱逐(节点缺资源直接干掉) |
结论很直接:核心服务(竞价、网关、检索)一律配成 Guaranteed(requests==limits),换取最强的调度保证和最后被驱逐的待遇;离线批处理、可容忍中断的任务才用 Burstable/BestEffort 去薅超卖红利。生产服务绝不能裸奔成 BestEffort——节点一紧张它就没了。
2. 调度:scheduler 怎么选节点
Pod 创建出来时是”未绑定节点”的 Pending 状态。kube-scheduler 的工作就是给它挑一个节点绑上去,这是一个两阶段过程。
调度分两步:Filter 先按硬约束把”放不下”的节点淘汰掉,Score 再给剩下的候选打分排序,绑定最高分节点。约束要在调度前就写对——调度只发生一次,绑定后不自动迁移。
2.1 Filter(预选)与 Score(优选)
- Filter 预选:把绝对放不下的节点全部排除——资源不够(余量 < Pod requests)、Pod 不容忍节点的污点、硬亲和/反亲和不满足、
nodeSelector不匹配、节点 NotReady 等。这是硬门槛,不满足就出局。 - Score 优选:对通过 Filter 的候选节点打分排序——资源是否均衡(有的策略偏向装箱紧凑省节点,有的偏向摊平避免热点)、软亲和偏好、拓扑分散、镜像本地性(节点已有镜像 → 省去拉取时间,加分)等。选总分最高的节点绑定。
关键认知:调度只在 Pod 创建时发生一次。一旦绑定,除非 Pod 被删除/驱逐/重建,它不会因为后来节点变忙而自动迁移。所以约束必须在调度前就配对——事后发现放错了,只能重建 Pod。
2.2 亲和性:affinity 与 anti-affinity
- nodeAffinity:Pod 想去/避开哪类节点(按 node label,如”要 GPU 节点""要 SSD 节点”)。分
required(硬,进 Filter)和preferred(软,进 Score 加分)。 - podAffinity / podAntiAffinity:按其他 Pod 的位置决定——
podAffinity是”我想和某些 Pod 靠近”(如和缓存 Pod 同节点降延迟);podAntiAffinity是”我要和某些 Pod 分开”(如同一个服务的多个副本别挤在一台机器上,防单点故障——这是广告核心服务的标配)。
2.3 污点与容忍:taint + toleration
taint(污点) 打在节点上,表示”这个节点默认拒绝 Pod”;toleration(容忍) 配在 Pod 上,表示”我能容忍这种污点”。只有容忍了对应污点的 Pod 才能被调度上去。典型用途:
- 专用节点池:给 GPU 节点、大内存节点打污点,只让特定 Pod(配了 toleration)进去,普通 Pod 进不来。
- 节点维护/驱逐:节点故障或要下线时,控制面自动打上
NoExecute污点,把不容忍的 Pod 赶走。
2.4 拓扑分散:topologySpreadConstraints
topologySpreadConstraints 让一批 Pod(如同一个 Deployment 的副本)尽量均匀分散到不同的拓扑域(可用区 zone、节点 node)。这样一个可用区挂了,不至于把所有副本一锅端——是跨 AZ 高可用的关键(末篇 反亲和 + PDB 会结合讲)。
topologySpreadConstraints:
- maxSkew: 1 # 各 zone 副本数最多差 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels: { app: bid-service }
3. 弹性伸缩:HPA、VPA 与 Cluster Autoscaler
广告流量有明显的日内波峰波谷、还有大促尖峰。固定副本数要么高峰不够(挂)、要么低谷浪费(贵)。K8s 的弹性分三个层次,各管一件事:
- HPA(Horizontal Pod Autoscaler):横向——按指标自动增减 Pod 数量。最常用。
- VPA(Vertical Pod Autoscaler):纵向——自动调整单个 Pod 的 requests/limits(适合不方便横向扩的有状态服务,但调整通常要重建 Pod)。
- Cluster Autoscaler(CA):节点级——当 Pod 因没有节点放得下而 Pending 时,自动加节点;节点长期空闲时缩节点。
3.1 HPA:一个负反馈控制环
HPA 本质是个负反馈控制器,周期性地把”当前指标”朝”目标指标”调:
HPA 是负反馈环:采集指标 → 对比目标算出期望副本数 → 调整 Deployment → Pod 数变化又改变指标 → 闭环收敛。关键是设稳定窗口防抖、扩快缩慢、用业务指标。
核心公式:
desiredReplicas = ceil( currentReplicas × (currentMetric / targetMetric) )
例如当前 4 个副本、目标 CPU 利用率 50%、实际 100%,则期望 = ceil(4 × 100/50) = 8 副本,扩容一倍。指标回落后再反向缩容。
3.2 HPA 的工程陷阱
HPA 配不好,弹性会自己”抖”起来,反而制造故障:
- 抖动(flapping):指标在阈值附近波动,导致反复扩缩容。K8s 用稳定窗口(
stabilizationWindowSeconds,scaleDown 默认 300s)来平滑——缩容要保守(多观察一会儿再缩,防止刚缩完流量又来)。 - 扩快缩慢:通过
behavior分别配置扩容和缩容的速率——扩容要快(高峰来了要立刻顶上),缩容要慢(避免误缩后来不及再扩)。 - 指标选型:只看 CPU 常常不准(IO 密集、等锁的服务 CPU 不高但已经过载)。优先用业务指标——QPS、请求队列长度、消费延迟(通过 custom/external metrics)——比 CPU 更能反映真实压力。
- 和就绪的配合:新扩出来的 Pod 要真正 ready 后才接流量(§4),否则 HPA 扩了一堆还没热起来的 Pod,流量打进去照样错。
- 和镜像大小的配合:镜像太大 → 拉取慢 → 扩容 Pod 迟迟不 ready,弹性形同虚设(呼应开篇 的镜像瘦身)。
4. 探针:startup、readiness、liveness
K8s 靠探针(probe)判断容器的状态。三种探针回答三个不同的问题,配错会造成”没起来就接流量”或”好好的被反复重启”。
三种探针的时序:startup 先跑(保护慢启动),成功后 readiness(管接不接流量)和 liveness(管活着没)接棒。readiness 失败只摘流量不重启,liveness 失败才 kill 重启。
| 探针 | 回答的问题 | 失败后果 | 何时用 |
|---|---|---|---|
| startupProbe | ”启动完了吗?“ | 未成功前不跑另两个探针;超过失败阈值才判定启动失败 | 启动慢的应用(JVM 预热、大缓存加载) |
| readinessProbe | ”现在能接流量吗?“ | 摘出 Endpoints、不接流量,但不重启 | 几乎所有服务都该配 |
| livenessProbe | ”还活着吗?“ | kill 容器并重启 | 检测死锁/僵死,谨慎配 |
4.1 三者的分工与协作
- startupProbe 解决”慢启动被误杀”:一个 JVM 应用可能要 60 秒才启动完,如果直接上 liveness(比如 10 秒探一次、3 次失败重启),它还没起来就被 liveness 判死、kill 重启,永远起不来。startupProbe 给一个宽限期,成功前 liveness/readiness 都不生效,成功后再交棒。
- readinessProbe 解决”没准备好就接流量”:Pod 起来了但依赖(DB 连接池、本地缓存)还没就绪时,readiness 失败 → K8s 把它摘出 Service 的 Endpoints(呼应上一篇),流量不会打进来;但不重启它——它只是”还没准备好”,不是”坏了”。就绪后自动加回 Endpoints。这也是滚动更新和优雅停机的关键(末篇详解)。
- livenessProbe 解决”僵死自愈”:进程还在但已经死锁/无响应,liveness 连续失败 → kill 重启。
4.2 探针的经典误配
- liveness 配得太激进:探测间隔太短、超时太小、阈值太低,导致服务只是偶尔慢一点就被判死重启——重启期间流量转移到其他 Pod、它们更慢、更容易被判死,形成重启风暴。liveness 要宽松(宁可慢点重启,也别误杀)。
- 该用 readiness 却用 liveness:依赖抖动(下游慢)时,如果用 liveness,会把本来只是”暂时接不了流量”的 Pod 直接重启(毫无意义,重启也连不上下游);正确做法是 readiness——摘流量、等下游恢复、自动加回。“接不了流量”用 readiness,“自己坏了”才用 liveness。
- 没配 startup 导致慢启动被误杀:JVM/大模型加载类应用必配 startupProbe。
5. 把三块拼起来:一份生产级配置骨架
资源、调度、弹性、探针合在一起,一个核心服务的 Deployment 大致长这样:
spec:
replicas: 6
template:
spec:
# 调度:副本分散到不同可用区,避免单 AZ 故障全灭
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector: { matchLabels: { app: bid-service } }
# 反亲和:同一服务副本别挤同一节点
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector: { matchLabels: { app: bid-service } }
containers:
- name: bid
# 资源:Guaranteed(requests==limits),内存留足 headroom
resources:
requests: { cpu: "1", memory: "2Gi" }
limits: { cpu: "1", memory: "2Gi" }
# 探针:startup 保护慢启动,readiness 管流量,liveness 宽松
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30 # 最多容忍 30×2s=60s 启动
periodSeconds: 2
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 6 # 宽松,避免误杀重启风暴
外面再配一个 HPA(按 QPS 或 CPU)+ Cluster Autoscaler。这套骨架就是末篇实战 的地基。
参考
- Kubernetes. Managing Resources for Containers(requests/limits/QoS):资源模型与 QoS 等级的一手权威说明。
- Kubernetes. Assigning Pods to Nodes(affinity/taint/topology spread):调度约束的完整官方参考。
- Kubernetes. Horizontal Pod Autoscaling:HPA 算法、behavior、稳定窗口的权威文档。
- Kubernetes. Liveness, Readiness and Startup Probes:三种探针的语义与配置。
- Kubernetes. Scheduler & scheduling framework:Filter/Score 两阶段调度框架的内部机制。