Files

25 KiB
Raw Permalink Blame History

tags, create time
tags create time
k8s
pod
devops
2026-07-07 15:48

Pod — Kubernetes 最小调度单元

概述

Pod 是 Kubernetes 中最小的可部署和调度单元。它不是一个容器,而是一组(一个或多个)共享网络和存储资源的容器的逻辑封装。为什么 Kubernetes 不直接调度容器,而是引入了 Pod 这一抽象层?因为在实际业务中,多个紧密协作的进程往往需要共享同一个 IP 地址、端口空间和卷挂载——比如一个 Web 应用容器和一个负责收集日志的 sidecar 容器。Pod 正是为了解决这种「亲密耦合进程协同运行」的需求而设计的。理解 Pod,是掌握 Kubernetes 一切上层抽象(Deployment、StatefulSet、DaemonSet 等)的基础。

Pod 的本质

共享网络命名空间

同一个 Pod 内的所有容器共享同一个网络命名空间(Network Namespace),这意味着:

  • 它们拥有相同的 IP 地址
  • 它们可以通过 localhost 互相通信
  • 端口空间是共享的,因此同一 Pod 内的容器不能绑定相同的端口

[!question] 为什么不直接调度容器? 因为单个容器通常只运行一个进程(这是容器设计的最佳实践)。但在很多场景下,多个进程需要紧密协作——比如 Nginx + 日志收集器、主应用 + 配置热加载 sidecar。如果让调度器去理解「哪些容器应该放在一起」,复杂度会急剧上升。Pod 将这个决策交给了用户:你来决定哪些容器属于同一个 Pod。

Pause 容器 — Pod 的「灵魂」

每个 Kubernetes Pod 启动时,都会先创建一个特殊的Pause 容器(也叫 infra 容器),它是 Pod 的第一个容器,也是整个 Pod 的「根进程」。其他业务容器通过加入 Pause 容器的 Network Namespace 和 PID Namespace 来实现共享。

graph TB
    subgraph Pod
        P["Pause 容器<br/>(infra container)"]
        C1["业务容器 A"]
        C2["业务容器 B<br/>(sidecar)"]
        C3["业务容器 C"]
    end

    P -->|"提供 Network NS"| C1
    P -->|"提供 Network NS"| C2
    P -->|"提供 Network NS"| C3
    P -->|"提供 PID NS (可选)"| C1
    P -->|"提供 PID NS (可选)"| C2
    P -->|"提供 PID NS (可选)"| C3

Pause 容器的职责非常简单:

  1. 持有 Network Namespace — 确保 Pod 的 IP 地址在所有容器生命周期内保持不变
  2. 充当 PID 1 — 回收僵尸进程(zombie reaping)
  3. 极轻量 — 通常只有几百 KB,使用 registry.k8s.io/pause 镜像

[!info] Pause 容器的代码只有几百行 它的核心逻辑就是一个无限循环的 pause() 系统调用。正因为足够简单,它几乎不会出问题,可以稳定地为 Pod 内所有容器提供基础运行环境。

容器与 Pod 的关系(1:N)

一个 Pod 可以包含多个容器,典型模式如下:

模式 说明 示例
单容器 Pod 最常见的场景,一个 Pod 运行一个应用容器 一个 Go Web 服务
多容器 Pod (sidecar) 主容器 + 辅助容器,辅助容器扩展主容器功能 Nginx + Filebeat 日志收集
多容器 Pod (adapter) 辅助容器将主容器的输出转换为标准格式 应用日志 → Fluentd 标准格式
多容器 Pod (ambassador) 辅助容器代理网络请求 应用 → Envoy Proxy → 外部服务
# 一个多容器 Pod 的简单示例:Web 服务 + 日志收集 sidecar
apiVersion: v1
kind: Pod
metadata:
  name: web-with-logging
spec:
  containers:
    - name: web
      image: nginx:1.25
      ports:
        - containerPort: 80
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/nginx
    - name: log-collector
      image: fluent/fluent-bit:latest
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/nginx
          readOnly: true
  volumes:
    - name: log-volume
      emptyDir: {}

