Featured image of post K8s 资源限制与健康探针:limits / requests / livenessProbe / readinessProbe

K8s 资源限制与健康探针:limits / requests / livenessProbe / readinessProbe

为什么关闭 swap、Pod 怎么设置 CPU/内存请求与上限、活性和就绪探针的最佳实践——覆盖原理、YAML 模板、Spring Boot 应用调优与常见坑。

写于 2022-03,背景:K8s 1.23 GA,1.24 即将引入对 dockershim 移除。本文聚焦"Pod 资源调优"——关闭 swap、CPU/内存单位、requests/limits 区别、3 类探针最佳实践。

一、为什么 K8s 强制关 swap

K8s 安装文档里第一条永远是"关闭 swap",很多人不知道为什么。根本原因是 Kubelet 设计哲学

在计算集群中,我们希望 OOM 时直接杀进程,报错让运维处理,故障转移到其他节点重启。而不是用 swap 续命,导致节点 hang 住,集群性能大幅下降,运维还收不到报警。

机械盘 swap 更糟:swap 写磁盘,性能掉到个位数;机器卡死,连 SSH 都登不进去,结局就是硬重启。

类型是否需要 swap原因
计算集群(批处理、Spark、Flink)不需要OOM 立即杀,故障转移
服务型集群(MySQL、K8s、Redis)不需要性能稳定,OOM 即报警
个人开发机保留内存偶尔不够,swap 救命

swapoff -a + /etc/fstab 注释 swap 行,双保险:

1
2
3
free -m
swapoff -a
sed -ri 's/.*swap.*/#&/' /etc/fstab

iptables 不用关(K8s 自己用 iables 做 Service 转发),但 firewalld / ufw 建议关,避免端口冲突。

二、CPU / 内存单位

2.1 CPU 单位

1
2
3
4
5
6
7
resources:
  requests:
    cpu: 2000m    # 2000 毫核 = 2 核
    memory: 512Mi
  limits:
    cpu: 2000m
    memory: 512Mi

CPU 后缀

  • m:毫核(millicores),1 Core = 1000m
  • 数字(不带 m):直接写核数(cpu: 2 = 2 Core = 2000m)

CPU 是可压缩资源

  • 超过 limits → 容器被节流(throttled),不是杀掉
  • requests = 调度保证(这台节点上至少有这么多 CPU 可用)

2.2 内存单位

单位进制例子
K / M / G / T / P / E1000500M = 500 × 1000 字节
Ki / Mi / Gi / Ti / Pi / Ei1024512Mi = 512 × 1024 × 1024 字节

内存是不可压缩资源

  • 超过 limits → 容器被 OOMKilled(Exit Code 137)

三、requests vs limits

字段含义作用
requests调度的"最低保证"调度器看节点剩余资源够不够 requests
limits容器能用的"上限"CPU 节流 / 内存 OOM

Java 应用特殊坑

  • JVM 默认堆内存 = 物理内存的 1/4
  • Pod memory.limits: 1Gi 时,JVM 堆可能只分到 256MB
  • 必须显式设 -Xmx / -Xms
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
env:
- name: JAVA_OPTS
  value: "-Xms512m -Xmx512m"
resources:
  requests:
    memory: 1Gi   # 给 JVM 堆 + 堆外 + native + OS
    cpu: 500m
  limits:
    memory: 1Gi
    cpu: 1000m

四、QoS 等级

K8s 根据 requests/limits 设置给 Pod 打 3 个 QoS 等级:

QoS 等级特征驱逐顺序
Guaranteedrequests == limits(CPU 和 memory 都设了)最后被驱逐
Burstablerequests < limits中间
BestEffortrequests 和 limits 都没设最先被驱逐(节点压力时)

生产建议

  • 核心服务用 Guaranteed(不设 requests = BestEffort,节点紧张第一个杀)
  • 普通服务用 Burstable(设 requests,不设 limits 或 limits 更大)
  • 批处理可用 BestEffort(节点紧张先死批处理是合理的)

五、3 类探针

K8s 有 3 类探针,每类都有 3 种检测方式:

探针检测目标失败后果
livenessProbe容器是否存活kubelet 重启容器
readinessProbe容器是否就绪从 Service Endpoints 移除
startupProbe容器是否启动完成阻断 liveness / readiness

3 种检测方式

  • httpGet:HTTP GET 请求(最常用)
  • tcpSocket:TCP 端口
  • exec:执行命令

5.1 livenessProbe:活人检测

作用:容器是不是死了?死了 kubelet 重启它。

关键参数

  • initialDelaySeconds:第一次探测前等多久(启动慢的应用要加大)
  • periodSeconds:探测间隔
  • timeoutSeconds:单次探测超时
  • failureThreshold:连续失败几次标记失败
  • successThreshold:连续成功几次标记成功(liveness 必须是 1)
1
2
3
4
5
6
7
8
9
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: management-port
  initialDelaySeconds: 900   # 15 分钟
  periodSeconds: 30
  timeoutSeconds: 10
  successThreshold: 1
  failureThreshold: 3        # 连续失败 3 次重启

为什么 initialDelaySeconds 要大? JVM 启动慢,启动 5~10 分钟常见。如果 initialDelaySeconds = 60,JVM 还没启动完就被杀了,反复重启。

5.2 readinessProbe:就绪检测

作用:容器是不是能接流量了?没就绪就从 Service Endpoints 摘掉。

