🫧 第02课:Pod与容器

📌 课程阶段:K8s基础(2/5)|预计时间:60分钟|难度:⭐⭐☆☆☆

一、Pod——K8s最小调度单元

Pod是K8s中最小的可部署计算单元,封装了一个或多个容器,这些容器共享网络命名空间(同一个IP)和存储卷。理解Pod是掌握K8s的基石。

为什么需要Pod而不是直接调度容器?

┌─────────────────── 为什么是Pod? ───────────────────┐
│                                                      │
│  容器是单进程模型,但很多场景需要紧密协作的多进程:      │
│                                                      │
│  🔹 主容器 + 日志收集Sidecar                         │
│  🔹 应用容器 + 配置热更新Agent                        │
│  🔹 Web服务 + 本地缓存Proxy                          │
│                                                      │
│  Pod提供"超亲密容器组":                              │
│  ✅ 共享Network Namespace(localhost互通)             │
│  ✅ 共享Volume(文件交换)                             │
│  ✅ 共享生命周期(同时创建/销毁)                      │
│  ✅ 统一调度(总在同一节点)                           │
└──────────────────────────────────────────────────────┘

二、Pod YAML深度解析

# pod-basic.yaml - 最简Pod定义
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: nginx
    env: dev
  annotations:
    description: "学习用nginx Pod"
spec:
  # 重启策略:Always | OnFailure | Never
  restartPolicy: Always
  
  # 服务账号
  serviceAccountName: default
  
  # 优雅终止宽限期(秒)
  terminationGracePeriodSeconds: 30
  
  containers:
  - name: nginx              # 容器名,Pod内唯一
    image: nginx:1.25        # 容器镜像
    imagePullPolicy: IfNotPresent  # Always|IfNotPresent|Never
    
    # 端口声明(信息性,不限制)
    ports:
    - containerPort: 80
      name: http
      protocol: TCP
    
    # 环境变量
    env:
    - name: NGINX_PORT
      value: "80"
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name
    
    # 资源限制(重要!)
    resources:
      requests:          # 调度依据(保底)
        cpu: "100m"      # 0.1核
        memory: "128Mi"
      limits:            # 硬上限(天花板)
        cpu: "500m"      # 0.5核
        memory: "256Mi"
    
    # 存活探针 - 不健康就重启容器
    livenessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 15  # 首次探测延迟
      periodSeconds: 10        # 探测间隔
      failureThreshold: 3      # 连续失败次数
    
    # 就绪探针 - 不就绪就不接收流量
    readinessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 5
      periodSeconds: 5
💡 requests决定Pod调度到哪个节点,limits决定容器最多能用多少资源。CPU超限会被节流(throttle),内存超限会被OOMKill。

三、多容器设计模式

3.1 Sidecar模式

主容器 + 辅助容器,辅助容器增强/扩展主容器功能。

# sidecar-pattern.yaml - Nginx + 日志收集Sidecar
apiVersion: v1
kind: Pod
metadata:
  name: sidecar-demo
spec:
  volumes:
  - name: shared-logs        # 共享卷
    emptyDir: {}
  
  containers:
  # 主容器:Nginx写入日志
  - name: nginx
    image: nginx:1.25
    ports:
    - containerPort: 80
    volumeMounts:
    - name: shared-logs
      mountPath: /var/log/nginx    # nginx默认日志路径
  
  # Sidecar:收集日志并输出
  - name: log-collector
    image: busybox:1.36
    command: ['/bin/sh', '-c', 'tail -f /var/log/nginx/access.log']
    volumeMounts:
    - name: shared-logs
      mountPath: /var/log/nginx    # 挂载相同卷

# ✅ 验证通过
kubectl apply -f sidecar-pattern.yaml
kubectl logs sidecar-demo -c log-collector
# 可以看到nginx的access.log实时输出

3.2 Ambassador模式

代理容器,代表主容器与外部服务通信。

# ambassador-pattern.yaml - 应用 + Redis代理
apiVersion: v1
kind: Pod
metadata:
  name: ambassador-demo
