【K8s运维实战】Pod 频繁重启?ImagePullBackOff 和 OOMKilled 的排查三板斧,照做就有效

先说核心:Pod是什么

Pod是Kubernetes中最小的可部署单元,你可以把它理解成"一个或多个容器的组合",它们共享网络、存储和运行环境。实际生产中最常见的场景是单容器Pod------一个Pod里只跑一个容器,占了Kubernetes Pod使用场景的主流。

Pod的API版本是v1,Kind是Pod。定义Pod的YAML有四个顶级字段:apiVersionkindmetadataspec。其中spec是Pod的核心规格说明,containers字段是必需的,Pod中至少得有一个容器。

⚠️ 注意:Pod创建后,spec.containers无法更新。想改容器配置?删了重建。


最小的Pod:单容器配置

先上一个最干净的单容器Pod YAML,跑的是nginx(这里用了轻量化的alpine镜像,生产环境请根据安全策略选择具体版本):

复制代码
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
    tier: frontend
spec:
  containers:
  - name: nginx
    image: nginx:1.27-alpine

把它存成pod.yaml,然后用kubectl apply创建:

复制代码
kubectl apply -f pod.yaml
# 预期输出:pod/nginx created

创建完可以用kubectl get pods看一眼状态:

复制代码
kubectl get pods
# 预期输出:
# NAME    READY   STATUS    RESTARTS   AGE
# nginx   1/1     Running   0          40s

READY 1/1表示Pod里有一个容器,且该容器处于就绪状态。


Init Container:主容器启动前的"守门员"

先讲Init Container,是因为它在生产环境中的出场率极高,而且往往决定主容器能否正常启动。

Init Container在主容器之前 运行,并且必须全部成功退出 后,主容器才会被启动。如果某个Init Container失败了,Pod会一直重启它(取决于restartPolicy),直到成功为止。

典型应用场景:等待数据库/中间件就绪、初始化数据表结构、下载依赖文件、修改挂载目录权限。

看一个实战例子------等待MySQL完全启动后再跑主程序:

复制代码
apiVersion: v1
kind: Pod
metadata:
  name: app-with-init
spec:
  initContainers:
  - name: wait-for-mysql
    image: busybox:1.36
    command: ['sh', '-c', 
      'until nslookup mysql-service; do echo waiting for mysql; sleep 2; done;']
  containers:
  - name: main-app
    image: myapp:latest

注意 :Init Container的镜像建议优先用busyboxalpine这种轻量级工具镜像,别把重量级应用塞进去,否则会拖长启动时间。


怎么看Pod和容器的状态

kubectl get pods只能看个大概,真正排查问题要靠kubectl describe pod

复制代码
kubectl describe pod nginx

这个命令会输出一大堆信息,我一般重点关注这几个部分:

  • Node:Pod被调度到了哪个节点
  • IP地址:Pod的内网IP
  • Events:最底部的Event列表,所有调度、拉镜像、启动失败的日志都在这

彩蛋 :80%的部署问题靠kubectl describe podkubectl logs --previous就能搞定。

想看容器日志用这个:

复制代码
kubectl logs <pod-name>
# 如果容器重启过,看上一次的日志
kubectl logs <pod-name> --previous

自定义容器启动命令和参数

容器镜像本身有ENTRYPOINTCMD,Kubernetes允许你用commandargs覆盖它们。

对应关系是:

  • command → 覆盖镜像的ENTRYPOINT
  • args → 覆盖镜像的CMD

来个例子,跑一个debian容器并执行printenv HOSTNAME KUBERNETES_PORT

复制代码
apiVersion: v1
kind: Pod
metadata:
  name: command-demo
spec:
  containers:
  - name: command-demo-container
    image: debian:12-slim
    command: ["printenv"]
    args: ["HOSTNAME", "KUBERNETES_PORT"]
  restartPolicy: OnFailure

[终审微调] 由于restartPolicy: OnFailure且容器以exit 0正常退出,Pod的生命周期阶段(Phase)会变为Succeeded(在kubectl get pods的STATUS列中通常显示为Completed)。想看输出请立即执行kubectl logs command-demo,否则集群可能会在回收策略触发时清理掉该Pod。

复制代码
kubectl logs command-demo
# 预期输出类似:
# command-demo
# tcp://10.3.240.1:443

我踩过的坑:如果想把命令放到shell里执行(比如要管道操作),这样写:

复制代码
command: ["/bin/sh"]
args: ["-c", "while true; do echo hello; sleep 10; done"]

容器环境变量:配置注入的四种方式

环境变量在Pod启动时设定,不会动态更新。如果ConfigMap变了,需要重启Pod才能生效。

YAML里用env字段定义环境变量,支持四种来源:

1. 直接写死(Literal)

复制代码
env:
- name: APP_ENV
  value: "production"

2. 从ConfigMap取

复制代码
env:
- name: DB_HOST
  valueFrom:
    configMapKeyRef:
      name: app-config
      key: database_host