两个容器共享 log-volume 卷,Web 容器写入日志,Fluent Bit 容器读取并转发——这是经典的日志收集 sidecar 模式。

Pod 生命周期

Pod 的生命周期由**阶段(Phase)和条件(Condition)**共同描述。从创建到销毁,Pod 经历以下主要阶段:

stateDiagram-v2
    [*] --> Pending: kubectl apply / API 创建
    Pending --> Running: 至少一个容器启动成功
    Pending --> Failed: 拉取镜像失败 / 调度失败
    Running --> Succeeded: 所有容器正常退出(exit 0)
    Running --> Failed: 容器异常退出 / OOMKilled
    Running --> Unknown: Node 失联, 无法获取状态
    Succeeded --> [*]
    Failed --> [*]
    Unknown --> Running: Node 恢复通信
    Unknown --> Failed: 确认 Pod 已丢失

Init Containers

Init Containers 是在主容器启动之前按顺序运行的容器。它们主要用于完成一些前置条件的检查和准备工作。

与主容器的区别:

特性 Init Container 主容器 (App Container)
执行顺序 严格按顺序依次执行 并行启动
失败行为 失败后 Pod 重启(遵循 restartPolicy) 失败后根据探针配置重启
探针支持 不支持 liveness/readiness probe 支持
用途 前置条件检查、等待依赖、初始化配置 运行业务逻辑
资源计算 独立于主容器的资源请求 作为整体调度依据

[!tip] 何时使用 Init Container? 当你需要在主容器启动前完成某些「只执行一次」的任务时——比如等待数据库就绪、下载配置文件、执行数据库迁移等——Init Container 是最佳选择。

apiVersion: v1
kind: Pod
metadata:
  name: app-with-init
spec:
  initContainers:
    # 第一个 Init Container:等待数据库就绪
    - name: wait-for-db
      image: busybox:1.36
      command:
        - sh
        - -c
        - |
          until nc -z mysql-service 3306; do
            echo "Waiting for MySQL..."
            sleep 2
          done
          echo "MySQL is ready!"
    # 第二个 Init Container:执行数据库迁移
    - name: db-migration
      image: myapp/migration:v1.2
      env:
        - name: DB_HOST
          value: "mysql-service"
  containers:
    - name: app
      image: myapp/api:v1.2
      ports:
        - containerPort: 8080

上述示例中,两个 Init Container 严格按顺序执行:先等 MySQL 可达,再执行数据库迁移。只有两者都成功(exit 0)后,主容器 app 才会启动。

容器探针

Kubernetes 提供三种探针来监控容器的健康状态:

探针类型 作用 失败后果 典型场景
livenessProbe 检测容器是否存活 杀死容器并重启 进程死锁、无限循环
readinessProbe 检测容器是否就绪接收流量 从 Service 的 Endpoints 中移除 启动预热、临时过载
startupProbe 检测容器是否启动完成 杀死容器并重启 启动耗时长的应用(如 Java)

[!warning] startupProbe 与 livenessProbe 的关系 当配置了 startupProbe 时,在它成功之前,livenessProbe 和 readinessProbe 都不会执行。这是为了防止启动时间较长的应用被误判为不健康。startupProbe 的 failureThreshold * periodSeconds 应该大于应用的最大启动时间。

探针支持的检测方式:

  • httpGet — 发送 HTTP GET 请求,2xx/3xx 视为成功
  • tcpSocket — 尝试 TCP 连接,端口可达视为成功
  • exec — 执行命令,exit code 0 视为成功
  • grpc — gRPC 健康检查协议(Kubernetes 1.24+)
apiVersion: v1
kind: Pod
metadata:
  name: probes-demo
spec:
  containers:
    - name: app
      image: myapp:v1
      ports:
        - containerPort: 8080

      # 启动探针:最多给 60 秒启动时间 (10 次 x 6 秒)
      startupProbe:
        httpGet:
          path: /healthz
          port: 8080
        failureThreshold: 10
        periodSeconds: 6

      # 存活探针:每 15 秒检测一次,连续 3 次失败则重启
      livenessProbe:
        httpGet:
          path: /healthz
          port: 8080
        initialDelaySeconds: 0   # startupProbe 成功后才开始
        periodSeconds: 15
        failureThreshold: 3
        timeoutSeconds: 5

      # 就绪探针:每 5 秒检测一次,失败则从 Endpoints 移除
      readinessProbe:
        httpGet:
          path: /ready
          port: 8080
        periodSeconds: 5
        failureThreshold: 3
        timeoutSeconds: 3