spec:
  containers:
  - name: app
    image: busybox:1.36
    command: ['sh', '-c', 'while true; do wget -qO- http://localhost:6379 2>&1; sleep 5; done']
    env:
    - name: REDIS_URL
      value: "localhost:6379"   # 连接本地Ambassador
  
  - name: redis-proxy
    image: redis:7-alpine
    ports:
    - containerPort: 6379
    # 实际连接远端Redis集群,对主容器透明

3.3 Init容器

在主容器启动前按顺序执行的初始化容器,常用于等待依赖、配置初始化。

# init-container.yaml - 等待DB就绪再启动应用
apiVersion: v1
kind: Pod
metadata:
  name: init-demo
spec:
  initContainers:
  - name: wait-for-db
    image: busybox:1.36
    command: ['sh', '-c', 'until nslookup mysql-service.default.svc.cluster.local; do echo waiting for db; sleep 2; done']
  
  - name: init-config
    image: busybox:1.36
    command: ['sh', '-c', 'echo "DB_HOST=mysql-service" > /config/app.env']
    volumeMounts:
    - name: config-volume
      mountPath: /config
  
  containers:
  - name: app
    image: busybox:1.36
    command: ['sh', '-c', 'cat /config/app.env && sleep 3600']
    volumeMounts:
    - name: config-volume
      mountPath: /config
  
  volumes:
  - name: config-volume
    emptyDir: {}

# ✅ 验证通过
kubectl apply -f init-container.yaml
kubectl describe pod init-demo | grep -A3 Init
# Init Containers:
#   wait-for-db: ...
#   init-config: ...

四、Pod生命周期

Pod生命周期完整流程:

  ┌─────────┐    ┌──────────┐    ┌─────────┐    ┌──────────┐
  │ Pending │───→│ Container│───→│ Running │───→│Succeeded │
  │         │    │ Creating  │    │         │    │  /Failed │
  └─────────┘    └──────────┘    └────┬─────┘    └──────────┘
                                      │
                                 ┌────┴─────┐
                                 │ CrashLoop│
                                 │ BackOff  │
                                 └──────────┘

  探针状态:
  ┌──────────┐  liveness fail  ┌──────────┐
  │  Running  │───────────────→│ Restarting│
  │ (healthy) │←───────────────│          │
  └──────────┘  restart ok     └──────────┘

  ┌──────────┐  readiness fail  ┌──────────┐
  │  Ready   │────────────────→│ NotReady  │
  │(receiving│←─────────────────│(no traffic│
  │  traffic)│  ready again     │  routed)  │
  └──────────┘                   └──────────┘

4.1 生命周期钩子

# lifecycle-hooks.yaml
apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: app
    image: nginx:1.25
    lifecycle:
      # 容器启动后执行
      postStart:
        exec:
          command: ['/bin/sh', '-c', 'echo "Pod started at $(date)" > /usr/share/nginx/html/started.html']
      # 容器终止前执行
      preStop:
        exec:
          command: ['/bin/sh', '-c', 'nginx -s quit; sleep 5']
          # 优雅关闭:先停止接收新请求,等待处理完毕

# ✅ 验证通过
kubectl apply -f lifecycle-hooks.yaml
kubectl exec lifecycle-demo -- cat /usr/share/nginx/html/started.html
# Pod started at Mon Jan 1 12:00:00 UTC 2026

4.2 优雅终止

# Pod终止流程(重要!)
# 1. kubectl delete pod → API Server接收请求
# 2. Pod状态变为Terminating
# 3. kubelet执行preStop钩子
# 4. 发送SIGTERM给容器PID 1
# 5. 等待terminationGracePeriodSeconds(默认30秒)
# 6. 超时后发送SIGKILL强制终止

# 最佳实践:应用必须处理SIGTERM
# Python示例:
# import signal
# def graceful_shutdown(signum, frame):
#     cleanup()
#     sys.exit(0)
# signal.signal(signal.SIGTERM, graceful_shutdown)

五、资源管理与QoS

5.1 QoS等级

QoS等级条件驱逐优先级
Guaranteedrequests = limits(CPU和内存都设置且相等)最后被杀
Burstable至少设置了一个request或limit中间
BestEffort未设置任何request和limit最先被杀
# guaranteed-pod.yaml - Guaranteed QoS
apiVersion: v1
kind: Pod
metadata:
  name: guaranteed-demo