3. 从Secret取(敏感信息)

复制代码
env:
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-secret
      key: password

4. 从Downward API取(Pod自身信息)

复制代码
env:
- name: POD_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.name
- name: POD_NAMESPACE
  valueFrom:
    fieldRef:
      fieldPath: metadata.namespace
- name: POD_IP
  valueFrom:
    fieldRef:
      fieldPath: status.podIP
- name: MEMORY_LIMIT
  valueFrom:
    resourceFieldRef:
      containerName: app
      resource: limits.memory

如果想把ConfigMap或Secret里的所有键 都变成环境变量,用envFrom注意 optional必须缩进在 configMapRef secretRef内部

复制代码
envFrom:
- configMapRef:
    name: app-config
    optional: true
- secretRef:
    name: app-secrets
    optional: true

注意:envenvFrom同时存在时,env的优先级更高,相同key会覆盖envFrom的值。


健康探针:保命配置(必配)

很多初学者只定义了容器镜像就跑上了生产,结果流量一上来就频繁重启。探针是生产环境的强制性标准配置,没有之一。

Kubernetes提供了三种探针:

  • livenessProbe(存活探针):检测容器是否还活着,失败则重启容器。
  • readinessProbe(就绪探针):检测容器是否准备好接收流量,失败则从Service摘除。
  • startupProbe(启动探针):用于保护启动慢的容器,成功后其他探针才会介入(1.28+已是稳定功能)。

下面是一份同时配置了livenessreadiness的YAML示例,直接抄作业就行:

复制代码
apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  containers:
  - name: app
    image: nginx:1.27-alpine
    ports:
    - containerPort: 80
    # 存活探针:检查 /healthz,初始等待30秒(给足启动时间),每10秒检查一次
    livenessProbe:
      httpGet:
        path: /healthz
        port: 80
      initialDelaySeconds: 30
      periodSeconds: 10
    # 就绪探针:检查 /ready,初始等待5秒,每5秒检查一次
    readinessProbe:
      httpGet:
        path: /ready
        port: 80
      initialDelaySeconds: 5
      periodSeconds: 5

如果你的应用没有HTTP接口,可以用tcpSocket检查端口是否监听,或用exec执行脚本命令。


Pod的一生:创建与删除过程

生命周期阶段

Pod从创建到结束会经历这几个阶段:

|---------------|-----------------------------------|
| 阶段 | 说明 |
| Pending | 已提交给API,但还没被调度到节点,或者还在拉镜像、跑Init容器 |
| Running | 至少一个容器在运行,探针开始工作 |
| Succeeded | 所有容器正常退出(exit 0),一般用于Job类Pod |
| Failed | 至少一个容器异常退出(非0)或被系统杀掉 |
| Unknown | 跟节点失联了,状态未知 |

Pending最常见的原因:节点资源不足、nodeSelector/亲和性不匹配、PVC没绑定、有污点没容忍。

删除过程(这地方坑最多)

执行kubectl delete pod后,Kubernetes不会立刻干掉容器,而是走一套优雅终止流程:

  1. Pod标记为Terminating,同时从Service的Endpoints中移除(API Server并发发起这两个动作)。
  2. 如果定义了preStop钩子,执行它(必须在宽限期内完成)。
  3. 向容器PID 1发送SIGTERM信号。
  4. 开始宽限期倒计时(默认30秒)。
  5. 宽限期结束还没退出?发SIGKILL强制干掉。

这里有个经典坑[终审微调] Endpoints的更新通过API Server发起,但kube-proxy将新规则同步到iptables/IPVS需要一定时间(通常数百毫秒到几秒)。如果容器收到SIGTERM后立即退出,控制面组件(如kube-proxy)可能尚未完成规则刷新,仍有流量被转发到正在关闭的Pod上,导致5xx错误。因此 preStop sleep是为了"等待规则同步",而不是单纯为了拖时间

解决方案 :加个preStop sleep,给网络规则同步留出缓冲:

复制代码
lifecycle:
  preStop:
    exec:
      command: ["sleep", "5"]

完整优雅终止示例(结合Nginx):

复制代码
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  terminationGracePeriodSeconds: 60   # 宽限期改成60秒
  containers:
  - name: web
    image: nginx:1.27-alpine
    lifecycle:
      preStop:
        exec:
          command:
          - sh
          - -c
          - |
            sleep 5
            nginx -s quit
            while [ -f /var/run/nginx.pid ]; do sleep 1; done

while循环确保Nginx完全退出后才给kubelet返回,代码非常严谨,你可以直接拿去用。


资源配额与QoS:影响Pod生死的关键

resources字段定义容器的CPU和内存配额:

  • requests:调度器用它来决定把Pod放在哪个节点,保证容器至少能拿到这些资源。

  • limits:容器能使用的上限,防止单个容器把节点资源吃光。

    resources:
    requests:
    cpu: 100m # 0.1核
    memory: 200Mi
    limits:
    cpu: "1" # 1核
    memory: 500Mi

