三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

【Kubernetes从入门到精通】第30篇:QoS——K8s的“三六九等“资源优先级

【Kubernetes从入门到精通】第30篇:QoS——K8s的“三六九等“资源优先级

上一篇【第29篇】污点和容忍——K8s的“拒之门外“机制
下一篇【第31篇】LimitRange——给你的Namespace画个"圈"


摘要

上篇咱们聊了资源请求(requests)和限制(limits)怎么配,但你有没有想过一个问题:K8s集群资源紧张的时候,杀谁不杀谁?不是随机砍的——K8s有一套严格的"等级制度"叫QoS(Quality of Service),把Pod分成三等:Guaranteed(皇亲国戚,requests=limits全设且相等)、Burstable(中产阶级,设了requests但对不上limits)、BestEffort(底层打工人,啥都没设)。

等级不同,待遇天差地别——OOM Score从最低的-998(Guaranteed,基本不死)到最高的1000(BestEffort,首选开刀),驱逐顺序也是从BestEffort开始一层层清退。本文就把这三六九等的规则掰碎,告诉你QoS等级怎么判定、OOM Score怎么打分、生产环境怎么用Guaranteed保护核心服务——让你的金牌Pod永远不会被误杀。


一、三种QoS等级怎么判定——“你是不是亲生的”

1.1 判定规则——一张流程图搞定

【QoS 等级判定流程——"你是哪一级?"】 开始 │ ▼ ┌─────────────────────────────┐ │ 每个容器都设置了 │ │ requests 和 limits? │──No──┐ └─────────────┬───────────────┘ │ │ Yes │ ▼ ▼ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ 每个容器都 │ │ 至少有一个容器没设 │ │ requests.cpu == limits.cpu │ │ requests 或 limits │ │ AND │ │ │ │ requests.mem == limits.mem?│ │ → BestEffort(底层打工人) │ │ │ │ 谁都可以压缩、最先被驱逐 │ └─────────────┬───────────────┘ └─────────────────────────────┘ │ ┌────────┴────────┐ │ Yes │ No ▼ ▼ ┌───────────┐ ┌───────────┐ │Guaranteed│ │ Burstable │ │(皇亲国戚)│ │(中产阶级)│ │requests= │ │设了requests│ │limits │ │但不等limits│ └───────────┘ └───────────┘

1.2 三个等级的YAML实例

# ==========================================# 等级1:Guaranteed("VIP")# ==========================================# 条件:所有容器的 requests == limits(CPU和内存都要相等)apiVersion:v1kind:Podmetadata:name:guaranteed-podspec:containers:-name:appimage:nginxresources:requests:cpu:"500m"# ← 相等!memory:"512Mi"# ← 相等!limits:cpu:"500m"# ← 相等!memory:"512Mi"# ← 相等!# ✓ 单容器,requests=limits → Guaranteed---# ==========================================# 等级2:Burstable("中产阶级")——最常见# ==========================================# 条件:至少一个容器设了requests或limits,但不满足GuaranteedapiVersion:v1kind:Podmetadata:name:burstable-podspec:containers:-name:appimage:nginxresources:requests:cpu:"200m"# 设了requestsmemory:"256Mi"limits:cpu:"1000m"# limits和requests不相等!memory:"512Mi"# limits和requests不相等!# ✓ 设了requests但不等limits → Burstable# 这也是最常见的配置——大部分生产Pod都是Burstable---# ==========================================# 等级3:BestEffort("底层打工人")# ==========================================# 条件:没有任何容器设置requests或limitsapiVersion:v1kind:Podmetadata:name:besteffort-podspec:containers:-name:appimage:nginx# 没有 resources 字段!# ✗ 没设任何资源 → BestEffort# 这个Pod在资源紧张时是第一个被驱逐的

1.3 容易搞错的判定细节