1
2
3
4
5
6
7
8
9
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: management-port
  initialDelaySeconds: 600   # 10 分钟
  periodSeconds: 20
  timeoutSeconds: 5
  successThreshold: 1
  failureThreshold: 5        # 连续失败 5 次摘流量

liveness 与 readiness 的区别

  • liveness 是"是不是活着"(死 → 重启)
  • readiness 是"是不是能干活"(不能 → 摘流量,不重启)

典型场景

  • 启动期间:readiness 失败 → 流量不来,但容器继续启动
  • 运行时下游 DB 挂:readiness 失败 → 摘流量,但容器不重启
  • 应用死锁:liveness 失败 → 重启容器

5.3 startupProbe:启动检测(K8s 1.16+)

作用:慢启动应用(如 Java)需要更长探测时间,用 startupProbe 替代 liveness 的 initialDelaySeconds。

1
2
3
4
5
6
7
8
startupProbe:
  httpGet:
    path: /actuator/health
    port: management-port
  failureThreshold: 30        # 最多失败 30 次
  periodSeconds: 10          # 每 10 秒探一次
# 总允许启动时间 = 30 * 10 = 300 秒(5 分钟)
# startupProbe 成功前,liveness 和 readiness 都不启用

好处:不用猜 initialDelaySeconds 该设多久。

5.4 Spring Boot 完整示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: springboot-app
  template:
    metadata:
      labels:
        app: springboot-app
    spec:
      containers:
      - name: app
        image: my-app:1.0
        ports:
        - containerPort: 8080
        - name: management
          containerPort: 8081
        env:
        - name: JAVA_OPTS
          value: "-Xms512m -Xmx512m -XX:+UseG1GC"
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 1000m
            memory: 1Gi
        startupProbe:
          httpGet:
            path: /actuator/health
            port: management
          failureThreshold: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: management
          periodSeconds: 20
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 5
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: management
          periodSeconds: 30
          timeoutSeconds: 10
          successThreshold: 1
          failureThreshold: 3

六、Node 资源预留

K8s 节点本身要跑 kubelet、kube-proxy、containerd 等系统进程,需要预留资源:

1
2
3
4
# kubelet 启动参数
--kube-reserved=cpu=500m,memory=1Gi
--system-reserved=cpu=500m,memory=1Gi
--eviction-hard=memory.available<500Mi,nodefs.available<10%

作用:保留资源给系统,避免 Pod 把节点搞崩。

驱逐条件(eviction-hard):

  • memory.available < 500Mi:驱逐 BestEffort → Burstable
  • nodefs.available < 10%:磁盘满了开始杀 Pod
  • imagefs.available < 15%:镜像存储满了

七、LimitRange 与 ResourceQuota

7.1 LimitRange:namespace 默认值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: LimitRange
metadata:
  name: default
  namespace: dev
spec:
  limits:
  - type: Container
    default:           # limits 默认值
      cpu: 500m
      memory: 512Mi
    defaultRequest:    # requests 默认值
      cpu: 200m
      memory: 256Mi
    max:               # 上限
      cpu: 2000m
      memory: 2Gi

不设 requests/limits 的 Pod 会用 defaultRequest 和 default 兜底。

7.2 ResourceQuota:namespace 总配额

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 20Gi
    limits.cpu: "20"
    limits.memory: 40Gi
    pods: "100"
    services: "50"
    configmaps: "50"
    secrets: "50"
    persistentvolumeclaims: "20"

超过配额就拒绝创建新资源。

八、Pod 调度与资源

8.1 资源不够 Pod 怎么调度失败

1
2
3
4
5
6
# Pod Pending 状态
kubectl describe pod <pod>
# Events:
#   Warning  FailedScheduling  ...  0/5 nodes are available:
#     3 node(s) didn't match Pod's node affinity/selector.
#     2 node(s) didn't have enough free cpu.

解决

  • 加节点
  • 减小 requests
  • HPA 自动扩

8.2 Pod 资源使用率监控

1
2
3
4
5
6
7
# 实时(依赖 metrics-server)
kubectl top pod -A --sort-by=memory | head -10
kubectl top pod -A --sort-by=cpu | head -10

# 历史(用 Prometheus)
container_cpu_usage_seconds_total
container_memory_working_set_bytes

九、常见坑

  1. Java 没设 -Xmx:JVM 堆自动算 = 物理内存 / 4,1Gi limit 时只分到 256MB,应用反复 OOM
  2. liveness initialDelaySeconds 太短:慢启动应用被反复杀
  3. liveness 检测太严格:临时 GC pause 30 秒就触发重启
  4. readiness 失败后没流量:Spring Boot /actuator/health/readiness 没启用或路径写错
  5. requests/limits 单位错500 不是 500m,是 500 核(不可能)
  6. BestEffort 服务被先杀:核心服务忘设 requests

十、生产清单

检查项推荐值
requests.requests.memoryJava 1Gi 起,Go 256Mi 起
limits.requests.memoryrequests 的 1.5~2 倍
limits.requests.cpurequests 的 1.5~2 倍(突发流量)
liveness failureThreshold3
readiness failureThreshold5
startupProbe periodSeconds × failureThreshold> JVM 启动时间
JVM 启动慢应用必加 startupProbe
节点 kube-reservedcpu=500m, memory=1Gi
节点 eviction-hardmemory.available<500Mi

十一、前置知识 / 下一步

前置

下一步

  1. Kubeadm 一键部署(2022-06-15)—— 生产部署实战
  2. K8s 集群插件(2021-09-15)—— CNI/CoreDNS/Metrics/Dashboard

参考资料

使用 Hugo 构建
主题 StackJimmy 设计