Skip to content

Docker 与 Kubernetes 深度解析 ​

#组件 · #容器 · #Docker · #Kubernetes · #编排

Docker 定义了容器的标准,Kubernetes 让容器从"玩具"变成"生产级基础设施"。本节从 Docker 底层原理出发,深入 K8s 核心抽象与调度机制,建立容器编排的完整认知。


一、Docker 底层原理 ​

1.1 容器 vs 虚拟机 ​

理解容器必须先理解它"不是什么"——容器不是轻量级虚拟机。虚拟机虚拟硬件(Hypervisor 模拟 CPU/内存/磁盘),每个 VM 运行完整 Guest OS;容器只是被 namespace 隔离、被 cgroup 限制的普通 Linux 进程。这一根本差异决定了容器的一切优势:

mermaid
flowchart TB
    subgraph VM["虚拟机"]
        VM_App1["App 1"]
        VM_App2["App 2"]
        VM_Bins["Bins/Libs"]
        VM_Guest["Guest OS"]
        VM_Hyper["Hypervisor"]
        VM_Host["Host OS"]
        VM_Infra["Infrastructure"]
        VM_App1 --- VM_Bins --- VM_Guest --- VM_Hyper --- VM_Host --- VM_Infra
        VM_App2 --- VM_Bins
    end

    subgraph Container["容器"]
        C_App1["App 1"]
        C_App2["App 2"]
        C_Bins["Bins/Libs"]
        C_Engine["Container Engine (Docker)"]
        C_Host["Host OS"]
        C_Infra["Infrastructure"]
        C_App1 --- C_Bins --- C_Engine --- C_Host --- C_Infra
        C_App2 --- C_Bins
    end
维度虚拟机容器
隔离级别硬件级(Hypervisor)进程级(namespace + cgroup)
启动速度分钟级秒级
镜像大小GB 级MB 级
资源开销每 VM 一套完整 OS共享宿主机内核
密度单机 ≤ 几十个单机可数百个
跨平台✅ 完全隔离⚠️ 共享内核,Linux → Linux

1.2 Docker 镜像 — UnionFS 分层 ​

Docker 镜像的"轻量"来源于分层复用——不是每个容器拷一份完整文件系统,而是多个容器共享相同的只读底层,只有各自的修改写入独立的薄层。这是 Linux UnionFS(联合文件系统)的思想,Docker 用 overlay2 驱动实现:

mermaid
flowchart TB
    subgraph Layers["镜像分层结构"]
        direction TB
        RW["Container Layer (R/W)<br/>容器运行时的所有写操作"]
        L4["Image Layer: app.jar<br/>应用代码 (10MB)"]
        L3["Image Layer: pip install<br/>Python 依赖 (200MB)"]
        L2["Image Layer: apt-get install<br/>系统工具 (50MB)"]
        L1["Image Layer: FROM ubuntu:22.04<br/>基础系统 (77MB)"]
    end

    RW --> L4 --> L3 --> L2 --> L1

    View["联合挂载视图<br/>LowerDir=Layer1:4, UpperDir=Container, Merged=统一视图<br/>读: 从顶层向下搜索 | 写: 只写顶层(CoW)"]
    Layers --> View

核心机制:

bash
# 查看镜像层
docker image inspect nginx:latest | jq '.[0].RootFS.Layers'

# overlay2 挂载示例
# LowerDir: l1/l2/l3/l4 (只读层, 用 : 分隔)
# UpperDir: diff (容器可写层)
# Merged: 合并视图
# WorkDir: work (overlay2 内部工作目录)

写时复制 (Copy-on-Write):容器要修改底层文件时,先将文件从 LowerDir 复制到 UpperDir,再在 UpperDir 中修改。原始底层文件不受影响,所有容器共享同一份只读层。

1.3 Dockerfile 多阶段构建 ​

dockerfile
# 阶段 1: 编译
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app .

# 阶段 2: 运行(仅 13MB 的 scratch 镜像)
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /build/app /app
EXPOSE 8080
USER nobody
ENTRYPOINT ["/app"]

为什么用多阶段构建:编译依赖(如 Go SDK ~500MB)只在第一阶段存在,最终镜像只包含运行时文件和基础系统。镜像从 ~800MB 缩减到 ~20MB。


二、Kubernetes 核心抽象 ​