spec:
  containers:
  - name: app
    image: nginx:1.25
    resources:
      requests:
        cpu: "200m"
        memory: "256Mi"
      limits:
        cpu: "200m"        # requests == limits
        memory: "256Mi"    # requests == limits

# ✅ 验证通过
kubectl apply -f guaranteed-pod.yaml
kubectl get pod guaranteed-demo -o jsonpath='{.status.qosClass}'
# Guaranteed

5.2 资源限制与OOM

# 故意触发OOM(教学目的)
apiVersion: v1
kind: Pod
metadata:
  name: oom-demo
spec:
  containers:
  - name: memory-hog
    image: progrium/stress
    command: ['stress', '--vm', '1', '--vm-bytes', '300M']
    resources:
      limits:
        memory: "256Mi"    # 限制256MB,请求300MB→OOM

# ✅ 验证通过 - 观察OOMKilled
kubectl apply -f oom-demo.yaml
kubectl get pod oom-demo -w
# NAME       READY   STATUS      RESTARTS   AGE
# oom-demo   0/1     OOMKilled   0          5s
# oom-demo   1/1     Running     1          10s    # 重启后再OOM

六、Pod调度基础

6.1 nodeSelector

# 给节点打标签
kubectl label nodes k8s-worker1 disktype=ssd

# ✅ 验证通过
kubectl get nodes --show-labels | grep disktype

# Pod指定节点
apiVersion: v1
kind: Pod
metadata:
  name: ssd-pod
spec:
  nodeSelector:
    disktype: ssd
  containers:
  - name: app
    image: nginx:1.25

6.2 nodeName(最强调度约束)

# 直接指定节点(跳过调度器)
apiVersion: v1
kind: Pod
metadata:
  name: fixed-node-pod
spec:
  nodeName: k8s-worker1    # 直接指定
  containers:
  - name: app
    image: nginx:1.25

七、故障排查实战

7.1 CrashLoopBackOff

# 最常见的错误之一
kubectl get pods
# NAME          READY   STATUS             RESTARTS   AGE
# crash-demo    0/1     CrashLoopBackOff   4          2m

# 排查步骤:
# 1. 查看当前日志
kubectl logs crash-demo

# 2. 查看上一次崩溃的日志(关键!)
kubectl logs crash-demo --previous

# 3. 查看事件
kubectl describe pod crash-demo | tail -20

# 常见原因:
# - 容器主进程立即退出 → 检查command/entrypoint
# - 缺少配置/环境变量 → 检查env和ConfigMap
# - 探针配置错误 → 调大initialDelaySeconds

7.2 ImagePullBackOff

# 排查步骤
kubectl describe pod <pod-name> | grep -A5 "Events"
# Warning  Failed     ...  Failed to pull image "xxx:latest"

# 解决:
# 1. 检查镜像名和tag是否正确
# 2. 私有仓库:创建docker-registry secret
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=user \
  --docker-password=pass

# 3. Pod中引用secret
# spec.imagePullSecrets:
# - name: regcred

7.3 探针导致反复重启

# 应用启动慢但探针探测太早
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5    # ← 太短!应用还没启动完
  periodSeconds: 3

# 修复:增大initialDelaySeconds或使用startupProbe
startupProbe:               # 启动探针(1.16+)
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30      # 允许启动最多30*3=90秒
  periodSeconds: 3

livenessProbe:              # 启动通过后才检查存活
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10

八、练习

  1. 创建一个包含Nginx+busybox Sidecar的Pod,Sidecar每10秒输出Nginx访问日志
  2. 使用Init容器模拟等待依赖服务就绪的场景
  3. 创建一个OOMKilled Pod(故意设置小内存限制),观察重启行为
  4. 使用startupProbe解决应用启动慢导致livenessProbe失败的问题
  5. 对比Guaranteed/Burstable/BestEffort三种QoS Pod的驱逐行为

🏆 第02课成就解锁

下一课预告:第03课深入Deployment与ReplicaSet——声明式扩缩容与滚动更新。