【QoS判定中的"陷阱"】 场景1:多容器Pod——一个容器满足Guaranteed不算数! ┌─────────────────────────────────────────────────┐ │ containers: │ │ - name: app │ │ resources: │ │ requests: {cpu:500m, mem:512Mi} │ │ limits: {cpu:500m, mem:512Mi} ← Guaranteed条件 │ │ - name: sidecar │ │ resources: │ │ requests: {cpu:100m, mem:128Mi} │ │ limits: {cpu:200m, mem:256Mi} ← 不等! │ │ │ │ 判定:Burstable(不是Guaranteed!) │ │ 原因:sidecar的requests≠limits,拖了后腿 │ └─────────────────────────────────────────────────┘ 场景2:只设了limits没设requests ┌─────────────────────────────────────────────────┐ │ containers: │ │ - name: app │ │ resources: │ │ limits: │ │ cpu: "500m" │ │ memory: "512Mi" │ │ # 没设requests! │ │ │ │ 判定:Burstable │ │ 原因:K8s自动把requests=limits(至少有一个 │ │ 容器设了requests或limits就算Burstable) │ │ 注意:自动补的requests和limits是相等的 │ │ 但QoS判定只看你显式设的 │ └─────────────────────────────────────────────────┘ 场景3:只设requests不设limits ┌─────────────────────────────────────────────────┐ │ containers: │ │ - name: app │ │ resources: │ │ requests: │ │ cpu: "200m" │ │ memory: "256Mi" │ │ # 没设limits │ │ │ │ 判定:Burstable │ │ 原因:至少一个资源(requests)设了 │ │ 效果:可以用到Node上所有剩余资源 │ │ 但OOM时不会像Guaranteed那样被保护 │ └─────────────────────────────────────────────────┘
配置情况QoS等级特征
所有容器 requests=limits(CPU和内存都等)Guaranteed最高保护级别
至少一个容器设了requests或limits但不满足GuaranteedBurstable最常见的等级
所有容器都没设requests和limitsBestEffort最低保护级别

要点:QoS是Pod级别的——哪怕你有一个容器配得完美满足Guaranteed,只要另一个容器拉了后腿,整个Pod就降级。这也是为什么Istio/Envoy这类Sidecar注入要特别小心——它给你的Pod加了个没设资源的Sidecar容器,直接把你的Guaranteed拉成了BestEffort!


二、OOM Score——“你的生存分是多少”

2.1 三级QoS的OOM Score差异

【OOM Score 计分——Linux内核的"生死簿"】 OOM Score 计算公式(简化): ┌─────────────────────────────────────────────────────────┐ │ │ │ oom_score = (进程内存占用 / 系统总内存) × 1000 │ │ + oom_score_adj │ │ │ │ K8s设置的 oom_score_adj: │ │ │ │ Guaranteed Pod: │ │ ┌──────────────────────────────────────────────────┐ │ │ │ oom_score_adj = -998 │ │ │ │ (即使node OOM,基本也不会被杀,除非整个内存炸了) │ │ │ │ oom_score 范围:-998 ~ -900 │ │ │ │ 生存概率:★★★★★ 接近100% │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ Burstable Pod: │ │ ┌──────────────────────────────────────────────────┐ │ │ │ oom_score_adj = min(max(2, │ │ │ │ 1000 - 1000 × (request/limit) ), 999) │ │ │ │ │ │ │ │ 例:mem request=256Mi, limit=512Mi │ │ │ │ score_adj = 1000 - 1000×0.5 = 500 │ │ │ │ oom_score 范围:500 ~ 1500 │ │ │ │ 生存概率:★★★☆☆ 中等 │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ BestEffort Pod: │ │ ┌──────────────────────────────────────────────────┐ │ │ │ oom_score_adj = 1000 │ │ │ │ oom_score 范围:1000 ~ 2000 │ │ │ │ 生存概率:★☆☆☆☆ 随时可能被砍 │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘
# 查看Pod的OOM Score# 方法1:进入Node查看进程的oom_score_adjkubectl get pod guaranteed-pod-owide# NODE: worker-1sshworker-1# 找到容器进程PIDcat/proc/$(dockerinspect-f'{{.State.Pid}}'<container_id>)/oom_score_adj# -998 ← Guaranteed Pod# 500 ← Burstable Pod# 1000 ← BestEffort Pod# 方法2:用kubectl describe查看QoS等级kubectl describe pod guaranteed-pod|grep"QoS Class"# QoS Class: Guaranteedkubectl describe pod burstable-pod|grep"QoS Class"# QoS Class: Burstablekubectl describe pod besteffort-pod|grep"QoS Class"# QoS Class: BestEffort