[!tip] 最佳实践 生产环境中建议同时配置三种探针:startupProbe 保护慢启动应用、livenessProbe 检测死锁、readinessProbe 控制流量切换。readinessProbe 的检测频率可以比 livenessProbe 更高,因为它的失败不会触发重启。

Pod 阶段(Phase)与条件(Condition)

Pod Phase 是对 Pod 在其生命周期中所处位置的高层概括:

Phase 说明
Pending Pod 已被 API Server 接受,但尚未调度或容器镜像未拉取完成
Running Pod 已绑定到节点,且至少有一个容器处于运行状态
Succeeded 所有容器正常退出(exit 0),不会再重启
Failed 所有容器已终止,且至少有一个容器是非正常退出
Unknown 无法获取 Pod 状态,通常是与 Node 的通信中断

Pod Conditions 提供了更细粒度的状态信息,是一个数组,每个 Condition 包含以下字段:

conditions:
  - type: PodScheduled        # 条件类型
    status: "True"            # True / False / Unknown
    lastProbeTime: null       # 上次探测时间
    lastTransitionTime: "2026-07-07T07:00:00Z"  # 上次状态转换时间
    reason: "SchedulerCompletedSchedulingReason"
    message: "Successfully assigned default/my-pod to node-1"

常见的 Condition Type:

Type 说明
PodScheduled Pod 已被调度到某个节点
Initialized 所有 Init Container 已成功完成
ContainersReady 所有容器都已就绪
Ready Pod 可以对外提供服务,被纳入 Service Endpoints
graph LR
    A["PodScheduled<br/>status: True"] --> B["Initialized<br/>status: True"]
    B --> C["ContainersReady<br/>status: True"]
    C --> D["Ready<br/>status: True"]

[!info] Condition vs Phase Phase 是一个全局性的、互斥的状态值(一个 Pod 在同一时刻只有一个 Phase)。而 Conditions 是一组独立的布尔标志,一个 Pod 可以同时有多个 Condition 为 True。Conditions 通常用于精确排查 Pod 启动过程中的具体卡点。

资源管理

Requests 与 Limits

Kubernetes 通过 requests 和 limits 两个维度来管理容器的资源:

  • requests(请求量) — 调度器依据此值决定 Pod 放到哪个节点。是容器运行所需的最小资源量
  • limits(限制量) — 容器可使用的最大资源量。超出限制时的行为取决于资源类型
resources:
  requests:
    cpu: "250m"        # 0.25 核
    memory: "128Mi"    # 128 MiB
  limits:
    cpu: "500m"        # 0.5 核
    memory: "256Mi"    # 256 MiB

[!question] CPU 超限和 Memory 超限的处理方式为什么不同? CPU 是可压缩资源(compressible)——超出 limit 时会被节流(throttle),进程不会被杀死。而内存是不可压缩资源(incompressible)——超出 limit 时进程会被 OOMKilled。这是因为 CPU 可以通过时间片轮转来降级服务,但内存一旦不足,系统无法「回收」已分配的内存页面。

QoS(Quality of Service)三级分类

Kubernetes 根据 requests 和 limits 的配置将 Pod 分为三个 QoS 等级:

QoS 等级 条件 OOM 优先级 调度优先级
Guaranteed 每个容器的 requests == limits 最低(最后被杀) 最高
Burstable 至少一个容器设置了 requests,且 requests < limits 中等 中等
BestEffort 所有容器均未设置 requests 和 limits 最高(最先被杀) 最低
# Guaranteed — requests 等于 limits
containers:
  - name: critical-app
    resources:
      requests:
        cpu: "1"
        memory: "1Gi"
      limits:
        cpu: "1"
        memory: "1Gi"

---
# Burstable — requests 小于 limits
containers:
  - name: normal-app
    resources:
      requests:
        cpu: "250m"
        memory: "128Mi"
      limits:
        cpu: "1"
        memory: "512Mi"