Kubernetes 的核心哲学是声明式管理——你告诉 K8s"我想要 3 个副本、每个需要 512MB 内存",而不是"在 Node1 上启动一个进程"。K8s 的 Controller Manager 持续监控实际状态与期望状态的差异,自动修复。理解这一范式是理解所有 K8s 抽象的前提。

2.1 架构全景 ​

Master 节点(Control Plane)负责全局决策,Worker 节点负责执行。两者通过 API Server 解耦——这也是 K8s 所有操作的唯一入口:

mermaid
flowchart TB
    subgraph CP["Control Plane (Master)"]
        API["API Server<br/>RESTful API, 唯一入口"]
        ETCD["etcd<br/>集群状态存储"]
        Sched["Scheduler<br/>Pod 调度决策"]
        CM["Controller Manager<br/>状态协调循环"]
    end

    API --> ETCD
    API <--> Sched
    API <--> CM

    subgraph W1["Worker Node 1"]
        K1["kubelet<br/>Pod 生命周期管理"]
        P1["kube-proxy<br/>网络规则"]
        Pods1["Pods..."]
    end

    subgraph W2["Worker Node 2"]
        K2["kubelet"]
        P2["kube-proxy"]
        Pods2["Pods..."]
    end

    API <-->|"Watch 机制"| K1
    API <-->|"Watch 机制"| K2
    K1 --> Pods1
    K2 --> Pods2

关键设计原则:

组件职责工作原理
API Server集群唯一入口,RESTful CRUD认证→鉴权→准入→写入 etcd
etcd集群所有状态存储Raft 共识,强一致性 KV
Scheduler决定 Pod 跑在哪个 Node过滤(预选)→打分(优选)→绑定
Controller Manager确保实际状态=期望状态控制循环:Watch→Compare→Act
kubeletNode 上的"工头"Watch API Server → 创建/销毁 Pod
kube-proxyNode 上的网络代理iptables/IPVS 规则维护 Service 负载均衡

2.2 Pod — 最小调度单元 ​

Pod 是 K8s 中最小、最简单的调度单元——它不是"容器之上的抽象",而是一组共享 namespace 的容器的集合。为什么需要 Pod 而不是直接调度容器?因为很多场景需要多个紧密协作的进程共享网络和存储(如应用 + 日志采集 sidecar),Pod 为这种模式提供了原生支持。

Pod 中的每个容器共享相同的 network namespace(通过 Pause 容器实现的"地基"),可以通过 localhost 互相通信。

mermaid
flowchart TB
    subgraph Pod["Pod (共享 NET + IPC + UTS namespace)"]
        subgraph Pause["Pause 容器 (基础设施)"]
            NS["持有 NET/IPC/UTS namespace<br/>生命周期 = Pod 生命周期"]
        end
        C1["App Container<br/>业务进程"]
        C2["Sidecar Container<br/>日志收集/代理"]
    end

    Volume["共享 Volume"]
    C1 --- Volume
    C2 --- Volume

    Pause --> C1
    Pause --> C2

    Localhost["localhost 互通<br/>容器间通过 localhost 通信"]
    C1 --- Localhost
    C2 --- Localhost

关键理解:Pause 容器是每个 Pod 的"地基"。它创建并持有 network namespace,Pod 内其他容器加入这个 namespace。Pause 不干任何事(sleep),但它的生命周期决定了整个 Pod 的生命周期。

2.3 Service — 稳定的访问入口 ​

Pod 是临时的——重启后 IP 会变,滚动更新时新旧 Pod 共存,扩容缩容时后端集合动态变化。如果直接通过 Pod IP 通信,每次变化都需要重新发现。Service 解决了这个问题:它提供一个不变的虚拟 IP(ClusterIP)和 DNS 名称,自动将请求负载均衡到匹配 Label 的健康 Pod:

mermaid
flowchart TB
    Client["Client Pod<br/>访问 my-svc:80"] -->|"DNS: my-svc → 10.96.0.10"| SVC["Service<br/>ClusterIP: 10.96.0.10<br/>Port: 80→8080"]

    SVC -->|"kube-proxy<br/>iptables/IPVS 负载均衡"| P1["Pod A<br/>10.244.1.5:8080<br/>Labels: app=myapp"]
    SVC -->|"kube-proxy<br/>iptables/IPVS 负载均衡"| P2["Pod B<br/>10.244.2.8:8080<br/>Labels: app=myapp"]
    SVC -->|"kube-proxy<br/>iptables/IPVS 负载均衡"| P3["Pod C<br/>10.244.3.3:8080<br/>Labels: app=myapp"]

    EP["Endpoints<br/>{10.244.1.5:8080, 10.244.2.8:8080, 10.244.3.3:8080}"]
    SVC --> EP