2.2 为什么BestEffort第一个被杀

【驱逐链路——资源紧张时的"选择性牺牲"】 时刻1:Node内存开始紧张 ┌─────────────────────────────────────────────────────────┐ │ Node-1: 8Gi 内存 │ │ ┌────────────────────────────────────────────────┐ │ │ │ ████████████████████████████░░░░░░░░░░░░░░░░░░│ │ │ │ 已用 6.5Gi 剩余 1.5Gi │ │ │ └────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ 时刻2:内存持续增长,触发Eviction阈值 ┌─────────────────────────────────────────────────────────┐ │ Node-1: 已用 7.2Gi (90%),超过 eviction-hard 阈值 │ │ │ │ kubelet 驱逐决策: │ │ ┌────────────────────────────────────────────┐ │ │ │ 第1波驱逐:所有 BestEffort Pod → 直接杀掉 │ │ │ │ 第2波驱逐:Burstable中超出request最多的Pod │ │ │ │ 第3波驱逐:剩余Burstable Pod │ │ │ │ 第4波驱逐:(理论上)Guaranteed Pod │ │ │ │ (实际上系统和kubelet会死保它们) │ │ │ └────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ 时刻3:内存恢复安全水位 ┌─────────────────────────────────────────────────────────┐ │ Node-1: 已用 5.8Gi (72%) │ │ Scheduler 在别的Node上重建被驱逐的Pod │ └─────────────────────────────────────────────────────────┘

要点:驱逐是"排队枪毙"式的——BestEffort打头阵,Burstable按超出request的比例排第二梯队,Guaranteed在最后面。注意kubelet驱逐的是整个Pod(不是单个容器),按Pod级别的资源使用量排序。即使你的BackEffort Pod只用了10Mi内存,只要Node内存紧张,它也会被优先驱逐。


三、驱逐机制详解——“kubelet的水位线”

3.1 kubelet的驱逐阈值

【Eviction 阈值——kubelet 的"警戒水位线"】 ┌─────────────────────────────────────────────────────────┐ │ Node 内存状态 │ │ │ │ 100% ████████████████████████████████████████████████ │ │ │ │ │ 95% ├── eviction-hard 阈值(内存 < 100Mi → 开始驱逐) │ │ │ ┌───────────────────────────────────────┐ │ │ │ │ 触发条件(默认): │ │ │ │ │ • memory.available < 100Mi │ │ │ │ │ • nodefs.available < 10% │ │ │ │ │ • imagefs.available < 15% │ │ │ │ └───────────────────────────────────────┘ │ │ 85% ├── eviction-soft 阈值(默认不启用) │ │ │ │ │ 70% ├── 安全水位——正常运行 │ │ │ │ │ 50% │ │ │ │ │ │ 0% └─────────────────────────────────────────────────│ └─────────────────────────────────────────────────────────┘
# 查看kubelet的驱逐配置kubectl describenodeworker-1|grep-A10"Conditions:"# 或者直接看kubelet配置cat/var/lib/kubelet/config.yaml|grep-A10eviction# evictionHard:# memory.available: "100Mi"# nodefs.available: "10%"# nodefs.inodesFree: "5%"# imagefs.available: "15%"
# 自定义kubelet驱逐配置(kubelet配置文件)apiVersion:kubelet.config.k8s.io/v1beta1kind:KubeletConfigurationevictionHard:memory.available:"200Mi"# 提高到200Mi——更保守nodefs.available:"10%"imagefs.available:"15%"evictionSoft:memory.available:"500Mi"# 软阈值:到达500Mi时evictionSoftGracePeriod:memory.available:"60s"# 持续60秒后才触发驱逐evictionMaxPodGracePeriod:120# 驱逐时最长优雅关闭时间

3.2 驱逐优先级排序——“先杀谁”