---
# BestEffort — 未设置任何 requests / limits
containers:
  - name: batch-job
    image: busybox
    # 未配置 resources 字段

[!warning] 生产环境严禁 BestEffort BestEffort Pod 在节点资源紧张时会被最先驱逐。在生产环境中,至少应为所有容器设置合理的 requests。

OOM(Out of Memory)行为

当节点内存不足时,kubelet 会按照以下顺序选择 Pod 进行驱逐:

  1. BestEffort Pod(OOM Score 最高)
  2. Burstable Pod 中,实际内存使用量超过 requests 最多的
  3. Guaranteed Pod(OOM Score 最低,最后被杀)

同时,Linux 内核的 OOM Killer 也会根据进程的 oom_score_adj 值来决定杀哪个进程。Kubernetes 为不同 QoS 等级的容器设置了不同的 oom_score_adj:

QoS 等级 oom_score_adj
Guaranteed -998
Burstable 2 ~ 1000(按比例)
BestEffort 1000

CPU 节流机制

当容器的 CPU 使用超过 limit 时,Linux 内核的 CFS(Completely Fair Scheduler)会通过 CFS Bandwidth Throttling 机制限制容器的 CPU 时间片分配。

表现症状:

  • 容器进程运行变慢,响应延迟升高
  • container_cpu_cfs_throttled_periods_total 指标持续增长
  • 应用程序的 P99 延迟出现周期性毛刺

[!tip] 避免 CPU 节流 如果发现应用延迟升高且 CPU throttling 严重,考虑:① 提高 CPU limit;② 将 limit 设置为与 request 相同(Guaranteed QoS,不会被节流但调度更严格);③ 使用 CPU Manager Policy: static 获得独占 CPU 核心。

Sidecar 模式

Sidecar 模式是 Pod 多容器设计中最常用的架构模式。通过在 Pod 中添加辅助容器(sidecar),可以在不修改主应用代码的前提下扩展其功能。

日志收集

apiVersion: v1
kind: Pod
metadata:
  name: app-logging-sidecar
spec:
  containers:
    - name: app
      image: myapp:v2
      volumeMounts:
        - name: shared-logs
          mountPath: /var/log/app
    - name: log-shipper
      image: fluent/fluent-bit:2.2
      volumeMounts:
        - name: shared-logs
          mountPath: /var/log/app
          readOnly: true
        - name: fluentbit-config
          mountPath: /fluent-bit/etc/
      resources:
        requests:
          cpu: "50m"
          memory: "64Mi"
        limits:
          cpu: "200m"
          memory: "128Mi"
  volumes:
    - name: shared-logs
      emptyDir: {}
    - name: fluentbit-config
      configMap:
        name: fluentbit-config

服务网格代理(Service Mesh Proxy)

这是 Istio、Linkerd 等服务网格的典型数据面模式——每个应用 Pod 旁注入一个 Envoy sidecar 代理:

apiVersion: v1
kind: Pod
metadata:
  name: app-with-proxy
  annotations:
    sidecar.istio.io/inject: "true"
spec:
  containers:
    - name: app
      image: myapp:v3
      ports:
        - containerPort: 8080
    # 以下通常由 Istio 自动注入,这里手动展示
    - name: istio-proxy
      image: envoyproxy/envoy:v1.28
      args:
        - proxy
        - sidecar
        - --domain
        - $(POD_NAMESPACE).svc.cluster.local
      ports:
        - containerPort: 15001  # Envoy 入口
        - containerPort: 15006  # Envoy 出口
      env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace

配置热更新(Reloader Sidecar)

某些应用不支持热加载配置文件,此时可以用一个 sidecar 监听 ConfigMap 变化并通知主容器:

apiVersion: v1
kind: Pod
metadata:
  name: app-config-reload
spec:
  containers:
    - name: app
      image: nginx:1.25
      volumeMounts:
        - name: config-volume
          mountPath: /etc/nginx/conf.d
    - name: config-reloader
      image: jimmidyson/configmap-reload:v0.13
      args:
        - --volume-dir=/config
        - --webhook-url=http://localhost:8080/-/reload
      volumeMounts:
        - name: config-volume
          mountPath: /config
          readOnly: true
  volumes:
    - name: config-volume
      configMap:
        name: nginx-config