四层 Service 类型:

类型访问范围实现用途
ClusterIP集群内部kube-proxy iptables/IPVS内部服务间通信
NodePortNodeIP:30000-32767每 Node 开端口 + ClusterIP外部测试/简单接入
LoadBalancer外部 LB IP云厂商 LB + NodePort生产外部入口
ExternalNameDNS CNAME仅 DNS 层面外部服务映射

2.4 Deployment — 声明式副本管理 ​

如果直接创建 Pod,Pod 挂了就没了——没人会帮你重启。Deployment 是更高级的抽象:你声明"我想要 3 个 nginx Pod",Deployment 创建 ReplicaSet,ReplicaSet 确保 Pod 数量始终等于期望值。滚动更新时,Deployment 会创建新的 ReplicaSet、逐步替换旧 Pod——这个过程完全自动化,你只需要改镜像版本号:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3                    # 期望副本数
  strategy:
    type: RollingUpdate          # 滚动更新策略
    rollingUpdate:
      maxSurge: 1                # 更新过程中最多多出 1 个 Pod
      maxUnavailable: 1          # 最多 1 个 Pod 不可用
  selector:
    matchLabels:
      app: my-app
  template:                      # Pod 模板
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: my-app:v2
        readinessProbe:           # 就绪探针(决定能否接入流量)
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
        livenessProbe:            # 存活探针(决定是否重启)
          httpGet:
            path: /live
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10

Deployment → ReplicaSet → Pod 的层级控制:

mermaid
flowchart LR
    Deploy["Deployment<br/>期望: 3 副本, 滚动更新"] -->|"管理"| RS1["ReplicaSet v1<br/>当前: 2 Pods"]
    Deploy -->|"管理"| RS2["ReplicaSet v2<br/>当前: 2 Pods"]
    RS1 --> P1["Pod (v1)"]
    RS1 --> P2["Pod (v1)"]
    RS2 --> P3["Pod (v2)"]
    RS2 --> P4["Pod (v2)"]

滚动更新过程:新 RS 逐个创建新版本 Pod → 新 Pod Ready → 旧 RS 逐个删除旧版本 Pod。maxSurge=1, maxUnavailable=0 可实现无损更新(先建后删)。

2.5 Ingress — L7 路由 ​

mermaid
flowchart TB
    Client["外部用户<br/>api.example.com"] --> LB["LoadBalancer<br/>80/443"]

    LB --> Ing["Ingress Controller<br/>Nginx Ingress / Traefik"]

    Ing -->|"Host: api.example.com<br/>Path: /users"| SVC1["User Service<br/>ClusterIP"]
    Ing -->|"Host: api.example.com<br/>Path: /orders"| SVC2["Order Service<br/>ClusterIP"]
    Ing -->|"Host: admin.example.com"| SVC3["Admin Service<br/>ClusterIP"]

三、Kubernetes 调度 ​

3.1 调度流程 ​

mermaid
flowchart TB
    NewPod["新 Pod (nodeName 为空)"] --> Filter["预选 (Filtering/Predicates)<br/>过滤不满足条件的 Node"]
    Filter --> Score["优选 (Scoring/Priorities)<br/>对剩余 Node 打分"]
    Score --> Select["选出最高分 Node"]
    Select --> Bind["绑定 Pod 到 Node<br/>写入 Pod.Spec.NodeName"]

    subgraph Filters["预选检查"]
        CPU["CPU/内存资源是否够"]
        Port["HostPort 是否冲突"]
        Volume["PV/PVC 是否可挂载"]
        Affinity["NodeSelector/Affinity 匹配"]
        Taint["Node Taint 是否容忍"]
    end

    Filter --> Filters

3.2 资源管理 ​

资源管理模型:
  Requests: 调度时使用的最小保证(决定 Pod 落在哪个 Node)
  Limits:   运行时的硬限制(超过即 throttle 或 OOM Kill)

QoS 等级 (OOM 被杀优先级从高到低):
  BestEffort:  没设 requests 和 limits → 最先被杀
  Burstable:   requests < limits → 中优先级
  Guaranteed:  requests == limits (CPU+Mem 都设且相等) → 最后被杀

Pod 调度亲和性:

yaml
spec:
  affinity:
    podAffinity:            # Pod 亲和:跟某些 Pod 部署在一起
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: cache
        topologyKey: "kubernetes.io/hostname"
    podAntiAffinity:        # Pod 反亲和:彼此分开(高可用必备)
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: my-app
          topologyKey: "kubernetes.io/hostname"
  tolerations:              # 容忍污点
  - key: "node-type"
    operator: "Equal"
    value: "gpu"
    effect: "NoSchedule"

四、Kubernetes 网络模型 ​

4.1 核心原则 ​

K8s 网络模型的核心约定:

  • 所有 Pod 共享一个扁平网络(Pod 间直接通信,无需 NAT)
  • 每个 Pod 有唯一 IP
  • Node 可以与所有 Pod 通信

4.2 跨 Node 通信路径 ​

mermaid
flowchart TB
    subgraph Node1["Node 1 (10.0.0.1)"]
        P1["Pod A<br/>10.244.1.5"]
        V1["veth pair"]
        B1["cni0 Bridge<br/>10.244.1.1"]
        P1 --- V1 --- B1
    end

    subgraph Node2["Node 2 (10.0.0.2)"]
        P2["Pod B<br/>10.244.2.8"]
        V2["veth pair"]
        B2["cni0 Bridge<br/>10.244.2.1"]
        P2 --- V2 --- B2
    end

    B1 -->|"路由: 10.244.2.0/24 → Node2<br/>封装方式取决于 CNI 插件"| Tunnel["VXLAN/IPIP/BGP"]
    Tunnel --> B2

五、常见故障排查 ​

5.1 诊断命令速查 ​

bash
# Pod 状态
kubectl describe pod <name>               # 查看 Events(最常用)
kubectl logs <pod> -c <container> --tail=100 -f  # 查看日志

# 进入容器
kubectl exec -it <pod> -- /bin/sh         # 调试

# 网络诊断
kubectl run tmp --image=busybox --rm -it -- wget -O- http://svc:port
kubectl get endpoints <svc>               # Service 后端 Pod IP 列表

# 资源
kubectl top nodes                         # Node 资源使用
kubectl top pods -A                       # 所有 Pod 资源

# 调度问题
kubectl get events --sort-by='.lastTimestamp' | grep -i fail

5.2 经典问题定位 ​

问题排查路径
Pod CrashLoopBackOffkubectl logs 看启动日志 → kubectl describe 看 Events → 检查 OOMKilled?
Pod PendingEvents 看调度失败原因(资源不足/亲和性不满足/Volume 挂载失败)
Service 不生效kubectl get endpoints 看是否有后端 Pod → 检查 selector 匹配 → 检查 Pod readinessProbe
网络不通进入容器 nc -zv svc port → 检查 NetworkPolicy → 检查 CNI 插件状态

6. 容器运行时对应用的影响深度专题 ​

6.1 OverlayFS COW 的写放大 ​

容器镜像使用 OverlayFS 分层存储,但这引入了 COW(Copy-on-Write)写放大:

mermaid
flowchart TD
    subgraph "OverlayFS 分层"
        R["Container Layer (可写)<br/>Upper Dir"]
        I3["Image Layer 3: 应用"]
        I2["Image Layer 2: 运行时"]
        I1["Image Layer 1: 基础系统"]

        R --> I3 --> I2 --> I1
    end

    subgraph "写入时发生"
        W["应用写 /app/data/log"] --> C1{"该文件在 Lower Layer?"}
        C1 -->|"是"| CP["COW: 复制到 Upper Layer<br/>→ 写放大! 一次写入变两次IO"]
        C1 -->|"否,已在上层"| DW["直接写入"]
    end

如何减少 COW 写放大:

策略命令/配置说明
Volume 挂载VOLUME /data 或 bind mount高频写入目录直接绕过 OverlayFS
tmpfstmpfs /tmp临时文件不触发 COW
分层优化把不常变的放在下层COPY 顺序影响层复用

6.2 CPU Throttle 对 Go Runtime 的影响 ​

这是 K8s 环境中最隐蔽的性能杀手之一。CPU limit 不是硬限制——它是 CFS 周期内的加权时间片:

text
CFS (Completely Fair Scheduler) 工作原理:
  - 默认周期: cfs_period_us = 100ms
  - 设定 limits.cpu = 2 → cfs_quota_us = 200ms
  - 含义: 每 100ms 周期内,该容器最多使用 200ms CPU
  - 如果 100ms 内用完 200ms → 下一个 100ms 暂停