重点来了:QoS等级(Quality of Service)

Kubernetes根据requestslimits的配置组合,自动将Pod划分为三个QoS等级。当节点内存资源紧张时,kubelet会按照 BestEffort > Burstable > Guaranteed 的顺序驱逐Pod。

|----------------|------------------------------------------------|-------------|
| QoS等级 | 判定条件 | 驱逐优先级 |
| Guaranteed | Pod中所有容器 都同时设置了requests和limits,且两者值相等 | 最高保障,最不易被驱逐 |
| Burstable | Pod中至少有一个容器设置了requests或limits,但不满足Guaranteed条件 | 中等风险 |
| BestEffort | Pod中所有容器均未设置任何requests和limits | 最先被驱逐 |

我推荐这样设置

  • 生产环境必须设置requests和limits,避免被归为BestEffort。
  • 核心服务尽量让每个容器的requests == limits,享受Guaranteed待遇。
  • requests不要设太高,否则节点资源碎片化,调度不上去。

调度策略:让Pod去它该去的地方

生产环境中经常需要把Pod固定到特定节点(比如SSD机型、专有硬件),或者避开某些节点。最基础的是用nodeSelector

复制代码
spec:
  nodeSelector:
    disktype: ssd
  containers:
  - name: app
    image: nginx

如果情况更复杂,比如需要软性偏好或反亲和性(避免同应用Pod扎堆),可以用nodeAffinitypodAntiAffinity。由于配置较复杂,建议初级SRE先掌握nodeSelectortolerations(容忍节点污点),后续再深入学习亲和性。


常见故障:三板斧搞定

不管遇到啥Pod问题,先执行这三条:

复制代码
kubectl get pods                              # 看状态
kubectl describe pod <pod-name>               # 看Events
kubectl logs <pod-name> --previous            # 看崩溃前的日志

ImagePullBackOff / ErrImagePull

典型现象kubectl get pods 显示 ImagePullBackOff

复制代码
NAME         READY   STATUS             RESTARTS   AGE
api-server   0/1     ImagePullBackOff   0          5m

原因:镜像不存在、tag写错了、私有仓库没配凭证、或者Docker Hub拉取限额到了

解决办法

  • 检查镜像名和tag对不对。

  • 私有仓库要创建imagePullSecret并在Pod里引用。

    kubectl create secret docker-registry regcred
    --docker-server=registry.example.com
    --docker-username=user
    --docker-password=pass

    spec:
    imagePullSecrets:
    - name: regcred

ImagePullBackOff不是bug,是kubelet在告诉你"镜像拉不下来,我正在重试"。

CrashLoopBackOff

典型现象kubectl get pods 显示 CrashLoopBackOff

复制代码
NAME         READY   STATUS             RESTARTS   AGE
api-server   0/1     CrashLoopBackOff   5          10m

原因:容器启动后立刻崩溃,Kubernetes不断重启,且重启间隔指数增长

解决办法

  • kubectl logs <pod-name> --previous 看崩溃原因。
  • 检查存活探针是不是太激进(比如应用启动要30秒,initialDelaySeconds只给了3秒),参考上文探针配置调整initialDelaySeconds
  • 检查应用依赖(数据库连不上、配置文件不对等)。

CrashLoopBackOff是生产环境最常见的故障之一。

OOMKilled

典型现象 :容器状态显示 OOMKilled

原因 :容器内存使用超过了limits.memory,被内核杀了

解决办法 :调大limits.memory,或者排查应用的内存泄漏。同时检查该Pod的QoS等级,若为BestEffort,节点资源紧张时它会优先被驱逐。


总结与互动

回顾一下核心要点:

  1. Pod是Kubernetes的最小部署单元apiVersion: v1kind: Podspec.containers是必需的。
  2. Init Container在主容器之前运行,常用于等待依赖就绪,全部成功退出主容器才会启动。
  3. 存活探针和就绪探针是生产环境标配 ,务必配置initialDelaySecondsperiodSeconds,否则滚动更新必踩坑。
  4. QoS等级决定Pod被驱逐的优先级 ,核心服务务必让每个容器都满足requests == limits(Guaranteed)。
  5. Pod删除走优雅终止 :SIGTERM → 等待宽限期 → SIGKILL;务必加上preStop sleep规避网络规则同步延迟。
  6. 排查三板斧kubectl get podskubectl describe podkubectl logs --previous

你在生产环境遇到过什么诡异的Pod问题?欢迎在评论区分享,咱们一起排排坑。


如果觉得有用,欢迎分享给更多小伙伴。

参考来源

  • Kubernetes v1.28 官方文档 参考
  • 关于 Pod 生命周期与探针配置 参考
  • 关于 QoS 等级定义 参考
  • SRE 社区故障排查通用方法论