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 内的容器能用 localhost 互通?为什么需要 Pause 容器?这些都不是 K8s 发明的魔法——只是 Linux 内核提供的两个老功能:namespacecgroup

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 中对应哪个字段
cpuCPU 使用配额resources.limits.cpu
memory内存上限resources.limits.memory
blkio磁盘 I/O 速度CSI storageIO 限制
pids进程数上限kubelet --pod-max-pids 参数

1.4 容器 vs 虚拟机——一道面试必考题

容器

虚拟机

应用

依赖库

Guest OS(完整系统,几GB)

Hypervisor 虚拟化层

应用

依赖库

容器引擎(containerd)

宿主机 Linux 内核(共享)

物理硬件

维度虚拟机容器
启动速度分钟级秒级
资源占用GB 级(每 VM 一套完整 OS)MB 级(只打包应用+依赖)
隔离程度硬件级(独立内核,更强)进程级(共享内核)
单机可运行数几十个成百上千个
典型镜像大小几 GB几十 MB

二、为什么容器不够,还要发明 Pod?

2.1 问题:有些进程天生就该"贴在一起"

想象你要部署一个 Web 服务 + 日志采集器:

方案 A:两个独立容器(不用 Pod)

❌ 文件系统隔离,互相看不到

❌ 走 TCP/IP 网络,有延迟

日志采集容器 IP: 172.17.0.6

读日志?怎么读到对方的文件?

Web 容器 IP: 172.17.0.5

写日志到 /var/log/app.log

❌ 问题 1:日志采集器怎么读到 Web 容器的文件?——文件系统隔离,互相看不到!
❌ 问题 2:怎么通信?——只能走网络(172.17.0.5 → 172.17.0.6),有延迟
❌ 问题 3:怎么保证命运与共?——Web 挂了,日志采集器还傻傻地跑着

方案 B:放到同一个 Pod 里(K8s 的做法)

Pod 'web-pod' / 唯一 IP: 10.244.1.5

localhost 直连(零延迟)

日志采集容器

读 /var/log/app

📁 共享 Volume

Web 容器

写日志到 /var/log/app

✅ 共享 Volume → 日志采集器直接读 Web 的日志
✅ 共享 Network Namespace → 同一 IP,localhost 直连,零延迟
✅ 共享生命周期 → 一起创建一起销毁
✅ 共享 IPC → 可用信号量和共享内存通信

2.2 Pod 的定义

🎯 一句话:Pod 是 K8s 中最小、最简单的部署单元。 一个 Pod 可以包含一个或多个容器,这些容器共享网络、存储和生命周期。

📦 Pod(一个逻辑主机)

🌐 共享 Network NS → 同一 IP + 端口空间

🐳 容器 A

🐳 容器 B

🐳 容器 C

📁 共享 Volume → 同一存储目录

💬 共享 IPC → 信号量/共享内存

🎯 人话版: 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 容器是什么?

Pod 'my-pod' / IP: 10.244.1.23

加入 Network NS → 共享 IP

加入 Network NS → 共享 IP

日志采集容器

采集日志

🔧 Pause 容器(最先启动,永久睡眠)

创建 Network Namespace

创建 IPC Namespace

持有 Pod IP 地址

回收僵尸子进程

镜像仅 ~700KB

Nginx 容器

端口 :80

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 全景图

① kubectl
提交 YAML

② Scheduler
选机器

③ Kubelet
创建容器

④ CNI
分配网络

⑤ Service
注册入口

⑥ Controller
持续自愈

⑦ 删除
优雅终止

5.2 阶段①:kubectl apply → API Server

证书/Token 验证通过

RBAC 检查通过

策略检查通过

📨 kubectl 发来的
HTTPS POST 请求

🔐 认证:你是谁?

🔑 授权:有权限吗?

🛂 准入控制:合规吗?

✅ 写入 etcd
状态标记为 Pending

<注意:准入控制是很关键的一步——Istio 就是利用 MutatingAdmissionWebhook 在 Pod 提交时自动注入 Envoy Sidecar 容器。这也是后面 Service Mesh 章节的重点。>