实际影响:
  limits.cpu=0.5, cfs_period=100ms
  → 每 100ms 只能用 50ms CPU
  → 如果用满 → 再等 50ms
  → 对应用来说: 突然暂停 50ms,然后又恢复

CPU Throttle 对 Go 的影响:

mermaid
sequenceDiagram
    participant App as Go Runtime
    participant Kernel as CFS Scheduler

    App->>App: GC 标记阶段开始 (STW)
    Note over App: 需要 3ms 连续 CPU 时间
    Kernel->>Kernel: 100ms 周期内 quota 耗尽
    Kernel-->>App: Throttle! 暂停 100ms
    Note over App: STW 原计划 3ms<br/>实际: 3ms + 100ms throttle = 103ms
    Note over App: P99 RT: 103ms 尖刺!
影响层面具体表现根因
GC STWSTW 时间从 3ms 变 100ms+throttle 期间 GC 被暂停
P99 延迟周期性毛刺,和 CFS 周期 100ms 相关CPU 被限制在关键路径
调度延迟goroutine 等待 M 的时间变长M 被 throttle 后无法执行
慢启动Pod 启动时间从 3s 变 30s初始化阶段 CPU 密集但被限制

排查命令:

bash
# 看 throttle 指标
# 容器内:
cat /sys/fs/cgroup/cpu/cpu.stat
# nr_throttled:  被 throttle 的次数
# throttled_time: 累计被 throttle 的纳秒数

# 高 throttle 信号:
# throttled_time / uptime > 5% → 严重!

# K8s 层面:
kubectl top pod <pod> --containers
# 看 CPU 使用率是否接近 limit

建议:

建议原因
用 request 不用 limit避免 throttle,让 Pod 在空闲时用满节点 CPU
确实要 limit 时,limit ≥ request × 3给突发留足余量
对延迟敏感的服务,不设 CPU limitThrottle 对 P99 的伤害远大于"公平调度"的收益
用 GOMAXPROCS 限制 Go 使用核数让 Go 自己控制并行度,而非依赖 CFS throttle

6.3 Pod 启动慢排查流程 ​

mermaid
flowchart TD
    A["Pod 启动慢 (>30s)"] --> B{"哪个阶段慢?"}
    B -->|"镜像拉取慢"| C["kubectl describe pod 看 Events<br/>Pulling image 耗时"]
    C --> C1["原因: 镜像大 / 网络带宽 / registry 距离"]
    C1 --> C2["修复: 镜像瘦身 / 预拉取 / 镜像加速"]
    B -->|"容器启动慢"| D["容器启动到 readiness 就绪的耗时"]
    D --> D1{"原因?"}
    D1 -->|"CPU throttle"| E["检查 cpu.stat throttled_time<br/>调高 limit 或去掉 limit"]
    D1 -->|"依赖初始化"| F["健康检查/配置加载/连接池预热<br/>initContainer 预加载"]
    D1 -->|"GC 风暴"| G["Go: GODEBUG=gctrace=1<br/>看 GC 阶段耗时"]
    B -->|"健康检查失败超时"| H["readinessProbe initialDelaySeconds 太短"]
    H --> H1["调大 initialDelaySeconds<br/>避免 Pod 在就绪前被标记为 NotReady"]

6.4 OOMKilled 排查 ​

bash
# 1. 看 Pod 退出原因
kubectl describe pod <pod> | grep -A5 "State:"
# Exit Code: 137 → 被 SIGKILL (OOM)

# 2. 看 OOM 记录
kubectl describe pod <pod> | grep OOMKilled
# OOMKilled: true

# 3. 看内存使用趋势
kubectl top pod <pod> --containers

# 4. Go 应用排查
# pprof 看堆内存
go tool pprof http://localhost:6060/debug/pprof/heap
# 看 goroutine 是否泄漏
go tool pprof http://localhost:6060/debug/pprof/goroutine
OOM 原因Go 中常见表现修复
内存泄漏goroutine 数一直涨, 堆内存一直涨排查 ctx 未取消/goroutine 泄漏
流量突增临时分配过多调高 memory limit 或加限流
GC 不及时GOGC 默认 100, 堆涨到 2× 才 GC调低 GOGC 或设 GOMEMLIMIT
CGO 内存不受 GC 管理排查 C 库内存泄漏

参考 ​

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记