K8s Pod-最小调度单位深度拆解
K8s Pod 完全指南——从 Linux 内核到 YAML,一次彻底搞懂
大家好,这里是彩妙呀~ 🎉
在云原生时代,Kubernetes(K8s)已经成了容器编排的事实标准。不管你是做运维还是做开发,都绕不开一个最基础的概念——Pod。
很多小伙伴学了 kubectl apply -f pod.yaml,Pod 跑起来了,但不知道这条命令背后到底发生了什么。当你知道 Pod 底层运行的原理,排查问题的时候才不会抓瞎。
📖 官方文档:https://kubernetes.io/docs/concepts/workloads/pods/
下面,彩妙将会带着大家从 Linux 内核的 namespace/cgroup 讲起,到 Pod 的完整生命周期,到多容器设计模式,再到生产排错实战。全程 14 节,跟着看完,Pod 从此不再有秘密。
目录
- 一、容器是什么?——从 Linux 内核讲起
- 二、为什么容器不够,还要发明 Pod?
- 三、Pod YAML 逐字段详解
- 四、Pod 的骨架——Pause 容器深度拆解
- 五、Pod 生命周期·上——从 kubectl apply 到容器跑起来
- 六、Pod 生命周期·中——网络、Service、健康检查
- 七、Pod 生命周期·下——删除、优雅终止、自愈
- 八、多容器 Pod 与设计模式
- 九、Init 容器详解
- 十、Sidecar 容器详解
- 十一、Pod 安全上下文——别让你的容器裸奔
- 十二、新手最常踩的 6 个坑(附完整排查流程)
- 十三、动手实验
- 十四、总结
一、容器是什么?——从 Linux 内核讲起
🎯 为什么要把这一节放在最前面? 因为如果你不理解容器底层是怎么"隔离"的,你就不可能真正理解:为什么同一个 Pod 内的容器能用 localhost 互通?为什么需要 Pause 容器?这些都不是 K8s 发明的魔法——只是 Linux 内核提供的两个老功能:namespace 和 cgroup。
1.1 先说结论:容器 = 普通进程 + 隔离 + 限额
❌ 常见的错误理解:
"容器是一个微型虚拟机,里面跑着一个完整的操作系统"
"容器有自己独立的内核"
✅ 正确理解:
容器 = 宿主机上的一个普通进程
+ namespace(决定这个进程能"看见"什么)
+ cgroup(决定这个进程能"用"多少资源)
+ 独立的文件系统(通过镜像打包)
它和你在服务器上直接 ./run-my-app 的区别仅仅是:
- 直接 run → 能看到所有进程、所有文件、能用所有 CPU
- 容器 run → 只能看到自己被允许看到的、只能用被分配的资源
1.2 namespace ——决定进程"能看见什么"
🎯 人话版: namespace 就像给每个进程发了一副"特制 VR 眼镜"。宿主机上实际有 200 个进程在跑,但戴上眼镜后——你只能看到 3 个。你完全不知道真实世界长什么样。
Linux 提供了 6 种主要的 namespace:
| namespace | 隔离什么 | 没有它会怎样 |
|---|---|---|
| PID | 进程 ID | 容器里 ps aux 能看到宿主机所有进程 |
| NET | 网卡、IP、端口、路由表 | 容器没有独立 IP |
| MNT | 文件系统挂载点 | 容器和宿主机共享文件系统 |
| UTS | 主机名 | 容器改 hostname 会影响宿主机 |
| IPC | 信号量、消息队列 | 容器间 IPC 资源不隔离 |
| User | 用户 ID | 容器内的 root 在宿主机上也是 root |
验证实验:
# 在宿主机上查看进程数
ps aux | wc -l
# → 比如 342 个进程
# 在容器里查看进程数
docker run --rm -it alpine ps aux
# → 只看到 2 个!(容器自己的 shell 和 ps 命令)
# → PID 从 1 开始——容器以为自己是系统的"第一个进程"!
# 在宿主机上查看同一个容器的真实 PID:
ps aux | grep alpine
# → 宿主机上看到的 PID 是 28471
# 容器内 PID 1 → 宿主机 PID 28471 → PID namespace 做了映射
1.3 cgroup ——决定进程"能用多少"
🎯 人话版: 如果 namespace 是"VR 眼镜",那 cgroup 就是"水电表"。namespace 决定了你能看见什么,cgroup 决定了你能用多少——CPU 不能超过 2 核、内存不能超过 4GB。
# 你可以手动创建一个 cgroup 并限制一个进程!
# 1. 创建控制组(本质是新建目录)
sudo mkdir /sys/fs/cgroup/memory/myapp
# 2. 限制内存为 128MB(往文件写个数字就行!)
echo "134217728" | sudo tee /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes
# 3. 把进程放进去(写它的 PID)
echo "12345" | sudo tee /sys/fs/cgroup/memory/myapp/cgroup.procs
# → 进程 12345 现在最多用 128MB!超过就直接被内核 OOM Kill!
| cgroup 子系统 | 控制什么 | K8s 中对应哪个字段 |
|---|---|---|
cpu | CPU 使用配额 | resources.limits.cpu |
memory | 内存上限 | resources.limits.memory |
blkio | 磁盘 I/O 速度 | CSI storageIO 限制 |
pids | 进程数上限 | kubelet --pod-max-pids 参数 |
1.4 容器 vs 虚拟机——一道面试必考题
| 维度 | 虚拟机 | 容器 |
|---|---|---|
| 启动速度 | 分钟级 | 秒级 |
| 资源占用 | GB 级(每 VM 一套完整 OS) | MB 级(只打包应用+依赖) |
| 隔离程度 | 硬件级(独立内核,更强) | 进程级(共享内核) |
| 单机可运行数 | 几十个 | 成百上千个 |
| 典型镜像大小 | 几 GB | 几十 MB |
二、为什么容器不够,还要发明 Pod?
2.1 问题:有些进程天生就该"贴在一起"
想象你要部署一个 Web 服务 + 日志采集器:
方案 A:两个独立容器(不用 Pod)
❌ 问题 1:日志采集器怎么读到 Web 容器的文件?——文件系统隔离,互相看不到!
❌ 问题 2:怎么通信?——只能走网络(172.17.0.5 → 172.17.0.6),有延迟
❌ 问题 3:怎么保证命运与共?——Web 挂了,日志采集器还傻傻地跑着
方案 B:放到同一个 Pod 里(K8s 的做法)
✅ 共享 Volume → 日志采集器直接读 Web 的日志
✅ 共享 Network Namespace → 同一 IP,localhost 直连,零延迟
✅ 共享生命周期 → 一起创建一起销毁
✅ 共享 IPC → 可用信号量和共享内存通信
2.2 Pod 的定义
🎯 一句话:Pod 是 K8s 中最小、最简单的部署单元。 一个 Pod 可以包含一个或多个容器,这些容器共享网络、存储和生命周期。
🎯 人话版: Pod 就像"双人宿舍"——两个室友(容器)共用一个门牌号(IP)、一个冰箱(Volume)、可以在房间里直接喊话(localhost),但各自睡自己的床(独立的文件系统和进程空间)。
2.3 一个 Pod 该放几个容器?
| 情况 | 占比 | 示例 |
|---|---|---|
| 只放 1 个容器 | ~95% | 普通 Web、API、后台任务 |
| Sidecar 模式 | ~4% | 主容器 + 日志采集 / 代理 / 监控 |
| Init 容器 + 主容器 | ~1% | 启动前等数据库、跑迁移脚本 |
三、Pod YAML 逐字段详解
🎯 很多教程贴完 YAML 就完事了,彩妙要带你逐字段看懂每一行在说什么。
3.1 最简 Pod YAML(从这里起步)
apiVersion: v1 # API 版本:Pod 用 v1
kind: Pod # 资源类型:我要创建 Pod
metadata: # 身份信息
name: my-first-pod # Pod 名字(同命名空间唯一)
labels: # 标签(Service 靠这个找到 Pod!)
app: nginx
spec: # 期望状态:里面有什么
containers: # 容器列表(数组)
- name: nginx # 容器名
image: nginx:1.25 # 镜像:名字:版本
ports: # 端口声明
- containerPort: 80
3.2 每个字段到底在说什么
① apiVersion: v1
# 告诉 K8s:"我用的是 v1 版本的 Pod API"
# Pod / Service / ConfigMap 等核心资源 → v1
# Deployment / StatefulSet → apps/v1
# Job / CronJob → batch/v1
# 怎么查?kubectl api-resources | grep Pod
② kind: Pod
# 告诉 K8s:"我要创建的资源类型是 Pod"
# 大小写敏感!不能写 pod 或 POD
# 可用类型:kubectl api-resources 查看完整列表
③ metadata.name
# Pod 名字,同一命名空间下必须唯一
# 命名规范:小写字母 + 数字 + 连字符(-)
# ✅ nginx-frontend payment-service-v2 redis-cache-0
# ❌ Payment_Service(大写+下划线)
④ metadata.labels
# 标签——K8s 中最重要的组织方式!
# Service 通过标签找到后端 Pod → kubectl get pods -l app=nginx
# 建议每个资源至少打上:app / version / tier / team / env
labels:
app: nginx # 应用名
version: v2 # 版本 -> 灰度发布靠它
tier: frontend # 层级 -> frontend/backend/middleware
team: platform # 团队 -> 成本核算
env: production # 环境 -> dev/staging/prod
⑤ spec.containers
# 容器列表——这是一个数组(YAML 中用 - 表示数组元素)
# 一个 Pod 可以有多个容器,但一般只有一个
containers:
- name: nginx
image: nginx:1.25 # ⚠️ 不要用 :latest!锁死版本号
imagePullPolicy: IfNotPresent # 本地有就不重新下载
⑥ spec.containers[].ports
ports:
- name: http # 端口名字
containerPort: 80 # 容器监听的端口
protocol: TCP
# ⚠️ 关键理解:ports 只是"声明",不是"暴露"!
# 不写 ports → 容器照样能监听端口
# 写了 ports → kubectl describe 会显示、Service 可以用名字引用
3.3 镜像拉取策略——imagePullPolicy
| 值 | 行为 | 适用场景 |
|---|---|---|
Always | 每次重启都重新拉取 | 开发阶段,确保最新 |
IfNotPresent | 本地有就用本地 | 生产推荐(版本已锁定) |
Never | 只用本地 | 离线/安全环境 |
⚠️ 如果 tag 是
:latest且你没写imagePullPolicy,K8s 默认用Always——所以永远别用:latest!
3.4 工作目录和启动命令——workingDir / command / args
workingDir: /usr/share/nginx/html # 容器启动后的当前目录
command: ["/bin/sh"] # 覆盖 Dockerfile 的 ENTRYPOINT
args: ["-c", "nginx -g 'daemon off;'"] # 覆盖 Dockerfile 的 CMD
🎯 人话版:
command + args = 完整的启动命令。如果你不写,就用镜像里 Dockerfile 定义的默认值。
3.5 环境变量——env / envFrom
env:
# 方式 1:硬编码
- name: APP_ENV
value: "production"
# 方式 2:从 ConfigMap 引用(推荐!)
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: database-host
# 方式 3:从 Secret 引用(密码/Token)
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
# 方式 4:批量注入整个 ConfigMap 的所有 key
envFrom:
- configMapRef:
name: app-config
3.6 完整 YAML 速查卡
🍵 友情小知识: 遇到不确定的字段?不用 Google!用
kubectl explain pod.spec.containers比搜索快 10 倍。
apiVersion: v1
kind: Pod
metadata:
name: my-app
namespace: default
labels:
app: my-app
version: v1
tier: backend
annotations:
description: "生产后端服务"
git-commit: abc123
spec:
restartPolicy: Always # Always | OnFailure | Never
terminationGracePeriodSeconds: 60 # 优雅退出倒计时
serviceAccountName: my-sa # ServiceAccount
automountServiceAccountToken: true
containers:
- name: app
image: myapp:v1.2.3 # ⚠️ 不用 :latest!
imagePullPolicy: IfNotPresent
workingDir: /app
command: ["/app/server"]
args: ["--config", "/etc/config/app.yaml"]
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: ENV
value: "production"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
resources: # 资源管理(超重要!)
requests: # 下限:调度器按这个找节点
cpu: "250m" # 250m = 0.25 核
memory: "256Mi"
limits: # 上限:超过就限流/OOMKill
cpu: "500m"
memory: "512Mi"
volumeMounts: # 挂载存储卷
- name: app-data
mountPath: /data
readOnly: false
livenessProbe: # 存活探针
httpGet:
path: /healthz
port: 8080
periodSeconds: 15
failureThreshold: 5 # 保守!连续失败 5 次才重启
readinessProbe: # 就绪探针
httpGet:
path: /ready
port: 8080
periodSeconds: 5
failureThreshold: 3 # 敏感!失败 3 次就摘流
lifecycle: # 生命周期钩子
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"] # 等 5 秒让流量排空
securityContext: # 安全上下文
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumes: # Pod 级别存储卷
- name: app-data
persistentVolumeClaim:
claimName: app-pvc
四、Pod 的骨架——Pause 容器深度拆解
🎯 这是理解 Pod 网络模型最关键的一节。 为什么同一个 Pod 内的容器能用 localhost 互通?为什么容器重启后 Pod IP 不变?答案全在 Pause 容器。
4.1 Pause 容器是什么?
4.2 没有 Pause 容器会怎样?
假设没有 Pause 容器,让 Nginx 自己持有 Network Namespace:
Nginx 启动 → 创建 Network NS → 分配 IP 10.244.1.23
Nginx 崩溃 → kubelet 重启 Nginx
→ 旧 Network NS 销毁!新容器创建新 Network NS → 新 IP 10.244.2.45!
→ IP 变了 → Service 找不到 Pod → 所有连接断开!
有 Pause 容器时:
Pause 先启动 → 创建 Network NS → 分配 IP 10.244.1.23
Nginx 启动 → "加入" Pause 的 Network NS → 共享同一个 IP
Nginx 崩溃重启 → 重新"加入"同一个 Pause 的 Network NS → IP 不变!
→ Service 完全无感知!
4.3 Pause 容器源码(极简 20 行)
// Kubernetes pause 容器的核心代码
// 来源:kubernetes/build/pause/linux/pause.c
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
static void sigdown(int signo) { /* 忽略 SIGTERM/SIGINT */ }
static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0); // 回收僵尸子进程
}
int main(void) {
sigaction(SIGINT, &sigdown, NULL);
sigaction(SIGTERM, &sigdown, NULL);
sigaction(SIGCHLD, &sigreap, NULL);
for (;;) pause(); // 永久睡眠——只"占坑",不消耗 CPU
}
三件事: ① 不能被轻易杀死 ② 自动回收僵尸子进程 ③ 永久睡眠,只"占坑"
五、Pod 生命周期·上——从 kubectl apply 到容器跑起来
5.1 全景图
5.2 阶段①:kubectl apply → API Server
<注意:准入控制是很关键的一步——Istio 就是利用 MutatingAdmissionWebhook 在 Pod 提交时自动注入 Envoy Sidecar 容器。这也是后面 Service Mesh 章节的重点。>
5.3 阶段②:Scheduler 分配节点
🎯 人话版: Scheduler 就像房产中介——先过滤掉不满足要求的(海选淘汰),再从剩下的里面挑最合适的(决赛打分),最高分的就选它。
5.4 阶段③:Kubelet 创建容器
<注意:K8s 1.24 之后默认不再使用 Docker 作为容器运行时,而是直接使用 containerd。调用链上少了 Docker Engine 这一层。>
六、Pod 生命周期·中——网络、Service、健康检查
6.1 阶段④:CNI 分配网络
CNI 做三件事:① 分配 IP ② 创建 veth pair(虚拟网线)③ 配路由
🍵 友情小知识: Flannel、Calico 都是 CNI 的具体实现——就像不同品牌的交换机,功能一样,实现细节不同。
6.2 阶段⑤:Service 代理
Pod IP 会变(Pod 死了重建 IP 就变了),Service 解决这个问题——给一组 Pod 一个固定的"电话号码"(ClusterIP)。
Service 通过 Label(标签) 来匹配 Pod:
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx # 只要 Pod 带这个标签,就自动加入"通讯录"
ports:
- port: 80 # Service 对外的端口
targetPort: 80 # Pod 上容器监听的端口
| Service 类型 | 访问范围 | 用途 |
|---|---|---|
| ClusterIP | 集群内部 | 微服务间调用(默认,最常用) |
| NodePort | 节点 IP + 30000-32767 | 测试/临时暴露 |
| LoadBalancer | 公网 IP | 对接云厂商 LB |
| ExternalName | DNS 别名 | 映射外部服务 |
6.3 阶段⑥:健康检查 + 自愈
三种探针:
| 探针 | 问什么 | 失败了会怎样 |
|---|---|---|
| Startup | 启动完了吗? | 暂停 Liveness 检查,防误杀 |
| Liveness | 你还活着吗? | 杀掉容器,重新创建 |
| Readiness | 你能接客了吗? | 从 Service 通讯录移除(不重启!) |
startupProbe: # 慢启动应用救星
httpGet:
path: /health
port: 80
failureThreshold: 30 # 最多等 30×10 = 300 秒
periodSeconds: 10
livenessProbe: # 保守!别随便重启
httpGet:
path: /healthz
port: 80
periodSeconds: 15
failureThreshold: 5 # 连续失败 5 次才重启
readinessProbe: # 敏感!依赖不通就摘流
httpGet:
path: /ready
port: 80
periodSeconds: 5
failureThreshold: 3
自愈机制 = 控制循环(Reconciliation Loop):
七、Pod 生命周期·下——删除、优雅终止、自愈
<注意:SIGTERM(15)可以被捕获做优雅退出,SIGKILL(9)不行,内核直接杀。这是 Linux 面试高频题!>
🚨 你的应用没处理 SIGTERM 的后果
❌ 错误示范:
int main() {
while (true) {
handle_request(); // 被 SIGKILL 强杀时 → 正在处理的请求全部丢失!
}
}
✅ 正确示范(C++ 版):
#include <csignal>
#include <atomic>
std::atomic<bool> running{true};
void signal_handler(int signum) {
if (signum == SIGTERM) {
std::cout << "收到 SIGTERM,准备优雅退出..." << std::endl;
running = false;
}
}
int main() {
signal(SIGTERM, signal_handler);
while (running) { handle_request(); }
std::cout << "优雅退出完成" << std::endl;
return 0;
}
✅ 正确示范(Go 版):
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh // 阻塞等待信号
// 收到后:停新请求 → 等现有请求完成 → 退出
八、多容器 Pod 与设计模式
🎯 虽然 95% 的 Pod 只放一个容器,但理解剩下的 5% 能让你看懂 Istio、Fluentd、Prometheus 是怎么和你的应用"住在一起"的。
Init 容器 vs Sidecar 容器
| Init 容器 | Sidecar 容器 | |
|---|---|---|
| 什么时候运行 | Pod 启动时,主容器之前 | 和主容器同时运行 |
| 运行多久 | 跑完就停 | 和 Pod 一样长 |
| 典型用途 | 等数据库、跑迁移、下载配置 | 日志采集、代理、监控 |
| 顺序 | 按定义顺序串行 | 和主容器并行 |
九、Init 容器详解
9.1 等数据库就绪 + 执行迁移
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
# 第 1 步:等 MySQL 就绪
- name: wait-for-mysql
image: busybox:1.36
command:
- sh
- -c
- |
echo "等待 MySQL..."
until nslookup mysql-service.default.svc.cluster.local; do
echo "还没就绪, 2 秒后重试..."; sleep 2
done
echo "MySQL 就绪!"
# 第 2 步:等 Redis 就绪
- name: wait-for-redis
image: busybox:1.36
command:
- sh
- -c
- |
echo "等待 Redis..."
until nc -z redis-service 6379; do
echo "Redis 还没就绪..."; sleep 2
done
echo "Redis 就绪!"
# 第 3 步:执行数据库迁移
- name: db-migration
image: myapp-migration:v1.2
env:
- name: DB_HOST
value: mysql-service
# 所有 Init 成功后启动主容器
containers:
- name: app
image: myapp:v1.2
ports:
- containerPort: 8080
🎯 人话版: Init 容器就像"开工前的准备工作"——你先检查水电通了没(等 MySQL/Redis),再把工具准备好(跑迁移),最后才开始干活(启动主容器)。任何一步准备失败,整个 Pod 重新来过。
9.2 Init 容器的关键规则
- 按定义顺序串行执行(1 → 2 → 3 → …)
- 任何一个失败 → Pod 重启 → 所有 Init 重新执行
- 可以用和主容器完全不同的镜像(busybox 做网络检查、专用工具镜像做迁移)
- Init 成功后不再运行,不占用资源
十、Sidecar 容器详解
10.1 日志采集 Sidecar
apiVersion: v1
kind: Pod
metadata:
name: web-with-logger
spec:
volumes:
- name: shared-logs
emptyDir: {}
containers:
# 主容器:Web 服务
- name: web-server
image: nginx:1.25
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
# Sidecar:采集日志
- name: log-collector
image: fluentd:latest
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
readOnly: true
🍵 友情小知识: K8s v1.28+ 原生支持 Sidecar 容器(通过
initContainers+restartPolicy: Always)。Sidecar 在 Job 中也能正常终止了,不会像以前一样让 Job 永久挂起。
10.2 常见 Sidecar 模式
| 模式 | 作用 | 例子 |
|---|---|---|
| 日志采集 | 收集主容器日志 | Fluentd / Filebeat |
| 代理转发 | 代理主容器的网络流量 | Envoy(Istio) |
| 监控指标 | 导出主容器指标 | Prometheus Exporter |
| 格式转换 | 转换主容器输出 | 非标准日志 → JSON |
十一、Pod 安全上下文——别让你的容器裸奔
🎯 默认情况下,很多容器镜像是用 root 用户运行的。如果容器被攻击者突破——对方在容器内就是 root——再配合容器逃逸漏洞,就能拿到宿主机 root 权限!
11.1 生产环境的最低安全配置
spec:
securityContext: # Pod 级别
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000 # 挂载的卷归属这个用户组
containers:
- name: app
image: myapp
securityContext: # 容器级别(覆盖 Pod 级别)
privileged: false # 绝对不开特权模式
allowPrivilegeEscalation: false # 禁止 sudo/suid
readOnlyRootFilesystem: true # 根文件系统只读
capabilities:
drop: ["ALL"] # 丢弃所有 Linux Capability
add: ["NET_BIND_SERVICE"] # 只加回来必需的
11.2 每个字段是干嘛的
| 字段 | 作用 | 为什么重要 |
|---|---|---|
runAsNonRoot: true | 禁止以 UID 0 运行 | 防止容器逃逸后直接获得宿主机 root |
allowPrivilegeEscalation: false | 禁止 sudo/suid | 普通用户无法提权 |
readOnlyRootFilesystem: true | 根文件系统只读 | 攻击者无法写入恶意文件 |
privileged: false | 不是特权模式 | 特权模式 = 能访问所有宿主机设备 |
capabilities.drop: [ALL] | 丢弃所有 Linux Capability | 最小化内核能力 |
🎯 人话版: securityContext 就像给容器戴上了"手铐"——即使攻击者闯进来了,他也什么都干不了:不能写文件、不能提权、不能用危险的内核能力。
11.3 三个安全等级(Pod Security Standards)
| 等级 | 含义 | 适用 |
|---|---|---|
| Privileged | 无限制 | 仅系统级工作负载 |
| Baseline | 防已知提权 | 大多数场景 |
| Restricted | 最严格 | 生产环境高安全要求 |
# 给命名空间强制执行 Restricted 策略
kubectl label ns production \
pod-security.kubernetes.io/enforce=restricted
十二、新手最常踩的 6 个坑(附完整排查流程)
🚨 坑 1:Pod 一直 Pending
# 排查
kubectl describe pod <name> | grep -A 10 Events
# Events 速查:
# "Insufficient cpu/memory" → 资源不够 → 扩容节点或减小 requests
# "untolerated taint" → 节点有污点 → 加 toleration
# "persistentvolumeclaim not found" → PVC 不存在 → kubectl get pvc
🚨 坑 2:CrashLoopBackOff
# 核心排查命令
kubectl logs <pod> --previous # 看崩溃前的日志!
# 查退出码
kubectl describe pod <pod> | grep "Exit Code"
# 0=正常 1=应用错误 137=OOMKilled 139=段错误 143=SIGTERM
🚨 坑 3:OOMKilled(Exit Code 137)
# 确认 OOM
kubectl describe pod <pod> | grep -A5 "Last State"
# Reason: OOMKilled → 内存超 limits
# 解决:
# ① 增大 limits.memory
# ② 修复内存泄漏
# ③ Java 应用:用 -XX:MaxRAMPercentage=75.0 替代固定 -Xmx
<注意:OOMKilled 是静默被杀——内核在容器外杀掉进程,应用不会有任何错误日志。所以你用
kubectl logs看当前容器日志什么也看不到,必须用--previous。>
🚨 坑 4:Pod Running 但访问不到
# 三步排查
kubectl describe pod <pod> | grep -A5 Readiness # ① Probe 过了吗?
kubectl get endpoints <svc> # ② Endpoints 为空?
kubectl get pod --show-labels # ③ 标签匹配 Service 吗?
🚨 坑 5:ClusterIP ping 不通
# ❌ 错误测试
ping 10.96.0.1 # 不通!这是正确的——ClusterIP 是虚拟 IP
# ✅ 正确测试
curl http://10.96.0.1:80 # 通就对了!
# 解释:ClusterIP 没有物理网卡,只存在于 iptables/IPVS 规则中
# ping 用 ICMP 协议,不经过传输层 → 没有 iptables 规则处理 ICMP → 不通
# curl 用 TCP → iptables 有 DNAT 规则 → 通
🚨 坑 6:删 Pod 偶尔丢请求
# 原因:应用没处理 SIGTERM → 30 秒后被 SIGKILL 强杀 → 请求丢失
# 解决:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"] # 等 kube-proxy 更新规则
十三、动手实验
实验 1:创建 Pod,观察诞生
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: my-first-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
EOF
kubectl get pod -w
# 观察:Pending → ContainerCreating → Running
实验 2:看 Pod 的"一生"证据
kubectl describe pod my-first-pod
# 看 Events:Scheduled → Pulling → Pulled → Created → Started
实验 3:进容器看内部
kubectl exec -it my-first-pod -- /bin/bash
ps aux # 只看到自己的进程!(namespace 隔离)
ip addr # 看到 CNI 分配的 IP 10.42.0.x
curl localhost # 访问本机 nginx
exit
实验 4:测试自愈
# 终端 1
kubectl get pod -w
# 终端 2
kubectl delete pod my-first-pod
# 终端 1 中看 Pod 立刻被重建!(Controller 自动纠错)
实验 5:模拟 CrashLoopBackOff
kubectl run crash --image=busybox --restart=Always --command -- sh -c 'exit 1'
kubectl get pod -w
# 观察 RESTARTS 增长 + 退避间隔变大
kubectl logs crash --previous # 看崩溃前的输出
kubectl delete pod crash
实验 6:优雅终止测试
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: graceful-test
spec:
terminationGracePeriodSeconds: 60
containers:
- name: nginx
image: nginx:1.25
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "echo 'PreStop: 清理中...' && sleep 5 && echo 'PreStop: 完成'"]
EOF
kubectl delete pod graceful-test
kubectl get pod -w
# 观察:delete 后有明显延迟才消失(PreStop 正在执行)
实验 7:多容器 Pod(Init 容器)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
initContainers:
- name: init-step
image: busybox:1.36
command: ['sh', '-c', 'echo "Init 容器执行完毕" && sleep 3']
containers:
- name: main
image: busybox:1.36
command: ['sh', '-c', 'echo "主容器启动!" && sleep 3600']
EOF
kubectl logs init-demo -c init-step # 看 Init 容器的日志
kubectl logs init-demo # 看主容器日志
kubectl delete pod init-demo
十四、总结
一条 kubectl apply -f pod.yaml,背后是 6 大组件在接力:
你 → kubectl → API Server(三道门) → etcd(持久化)
→ Scheduler(调度) → Kubelet(创建容器) → CNI(分配网络)
→ Service(对外入口) → Controller Manager(自愈)
📌 本章必记住的 7 件事
- 容器 ≠ 虚拟机——容器 = 普通进程 + namespace(隔离)+ cgroup(限额)
- Pod 是 K8s 最小调度单位——同一 Pod 内容器共享网络(同一 IP)、存储、生命周期
- Pause 容器 = Pod 的"骨架"——持有 Network Namespace,保证 IP 不随容器重启而变
- Liveness vs Readiness:前者失败→重启容器,后者失败→摘流不重启
- SIGTERM vs SIGKILL:前者可捕获做优雅退出,后者直接强杀
- Exit Code 137 = OOMKilled——查
kubectl logs --previous找不到日志时就查这个 - 生产环境永远用 Deployment 管理 Pod——不要直接创建裸 Pod
这篇文章是 K8s 学习系列中 Pod 的全链路深度拆解。后面的文章会逐个深挖:etcd 的 Raft 共识、Scheduler 的预选+优选算法、kube-proxy 的 iptables 规则、Deployment/StatefulSet/DaemonSet 怎么选……
这些,彩妙会在后面的文章中逐个拆解~
本篇到这里就结束了,喜欢文章的小伙伴可以关注一下彩妙,我们下一篇再见~ 👋🐱

更多推荐
所有评论(0)