5.3 阶段②:Scheduler 分配节点

❌ CPU 不够

❌ 内存不够

❌ Pod 没容忍

Pod 要求:2核CPU + 4GB 内存

Node-1: 剩余 3核+6GB

候选列表

Node-2: 剩余 0.5核

淘汰

Node-3: 剩余 4核+2GB

淘汰

Node-4: 有 GPU 污点

淘汰

🎯 人话版: Scheduler 就像房产中介——先过滤掉不满足要求的(海选淘汰),再从剩下的里面挑最合适的(决赛打分),最高分的就选它。

5.4 阶段③:Kubelet 创建容器

没有

Kubelet Watch 到 Pod 分配给自己

通过 CRI(gRPC) 调用 containerd

本地有 nginx 镜像?

去 Registry 拉镜像
分层下载,只拉本地没有的层

直接创建容器

containerd-shim 守护进程

runc 真正创建容器

namespace 隔离 + cgroup 限制

执行 ENTRYPOINT

🎉 容器启动成功!

<注意:K8s 1.24 之后默认不再使用 Docker 作为容器运行时,而是直接使用 containerd。调用链上少了 Docker Engine 这一层。>


六、Pod 生命周期·中——网络、Service、健康检查

6.1 阶段④:CNI 分配网络

宿主机

veth pair

veth pair

📡 cni0 网桥

🌐 eth0 物理网卡

Pod A
10.42.0.5

Pod B
10.42.0.6

外部网络

CNI 做三件事:① 分配 IP ② 创建 veth pair(虚拟网线)③ 配路由

🍵 友情小知识: Flannel、Calico 都是 CNI 的具体实现——就像不同品牌的交换机,功能一样,实现细节不同。

6.2 阶段⑤:Service 代理

Pod IP 会变(Pod 死了重建 IP 就变了),Service 解决这个问题——给一组 Pod 一个固定的"电话号码"(ClusterIP)。

📞 请求 → 10.96.0.1:80

kube-proxy
查 iptables/IPVS 规则

Pod A 10.42.0.5 ✅

Pod B 10.42.0.6 ✅

Pod C 10.42.0.7 ❌
Readiness 探针没过

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
ExternalNameDNS 别名映射外部服务

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?

② 对比:和期望一样吗?

③ 行动:少的创建/多的删除

④ 等待:过一会儿再检查


七、Pod 生命周期·下——删除、优雅终止、自愈

容器进程 Pod Service API Server 👤 你 容器进程 Pod Service API Server 👤 你 应用处理完手头请求后优雅退出 === 30 秒到了还没退出 === kubectl delete pod xxx 打 deletionTimestamp + 启动 30s 倒计时 立刻从 Endpoints 移除 执行 PreStop Hook(如果有) 发送 SIGTERM(信号 15) SIGKILL(信号 9)强杀 💀

<注意: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 件事

  1. 容器 ≠ 虚拟机——容器 = 普通进程 + namespace(隔离)+ cgroup(限额)
  2. Pod 是 K8s 最小调度单位——同一 Pod 内容器共享网络(同一 IP)、存储、生命周期
  3. Pause 容器 = Pod 的"骨架"——持有 Network Namespace,保证 IP 不随容器重启而变
  4. Liveness vs Readiness:前者失败→重启容器,后者失败→摘流不重启
  5. SIGTERM vs SIGKILL:前者可捕获做优雅退出,后者直接强杀
  6. Exit Code 137 = OOMKilled——查 kubectl logs --previous 找不到日志时就查这个
  7. 生产环境永远用 Deployment 管理 Pod——不要直接创建裸 Pod

这篇文章是 K8s 学习系列中 Pod 的全链路深度拆解。后面的文章会逐个深挖:etcd 的 Raft 共识、Scheduler 的预选+优选算法、kube-proxy 的 iptables 规则、Deployment/StatefulSet/DaemonSet 怎么选……

这些,彩妙会在后面的文章中逐个拆解~

本篇到这里就结束了,喜欢文章的小伙伴可以关注一下彩妙,我们下一篇再见~ 👋🐱

在这里插入图片描述

更多推荐