Docker 正常,K8s 崩溃
01 前置理论与核心机制¶
1. 为什么本地运行正常,部署到 K8s 却报错?¶
- 环境差异:如果在本地使用 docker run 或 docker-compose 能够正常拉起并运行,说明核心代码和业务逻辑通常没有问题。
- K8s 的附加约束:容器进入 K8s 集群后,不再处于缺乏限制的默认环境。K8s 会在容器外层施加一系列严格的运行约束,包括:资源限制 (Cgroups limits)、安全上下文 (SecurityContext)、健康探针 (Probes) 以及配置与存储挂载 (Volumes)。只要容器的行为触发了其中任何一项限制,Kubelet 就会将其强制终止并不断尝试重启,最终表现为 CrashLoopBackOff。
2. 退出码 (Exit Code) 的关键指引¶
- Kubelet 会记录容器退出时的状态码,这是排查的核心线索:
- Exit Code 0:进程正常退出。通常是因为启动命令切换到了后台运行(如使用了 nohup 或 systemctl),导致容器内缺乏前台常驻进程。
- Exit Code 1:应用程序内部发生错误退出(如代码抛出未捕获异常、找不到配置文件、文件权限不足或端口冲突)。
- Exit Code 137:进程收到 SIGKILL 信号被强制终止。绝大多数情况是因为内存超限触发了 OOMKilled,或存活探针失败被 Kubelet 强制重启。
02 故障排查与恢复闭环 SOP¶
面对不断重启的 Pod,无需优先排查业务代码,建议按照“状态事件 → 崩溃前日志 → 运行约束”的链路逐层排查:
步骤一:提取现场事件与退出码¶
优先查看控制面记录的全局状态与事件:
- 观测点 1(Events):如果 Events 明确提示 FailedScheduling、ErrImagePull 或 MountVolume.SetUp failed,说明容器尚未进入到执行代码阶段,需直接排查节点资源、镜像仓库凭证或存储卷配置。
- 观测点 2(Last State):如果 Events 显示 BackOff,向上查看 Last State,记录容器上一次退出的 Exit Code。若为 137,优先排查内存限制或探针超时;若为 1 或其他非零错误码,进入下一步。
步骤二:调取上一实例的崩溃日志 (Previous Logs)¶
对于处于不断重启状态的容器,常规的 kubectl logs 往往无法获取崩溃瞬间的报错,必须加上 --previous 参数:
- 观测点:通过日志判断是业务内部异常,还是系统级环境报错(如 Permission denied、No such file or directory)。
步骤三:排查 K8s 附加的运行规则限制¶
若日志中无明显业务代码报错,需逐一排查 K8s 施加的环境限制:
- 权限限制 (SecurityContext):
- 检查 YAML 中的 runAsUser 配置。本地 Docker 默认以 root (uid 0) 运行,如果 K8s 限制为 runAsUser: 1000(普通用户),业务进程写入 /var/log 目录或监听 1024 以下端口时,会因 Permission denied 直接退出。
- 验证命令:kubectl get pod
步骤四:配置修正与验证¶
查明原因后,针对性调整配置: - 权限问题:移除不当限制,或在构建基础镜像时修改目录属主(如 chown -R 1000:1000 /app,推荐做法)。 - 探针过早介入:调大 initialDelaySeconds,或改用针对慢启动程序的 startupProbe。 重新应用配置并验证状态是否恢复为 Running。
03 真实生产故障现象与案例库¶
案例 1:容器权限受限 (Permission denied)¶
- 故障现象:Node.js 项目在本地 Docker Desktop 运行正常,部署至 K8s 后持续报 Exit Code 1 重启。
- 排查结果:logs --previous 显示 Error: EACCES: permission denied, mkdir '/app/cache'。
- 根因与处置:运维团队出于安全规范,在 Pod 模板中全局注入了 securityContext: runAsUser: 1000。但镜像内的 /app/cache 目录属主为 root,导致进程无权写入。处置方案为修改 Dockerfile,在构建末尾增加 RUN chown -R 1000:1000 /app。
案例 2:内存超限触发强制终止 (OOMKilled)¶
- 故障现象:Java 容器启动数秒后即崩溃,无任何业务报错日志。
- 排查结果:kubectl describe 显示 Last State 的 Reason 为 OOMKilled,Exit Code 为 137。
- 根因与处置:本地开发机内存充足且未做限制;而 K8s 的 YAML 中配置了 limits.memory: 512Mi。JVM 启动时未配置内存限制参数,默认按宿主机物理内存比例申请堆空间,超出 cgroups 限制被内核强制终止。处置方案为合理调大 limit,并显式配置 -XX:MaxRAMPercentage。
案例 3:启动耗时过长导致存活探针误杀¶
- 故障现象:Spring Boot 服务无法正常就绪,describe 的 Events 持续提示 Liveness probe failed: HTTP probe failed with statuscode: 502。
- 排查结果:应用平均启动耗时 40 秒,但 livenessProbe 的 initialDelaySeconds 仅配置了 20 秒。
- 根因与处置:探针提前介入,在服务尚未启动完成时将其判定为故障并触发重启,形成死循环。处置方案为引入 startupProbe(启动探针)进行前置保护,待应用完全启动后再交由 liveness 探针接管。
案例 4:配置文件挂载覆盖原目录¶
- 故障现象:Python 容器启动报错 python: can't open file 'main.py': [Errno 2] No such file or directory。
- 排查结果:检查 YAML 的 volumeMounts 发现,为了注入配置文件,将一个 ConfigMap 直接挂载到了 /usr/src/app 目录下。
- 根因与处置:K8s 默认的目录挂载会覆盖容器内原有的同名目录内容,导致原本存放在该目录下的 main.py 等业务代码被隐藏。处置方案为使用 subPath 参数仅挂载单个文件,避免覆盖整个目录。
04 生产排障速查表(本地正常 K8s 崩溃)¶
异常定性 (Exit Code)现象与日志特征高频根因场景标准处置动作Exit Code 137Reason: OOMKilled,无业务报错limits.memory 设置过小,或 JVM 未限制堆内存调大 Memory Limit,修正程序启动内存参数Exit Code 1--previous 报 Permission deniedK8s 配置了 runAsUser 导致非 root 运行受限移除冲突配置,或在构建镜像时赋予目录对应 UID 权限Exit Code 1--previous 报找不到文件 / Not FoundVolume 挂载覆盖了代码目录,或缺失环境变量改用 subPath 挂载单文件,核对 ConfigMap 配置Exit Code 0正常输出少量日志后立即退出并重启启动脚本在后台运行,容器缺乏前台常驻进程修改启动命令,去除 nohup 或 -d 等后台参数非正常重启进程未报错,Events 报 Unhealthy 后重启启动耗时较长,存活探针过早介入判定失败调大 initialDelaySeconds,或引入 startupProbe
05 面试应答参考话术¶
面试官问:“同一个镜像,本地 Docker 跑得好好的,一部署到 K8s 就报错、无限重启。你怎么排查?”
可以这样回答:这个问题主要考察本地环境与 K8s 集群运行规则的差异。既然本地 Docker 能够正常运行,说明代码逻辑本身通常没有问题,故障原因主要集中在 K8s 附加的资源限制、安全权限、探针配置以及存储挂载上。我会按以下三个步骤排查:第一步:查看状态事件和退出码
首先执行 kubectl describe pod,查看底部的 Events,排除调度或镜像拉取问题。随后重点检查 Last State 中记录的 Exit Code。 如果退出码是 137,通常是因为 K8s 中的 limits.memory 配额过小触发了 OOMKilled,或者被存活探针强制重启;如果退出码是 0,说明启动命令被放到了后台执行,容器因没有前台常驻进程而自动退出。第二步:获取上一实例的崩溃日志
如果退出码是 1 或其他非零状态,常规日志往往无法抓取崩溃瞬间的输出。我会使用 kubectl logs <pod> --previous 命令,提取容器上一次崩溃退出前的报错信息。第三步:排查 K8s 附加的环境限制
结合报错日志,重点核对三类环境差异: - 权限差异:检查 securityContext。本地默认是 root 运行,K8s 中若配置了 runAsUser 强制以普通用户运行,进程写入日志或绑定低位端口时就会因权限拒绝而退出。 - 探针配置:检查 livenessProbe。若应用启动较慢,而探针的 initialDelaySeconds 设置过短,应用在启动阶段就会被 Kubelet 误判异常并反复重启。 - 目录挂载:检查 volumeMounts。如果将 ConfigMap 直接挂载到业务代码目录,且未配置 subPath,会直接覆盖原有的代码文件,导致启动时报错找不到入口文件。
沿着这套排查链路核对环境差异,通常能快速定位根因。
06 面试高频追问与应答预案¶
追问 1:如果容器刚启动就崩溃了,想用 exec 进容器手动执行命令排查,但根本进不去怎么办?¶
- 回答要点:
- 容器处于 Terminated 或 CrashLoopBackOff 冷却状态时,无法执行 exec。
- 解决办法:临时修改 Deployment 的启动命令(command 或 args),将其替换为 ["sleep", "infinity"] 或 ["tail", "-f", "/dev/null"]。让容器启动后保持前台休眠的 Running 状态。随后通过 kubectl exec -it
-- sh 进入容器,手动执行原有的启动脚本,观察具体的报错输出。
追问 2:为什么容器里的进程不能用后台方式运行(比如 nohup java -jar app.jar &)?¶
- 回答要点:
- 容器的生命周期与其内部的 PID 1 进程强绑定。
- 如果使用后台方式运行,启动脚本(作为 PID 1)在执行完 nohup 命令后,会认为自身任务已完成并正常退出(Exit Code 0)。一旦 PID 1 退出,容器运行时就会判定容器生命周期结束并将其销毁,进而触发 Kubelet 不断重启。因此,容器的主业务进程必须保持在前台阻塞运行。
07 自动诊断脚本(check_crash_reason.sh)¶
该脚本用于快速分析处于 CrashLoopBackOff 状态的 Pod,自动提取 Exit Code、上一实例崩溃日志,并核查常见的 K8s 环境限制配置。
#!/usr/bin/env bash
# ==============================================================================
# 脚本名称: check_crash_reason.sh
# 功能描述: 自动诊断 Pod CrashLoopBackOff 的核心环境原因 (纯只读)
# 使用方式: ./check_crash_reason.sh [namespace]
# ==============================================================================
set -o pipefail
POD_NAME=$1
NAMESPACE=${2:-"default"}
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PLAIN='\033[0m'
if [ -z "$POD_NAME" ]; then
echo -e "${RED}用法: $0 [namespace]${PLAIN}"
exit 1
fi
echo -e "${BLUE}================================================================${PLAIN}"
echo -e "${BLUE} Pod CrashLoopBackOff 异常原因自动诊断链路 ${PLAIN}"
echo -e "${BLUE}================================================================${PLAIN}"
# 1. 检查退出码与 OOM
echo -e "\n${YELLOW}[1/4] 分析上一次崩溃退出码 (Exit Code)...${PLAIN}"
LAST_STATE=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.status.containerStatuses[0].lastState.terminated}')
if [ -z "$LAST_STATE" ] || [ "$LAST_STATE" == "{}" ]; then
echo -e " -> ${RED}[WARN] 未获取到 lastState 信息,容器可能尚未启动或处于其他异常状态。${PLAIN}"
else
EXIT_CODE=$(echo "$LAST_STATE" | grep -o '"exitCode":[^,]*' | awk -F':' '{print $2}')
REASON=$(echo "$LAST_STATE" | grep -o '"reason":[^,]*' | awk -F'"' '{print $4}')
echo -e " -> 提取到退出码: ${RED}${EXIT_CODE}${PLAIN}, 退出原因: ${RED}${REASON}${PLAIN}"
if [ "$EXIT_CODE" == "137" ] || [ "$REASON" == "OOMKilled" ]; then
echo -e " -> ${RED}[诊断] 触发强制终止 (137/OOMKilled),请排查 limits.memory 配额或存活探针日志。${PLAIN}"
elif [ "$EXIT_CODE" == "0" ]; then
echo -e " -> ${YELLOW}[诊断] 退出码为 0。请检查启动脚本是否在后台执行 (如包含 & 或 nohup),容器要求主进程前台常驻。${PLAIN}"
else
echo -e " -> ${YELLOW}[INFO] 进程异常退出,需结合下方配置检查与崩溃日志进一步定位。${PLAIN}"
fi
fi
# 2. 检查权限限制配置
echo -e "\n${YELLOW}[2/4] 检查安全上下文配置 (SecurityContext)...${PLAIN}"
RUN_AS_USER=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.securityContext.runAsUser}')
if [ -z "$RUN_AS_USER" ]; then
RUN_AS_USER=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.containers[0].securityContext.runAsUser}')
fi
if [ -n "$RUN_AS_USER" ]; then
echo -e " -> ${RED}[WARN] 检测到非 root 运行配置: runAsUser=${RUN_AS_USER}。${PLAIN}"
echo -e " -> ${YELLOW}[诊断] 请排查容器是否因权限不足导致写入文件或绑定端口时报 Permission denied。${PLAIN}"
else
echo -e " -> ${GREEN}[OK] 未配置 runAsUser 限制。${PLAIN}"
fi
# 3. 检查存活探针配置
echo -e "\n${YELLOW}[3/4] 检查存活探针配置与超时风险...${PLAIN}"
LIVENESS=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.containers[0].livenessProbe}')
if [ -n "$LIVENESS" ]; then
DELAY=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.containers[0].livenessProbe.initialDelaySeconds}')
echo -e " -> 检测到存活探针配置,initialDelaySeconds = ${DELAY}s。"
EVENTS=$(kubectl get events -n "$NAMESPACE" --field-selector involvedObject.name="$POD_NAME" | grep -i "Liveness probe failed")
if [ -n "$EVENTS" ]; then
echo -e " -> ${RED}[诊断] 检测到存活探针失败事件!应用启动耗时可能超过探针预期,建议调大 initialDelaySeconds 或配置 startupProbe。${PLAIN}"
else
echo -e " -> ${GREEN}[OK] 暂未在 Events 中发现探针失败记录。${PLAIN}"
fi
else
echo -e " -> ${GREEN}[OK] 未配置 livenessProbe。${PLAIN}"
fi
# 4. 提取上一实例崩溃日志
echo -e "\n${YELLOW}[4/4] 提取崩溃前最后 10 行日志 (--previous)...${PLAIN}"
kubectl logs "$POD_NAME" -n "$NAMESPACE" --previous --tail=10 2>/dev/null | while read -r line; do
echo -e " | $line"
done || echo -e " -> ${YELLOW}无法获取上一实例日志 (容器可能已被重建或未曾成功启动)。${PLAIN}"
echo -e "\n${BLUE}================================================================${PLAIN}"
echo -e "${BLUE}诊断结束。若以上配置均正常且日志报找不到文件,请检查 volumeMounts 是否覆盖了原目录。${PLAIN}"
echo -e "${BLUE}================================================================${PLAIN}"