【Eviction 排序算法——"排好队,一个个来"】 排序因子: ┌─────────────────────────────────────────────────────────┐ │ 1. QoS等级(权重最大) │ │ BestEffort > Burstable > Guaranteed │ │ │ │ 2. 同一QoS内按"超出部分占比"排序 │ │ (Pod实际使用量 - Pod request) / Pod实际使用量 │ │ 这个比例越大的Pod越先被驱逐 │ │ (说明它"多占了"更多) │ │ │ │ 3. Priority(优先级) │ │ 低优先级的Pod先驱逐 │ └─────────────────────────────────────────────────────────┘ 举例——3个Burstable Pod的驱逐顺序: ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │ Pod │ Request │ Usage │ 超出量 │ 超出比例 │ 驱逐顺序 │ ├──────────┼──────────┼──────────┼──────────┼──────────┤ │ Burst-A │ 256Mi │ 800Mi │ 544Mi │ 68% │ 第1个 │ │ Burst-B │ 512Mi │ 900Mi │ 388Mi │ 43% │ 第2个 │ │ Burst-C │ 512Mi │ 600Mi │ 88Mi │ 15% │ 第3个 │ └──────────┴──────────┴──────────┴──────────┴──────────┘

3.3 驱逐过程——Pod是怎么被"请走"的

# 查看Pod被驱逐的原因kubectl describe pod evicted-pod# Status: Failed# Reason: Evicted# Message: The node was low on resource: memory.# Threshold quantity: 100Mi, available: 80Mi# 被驱逐的Pod的状态kubectl get pod evicted-pod# NAME READY STATUS RESTARTS AGE# evicted-pod 0/1 Evicted 0 5m# 被驱逐的Pod会在其他Node上重建(如果由Deployment管理)kubectl get pod-lapp=my-app# NAME READY STATUS NODE# my-app-new-001 1/1 Running worker-2 ← 被驱逐了但在别的Node重建了

要点:驱逐不是"杀掉再原地重启"——是被驱逐的Pod从当前Node强行移除,由Scheduler在别的Node上重新调度一个新的Pod。这就是为什么驱逐期间会有短暂的请求中断——旧Pod被驱了新Pod还没Ready。如果你的业务对可用性要求极高,保证至少3个副本 + 用Guaranteed QoS + 配好PodDisruptionBudget。


四、生产环境QoS最佳实践

4.1 QoS等级选择策略

【按服务重要性选择QoS等级】 Tier 1:核心业务(支付、订单、用户登录) ┌─────────────────────────────────────────────────┐ │ QoS: Guaranteed │ │ requests = limits(相等) │ │ 原因:绝不能因为资源紧张被杀,宁可少部署几个 │ │ 代价:资源预留较多,弹性空间小 │ │ 适合:对稳定性要求极高的核心服务 │ └─────────────────────────────────────────────────┘ Tier 2:普通业务(API服务、后台任务、前端页面) ┌─────────────────────────────────────────────────┐ │ QoS: Burstable │ │ requests < limits(不等) │ │ 原因:平时用很少,高峰可以多申请,有一定保护 │ │ 代价:可能被驱逐但概率较低 │ │ 适合:大部分Web服务 │ └─────────────────────────────────────────────────┘ Tier 3:可牺牲任务(批处理、调试Pod、临时测试) ┌─────────────────────────────────────────────────┐ │ QoS: BestEffort │ │ 不设requests和limits │ │ 原因:用完就扔的任务,被杀也不心疼 │ │ 适合:CI/CD任务、临时调试、一次性脚本 │ └─────────────────────────────────────────────────┘

4.2 实战:给核心服务套上Guaranteed金钟罩