Pod 调度基础

Kubernetes 调度器(kube-scheduler)负责将 Pod 分配到合适的节点上。Pod 本身可以通过以下字段影响调度决策:

nodeSelector

最简单的调度约束,通过标签匹配将 Pod 调度到特定节点:

apiVersion: v1
kind: Pod
metadata:
  name: gpu-task
spec:
  nodeSelector:
    gpu: "true"       # 必须调度到带有 gpu=true 标签的节点
    disktype: "ssd"
  containers:
    - name: ml-trainer
      image: pytorch:v2

nodeName

直接指定节点名称,跳过调度器:

spec:
  nodeName: node-3   # 直接绑定到 node-3,不经过调度器

[!warning] 慎用 nodeName 使用 nodeName 会绕过调度器的资源检查、亲和性计算和污点容忍判断。如果目标节点资源不足,Pod 会一直处于 Pending 状态。一般仅用于测试或特殊运维场景。

亲和性(Affinity)简介

亲和性提供了比 nodeSelector 更丰富的调度规则:

  • nodeAffinity — 节点亲和性,支持硬性(required)和软性(preferred)规则
  • podAffinity — Pod 亲和性,将 Pod 调度到与其他 Pod 相近的位置
  • podAntiAffinity — Pod 反亲和性,将 Pod 调度到与其他 Pod 远离的位置
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:  # 硬性要求
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/arch
              operator: In
              values:
                - amd64
    preferredDuringSchedulingIgnoredDuringExecution:  # 软性偏好
      - weight: 80
        preference:
          matchExpressions:
            - key: zone
              operator: In
              values:
                - cn-east-1a

[!info] 深入阅读 调度器的完整机制(包括 Taint/Toleration、PriorityClass、拓扑分布约束等)将在 k8s-scheduler 中详细展开。

静态 Pod(Static Pod)

静态 Pod 是由 kubelet 直接管理的 Pod,不通过 API Server 创建。kubelet 会监视指定目录(默认为 /etc/kubernetes/manifests/)中的 YAML 文件,并自动创建和维护对应的 Pod。

特点

特性 静态 Pod 普通 Pod
管理者 kubelet API Server / Controller Manager
存储位置 节点本地文件系统 etcd
是否出现在 API Server 中 是(kubelet 创建 mirror pod) 是
是否受控制器管理 否 是(Deployment、StatefulSet 等)
是否可被 kubectl 删除 否(需删除文件) 是

用途

静态 Pod 最重要的用途是部署控制平面组件:

# kubeadm 部署的集群中,控制平面组件都是静态 Pod
/etc/kubernetes/manifests/
├── kube-apiserver.yaml
├── kube-controller-manager.yaml
├── kube-scheduler.yaml
└── etcd.yaml
# /etc/kubernetes/manifests/kube-apiserver.yaml(简化示例)
apiVersion: v1
kind: Pod
metadata:
  name: kube-apiserver
  namespace: kube-system
  labels:
    component: kube-apiserver
spec:
  containers:
    - name: kube-apiserver
      image: registry.k8s.io/kube-apiserver:v1.30.0
      command:
        - kube-apiserver
        - --advertise-address=192.168.1.10
        - --etcd-servers=https://127.0.0.1:2379
        - --service-cluster-ip-range=10.96.0.0/12
      ports:
        - containerPort: 6443
  hostNetwork: true

[!info] 为什么控制平面用静态 Pod? 这是一个「鸡生蛋」的问题:kube-apiserver 是 Kubernetes 集群的核心,如果 kube-apiserver 本身也依赖 API Server 来创建,那集群就无法启动。静态 Pod 由 kubelet 独立管理,不依赖 API Server,因此可以用来部署 API Server 本身。

常见陷阱与最佳实践

镜像拉取策略(imagePullPolicy)

