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 |
| kubelet | Node 上的"工头" | Watch API Server → 创建/销毁 Pod |
| kube-proxy | Node 上的网络代理 | 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 | 内部服务间通信 |
| NodePort | NodeIP:30000-32767 | 每 Node 开端口 + ClusterIP | 外部测试/简单接入 |
| LoadBalancer | 外部 LB IP | 云厂商 LB + NodePort | 生产外部入口 |
| ExternalName | DNS 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: 10Deployment → 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 --> Filters3.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 fail5.2 经典问题定位
| 问题 | 排查路径 |
|---|---|
| Pod CrashLoopBackOff | kubectl logs 看启动日志 → kubectl describe 看 Events → 检查 OOMKilled? |
| Pod Pending | Events 看调度失败原因(资源不足/亲和性不满足/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 |
| tmpfs | tmpfs /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 STW | STW 时间从 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 limit | Throttle 对 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 库内存泄漏 |
登录后即可发表评论 👇