# 核心支付服务——Guaranteed QoSapiVersion:apps/v1kind:Deploymentmetadata:name:payment-servicespec:replicas:3selector:matchLabels:app:paymenttemplate:metadata:labels:app:paymentspec:# 高优先级——配合QoS保护priorityClassName:high-priority# Pod反亲和性——分散到不同Nodeaffinity:podAntiAffinity:requiredDuringSchedulingIgnoredDuringExecution:-labelSelector:matchExpressions:-key:appoperator:Invalues:-paymenttopologyKey:kubernetes.io/hostnamecontainers:-name:paymentimage:payment:v3.2resources:requests:cpu:"2000m"# ← 相等 → Guaranteed!memory:"4Gi"# ← 相等 → Guaranteed!limits:cpu:"2000m"# ← 相等memory:"4Gi"# ← 相等# 结果:QoS = Guaranteed, OOM Score = -998# → 除非整个Node的内存都被吃光了,否则这个Pod不会死
# 普通Web服务——Burstable QoS(最常见的配置)apiVersion:apps/v1kind:Deploymentmetadata:name:web-frontendspec:replicas:5selector:matchLabels:app:webtemplate:metadata:labels:app:webspec:containers:-name:nginximage:nginx:1.25resources:requests:cpu:"200m"# 保证200mmemory:"256Mi"# 保证256Milimits:cpu:"1000m"# 最多1000m(不等于!)memory:"512Mi"# 最多512Mi(不等于!)# 结果:QoS = Burstable# 好处:平时用很少省资源,高峰可以爆发到limit

4.3 关于Sidecar容器的QoS陷阱——“队友拖后腿”

# 场景:你给应用配了完美的Guaranteed# 但Istio自动注入了一个Sidecar——QoS被拖累!apiVersion:v1kind:Podmetadata:name:app-with-sidecarannotations:sidecar.istio.io/inject:"true"# Istio自动注入spec:containers:-name:appimage:my-app:v1resources:requests:cpu:"500m"memory:"512Mi"limits:cpu:"500m"# ← 完美Guaranteedmemory:"512Mi"# Istio自动注入的Sidecar容器——拖后腿!-name:istio-proxy# ← 这个容器是自动加的image:istio/proxyv2# 注意:这个Sidecar可能没设resources或requests≠limits# → 整个Pod的QoS从Guaranteed降级为Burstable!# 解决方案:给Sidecar也配好resources---apiVersion:v1kind:Podmetadata:name:app-with-sidecar-fixedannotations:# Istio配置——给Sidecar设资源sidecar.istio.io/proxyCPU:"100m"sidecar.istio.io/proxyCPULimit:"100m"# ← 相等!sidecar.istio.io/proxyMemory:"128Mi"sidecar.istio.io/proxyMemoryLimit:"128Mi"# ← 相等!spec:containers:-name:appimage:my-app:v1resources:requests:{cpu:"500m",memory:"512Mi"}limits:{cpu:"500m",memory:"512Mi"}# 现在app和istio-proxy都是requests=limits → Global QoS = Guaranteed!

要点:Service Mesh(Istio/Linkerd)的Sidecar注入是Guaranteed QoS的隐形杀手——你辛辛苦苦配好Guaranteed,结果Sidecar一来全给你拉成Burstable。解决方案:(1) 给Sidecar也配requests=limits;(2) 或者接受Burstable但至少保证Sidecar有足够的requests。


本篇小结

QoS是K8s资源管理的"等级制度",决定了资源紧张时谁先被牺牲:

  1. 三种等级判断:Guaranteed(所有容器requests=limits)、Burstable(有requests但不等于limits)、BestEffort(啥都没设)
  2. OOM Score是天差地别:Guaranteed是-998(接近免死),Burstable在0-999之间,BestEffort是1000(首选开刀)
  3. 驱逐是排队枪毙:BestEffort先死→Burstable按超量比例排→Guaranteed最后(基本不死)
  4. 核心服务用Guaranteed——虽然多占点资源但换来的是OOM保护,很值
  5. Sidecar是QoS杀手——Istio/Envoy注入后如果没配resources,会把你的Guaranteed拖成Burstable甚至BestEffort

QoS是Pod级别的保护,但如果你管理着一个多团队共享的集群,光靠QoS不够——还得用LimitRange给每个Namespace画个"圈",强制约束Pod的资源声明。下一篇咱们聊LimitRange——给你的Namespace立规矩。


上一篇【第29篇】污点和容忍——K8s的“拒之门外“机制
下一篇【第31篇】LimitRange——给你的Namespace画个"圈"


← 返回列表