策略 行为 适用场景
Always 每次创建 Pod 都拉取镜像 使用 :latest 标签时(默认策略)
IfNotPresent 本地不存在时才拉取 使用固定版本标签(如 :v1.2.3)
Never 从不拉取,仅使用本地镜像 离线环境或测试场景
containers:
  - name: app
    image: myapp:v1.2.3      # 使用固定标签
    imagePullPolicy: IfNotPresent  # 推荐:避免不必要的拉取

[!warning] :latest 标签陷阱 当镜像标签为 :latest 或不指定标签时,imagePullPolicy 默认为 Always。这意味着每次 Pod 重建都会重新拉取镜像,带来不确定性。生产环境必须使用固定版本标签(如 :v1.2.3 或 SHA256 digest)。

优雅终止(Graceful Shutdown)

当 Kubernetes 删除一个 Pod 时,会经历以下流程:

sequenceDiagram
    participant User as 用户/API
    participant API as API Server
    participant Kubelet as kubelet
    participant App as 应用进程

    User->>API: 删除 Pod
    API->>Kubelet: 标记 Pod 为 Terminating
    Note over API: 从 Service Endpoints 移除

    Kubelet->>App: 执行 preStop hook
    App-->>App: 执行清理逻辑
    App-->>Kubelet: preStop 完成

    Kubelet->>App: 发送 SIGTERM 信号
    App-->>App: 开始优雅关闭

    alt 在 terminationGracePeriodSeconds 内退出
        App-->>Kubelet: 进程正常退出
    else 超时未退出
        Kubelet->>App: 发送 SIGKILL 强制杀死
    end

关键配置项:

apiVersion: v1
kind: Pod
metadata:
  name: graceful-demo
spec:
  terminationGracePeriodSeconds: 60   # 默认 30s,给足够时间清理
  containers:
    - name: app
      image: myapp:v1
      lifecycle:
        preStop:
          exec:
            command:
              - /bin/sh
              - -c
              - |
                # 1. 通知应用停止接收新请求
                curl -s http://localhost:8080/admin/drain
                # 2. 等待正在进行的请求处理完成
                sleep 15
                # 3. 关闭数据库连接池
                echo "Shutting down gracefully..."

[!tip] preStop 的妙用 preStop hook 在 SIGTERM 之前执行,可以用来做「通知式关闭」——比如先从注册中心注销、停止接收新连接、等待进行中的请求完成。配合 terminationGracePeriodSeconds 调大值,可以实现零请求丢失的优雅关闭。

PID 1 信号问题

在 Linux 中,PID 1 进程有特殊行为:默认不响应 SIGTERM 信号(为了兼容 init 系统)。如果容器的入口进程是 shell 脚本,shell 不会将 SIGTERM 转发给子进程,导致容器无法优雅退出。

问题示例:

# 有问题的 Dockerfile
FROM node:20
COPY server.js .
CMD node server.js     # node 进程成为 PID 1,但不一定正确处理信号

解决方案:

# 方案 1:使用 exec 形式(推荐)
CMD ["node", "server.js"]    # node 直接成为 PID 1

# 方案 2:使用 tini 作为 init 进程
RUN apt-get update && apt-get install -y tini
ENTRYPOINT ["tini", "--"]
CMD ["node", "server.js"]

# 方案 3:在 Pod Spec 中设置(Kubernetes 1.28+)
# 通过 feature gate: SidecarContainers
# 方案 3:Pod 级别的 init 进程设置
apiVersion: v1
kind: Pod
metadata:
  name: signal-demo
spec:
  # Kubernetes 1.28+ 支持设置 Pod 的 PID namespace
  # 确保 PID 1 被正确处理
  containers:
    - name: app
      image: myapp:v1

其他注意事项

  • 不要在同一 Pod 内运行多个不相关的服务 — 这违反了 Pod 的设计初衷,应该拆分为多个 Pod
  • 使用 Liveness Probe 要谨慎 — 错误的 liveness 探针配置会导致容器反复重启(restart loop),建议先用 readiness 验证探针逻辑
  • 设置资源 requests — 不设 requests 的 Pod 会变成 BestEffort QoS,在节点压力下最先被驱逐
  • 避免使用 hostNetwork — 除非必要(如 CNI 插件、Ingress Controller),否则不要使用 hostNetwork,它会占用宿主机端口并绕过 Kubernetes 网络策略

延伸阅读