元数据卡
- 前置知识:Vol 4 网络(Linux network namespace)、第6章(微服务架构)
- 预计时间:45 分钟
- 核心难度:进阶
- 阅读模式:高度专注
- 完成标志:理解 Docker 镜像是如何分层的,能说清 Pod 与容器的关系,理解 Deployment 的发布策略,知道 HPA 的工作原理
你的进度
20 个微服务,100 个实例。每个实例的部署流程是:
- 申请一台虚拟机
- 安装 JDK 17
- 配置环境变量
- 复制 jar 包
- 启动
- 健康检查
如果一切顺利,一个实例部署耗时 15 分钟。但 "一切顺利" 很少发生——JDK 版本不对、环境变量漏了、端口冲突、依赖了没装的系统库。
加上新版本发布时,你需要滚动升级 100 个实例,逐个替换,新老版本并存。
林将军说:"你打仗的时候,每个战士要自己生火做饭吗?"
当然不需要。军队有后备梯队统一供应军粮,每个战士拿口粮就是。
容器就是你的标准化"口粮"。而 Kubernetes 就是供应和调度这支军队的后勤系统。
你的任务
理解容器化的核心机制——Cgroups 和 Namespace 如何隔离进程,Docker 镜像的分层存储如何节省空间,Kubernetes 的三个核心抽象——Pod、Service、Deployment——形成怎样的管理模型。不需要成为 K8s 管理员,但要知道编排系统到底在编排什么。
破局 · 溯源
容器的本质不是"轻量级虚拟机"
很多人说"容器是轻量级虚拟机"。这个类比是错的,它会让你误解容器的安全性和隔离性。
虚拟机的隔离层是 Hypervisor,每个 VM 运行自己独立的操作系统内核:
+--------+ +--------+ +--------+
| App A | | App B | | App C |
| OS A | | OS B | | OS C |
+--------+ +--------+ +--------+
| Hypervisor(Type 1/2) |
+-------------------------------+
| 物理硬件 |
+-------------------------------+容器的隔离层是 Linux 内核的 Namespace 和 Cgroups:
+--------+ +--------+ +--------+
| App A | | App B | | App C |
+--------+ +--------+ +--------+
| Linux Kernel |
+-------------------------------+
| 物理硬件 |
+-------------------------------+所有容器共享同一个操作系统内核。这是优点也是缺点:
- 优点:启动快(毫秒级)、资源开销极低、密度高
- 缺点:容器之间隔离性弱于 VM(共享内核)、Windows 容器与 Linux 容器不能混合运行
Namespace 做了什么?
Linux Namespace 让每个容器拥有自己的"视图":
| Namespace | 隔离的资源 | 容器中的感觉 |
|---|---|---|
| PID | 进程编号 | 自己是 PID 1,看不到其他容器的进程 |
| Network | 网络栈 | 自己有独立的 eth0、IP 和端口空间 |
| Mount | 文件系统挂载点 | 看到自己独立的 /proc 和 /sys |
| UTS | 主机名 | 自己的 hostname |
| IPC | 进程间通信 | 独立的 System V IPC 和 POSIX 消息队列 |
| User | 用户 ID | 容器内 root ↔ 容器外普通用户 |
Cgroups 做了什么?
Cgroups 控制容器能"用多少":
# 限制一个容器最多使用 2 核 CPU、1GB 内存
# 这些配置由 Cgroups 子系统实现
# CPU 限制(CFS 配额)
--cpus=2 # 容器最多使用 2 核
# 内存限制
--memory=1g # 容器最多使用 1GB 内存
--memory-reservation=512m # 一般情况下尽量不使用超过 512MB
# IO 限制
--device-read-bps=/dev/sda:100mb # 磁盘读速率限制每个 Namespace 和 Cgroup 的控制器在 /sys/fs/cgroup/ 下有对应的子系统控制文件。
Docker:容器标准化的第一步
Docker 把容器的打包、分发、运行标准化了。核心概念两个:镜像和容器。
镜像分层
镜像 = 基础层 + 一层一层叠加的文件变更
例如你的应用镜像:
+-------------------------------+
| 应用层: atlas-service.jar | 第4层
| 命令: CMD ["java", "-jar"] |
+-------------------------------+
| 依赖层: 安装 JDK | 第3层
+-------------------------------+
| OS 更新: apt update && install | 第2层
+-------------------------------+
| 基础层: Ubuntu 22.04 | 第1层
+-------------------------------+Dockerfile 中的每一行 RUN、COPY、ADD 创建一个新层:
# Dockerfile - atlas-service
# 构建: docker build -t atlas-service:1.0 .
# 运行: docker run -p 8080:8080 atlas-service:1.0
FROM eclipse-temurin:17-jre # 基础层: JDK 17 JRE
WORKDIR /app # 不产生新层,设置工作目录
COPY target/atlas-service.jar app.jar # 新层: 添加 JAR 包
EXPOSE 8080 # 文档:暴露端口
ENTRYPOINT ["java", "-jar", "app.jar"] # 启动命令层的好处:
- 缓存复用:修改了 jar 包,但 JDK 层不变——可以复用
- 空间节省:10 个服务都基于
eclipse-temurin:17-jre,这个基础层只存一份 - 增量传输:升级只传输差异层,不是整个镜像
用 docker history 查看镜像的层结构:
docker history atlas-service:1.0
IMAGE CREATED CREATED BY SIZE
c0a1b2c3d4e5 2 minutes ago ENTRYPOINT ["java" "-jar" "app.jar"] 0B
b1c2d3e4f5a6 2 minutes ago COPY target/atlas-service.jar app.jar 32MB
b2c3d4e5f6a7 2 minutes ago WORKDIR /app 0B
a3b4c5d6e7f8 2 weeks ago /bin/sh -c apt-get ... 205MB
...网络模型
Docker 使用 Container Network Model(CNM):
Bridge 网络(默认):
容器 eth0 ←→ docker0 网桥 ←→ 宿主机网络 → 外部
Host 网络:
容器直接使用宿主机网络栈(无独立网络命名空间)
Overlay 网络:
跨主机的容器通信,通过 VXLAN 隧道当两个容器要相互通信时,默认在同一个 bridge 网络上可以直接通过 IP 通信——但更好的方式是用 docker-compose 或 K8s 管理的服务名,而不是直接依赖 IP。
Kubernetes:容器编排系统
Docker 解决了"一台机器上跑容器"的问题。但你有一百台机器、500 个容器,你需要:
- 容器调度(哪个容器放在哪台机器上)
- 服务发现(容器 IP 频繁变化)
- 自动扩缩
- 滚动升级
- 配置管理
Kubernetes 就是为这些问题而生的。它不管理容器,它管理控制单元。
Pod:最小调度单元
Kubernetes 不直接管理容器。它管理的是一组紧密协作的容器——Pod。
Pod 结构:
+-----------------------+
| Pod ("atlas-logger") |
| +------------------+ |
| | 主容器: app.jar | | ← 处理业务
| +------------------+ |
| +------------------+ |
| | 伴生容器: sidecar| | ← 日志采集、Metrics导出
| +------------------+ |
| | 共享网络命名空间 | | ← localhost 通信
| | 共享存储卷 | |
+-----------------------+
Pod 的 IP 在每个容器之间共享 —— localhost:8080 指向同一个端口空间。Pod 的特点:
- 同一 Pod 中的容器共享网络命名空间(同一个 IP、端口空间)
- 共享存储卷(Volume)
- Pod 是调度的最小单位——Pod 要么整部署一台 Node 上,要么整部署另一台
- Pod 的 IP 是不稳定的(Pod 重启后 IP 会变)
Pod 是短暂的。你通常不直接创建 Pod,而是通过更高级的资源创建——Deployment。
Deployment:声明式更新
Deployment 管理 Pod 的生命周期。它定义"我想要 N 个运行中的 Pod 副本"。
# deployment.yaml
# 部署 atlas-user-service 到 K8s
# 创建: kubectl apply -f deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: atlas-user-service
spec:
replicas: 3 # 期望 3 个副本
selector:
matchLabels:
service: atlas-user # 管理带有此标签的 Pod
template:
metadata:
labels:
service: atlas-user
spec:
containers:
- name: user-service
image: registry/atlas-user:1.2
ports:
- containerPort: 8080
resources:
requests: # 调度时保证的资源
cpu: 500m # 0.5 核
memory: 512Mi
limits: # 容器不可超过的资源上限
cpu: 1000m
memory: 1Gi
livenessProbe: # 活体检测:容器还活着吗?
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe: # 就绪检测:容器可以接受流量吗?
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 3Deployment 的控制循环:
用户写入 YAML → API Server → etcd 持久化
↓
Deployment Controller 监听到变更 → 创建 ReplicaSet
↓
ReplicaSet Controller 启动/停止 Pod
↓
Scheduler 把 Pod 调度到具体的 Node 上
↓
Kubelet 在 Node 上启动容器
↓
健康检查 → 就绪检查 → 注册到 Service滚动更新策略:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 升级时最多允许 1 个 Pod 不可用
maxSurge: 1 # 升级时最多允许额外 1 个 Pod 存在把镜像从 1.2 更新到 1.3:
kubectl set image deployment/atlas-user-service \
user-service=registry/atlas-user:1.3Deployment 会逐渐替换 Pod:先创建一个 1.3 的新 Pod,等待它就绪后,停止一个 1.2 的旧 Pod。如此反复,直到所有 Pod 都更新到 1.3。
Service:稳定的访问端点
Pod 的 IP 是变化的,但 Service 提供一个稳定的虚拟 IP(ClusterIP)和 DNS 名称:
# service.yaml
# 为 atlas-user-service 创建稳定的访问端点
# kubectl apply -f service.yaml
apiVersion: v1
kind: Service
metadata:
name: atlas-user-service
spec:
selector:
service: atlas-user # 转发流量给具有此标签的 Pod
ports:
- port: 80 # Service 端口
targetPort: 8080 # Pod 端口
type: ClusterIP # 集群内可访问(默认值)其他服务可以通过 atlas-user-service:80 访问它,无需关心具体 Pod IP。
Kubernetes 内置了 DNS 服务(CoreDNS),把 Service 名称自动解析为 ClusterIP。
HPA:自动水平扩缩
当 Atlas 服务的流量上升时,手动增加副本数很麻烦。HPA(Horizontal Pod Autoscaler)根据 CPU/内存使用率或自定义指标自动调整副本数。
# hpa.yaml
# 根据 CPU 使用率自动扩缩
# kubectl apply -f hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: atlas-user-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: atlas-user-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 当 CPU 使用率超过 70% 时扩容HPA 的控制算法:
desiredReplicas = ceil[currentReplicas * (currentMetricValue / desiredMetricValue)]
例如:当前 2 个副本,CPU 平均 85%,目标是 70%:
desiredReplicas = ceil[2 * (85 / 70)] = ceil[2.43] = 3
当副本数达到 3,CPU 降到 60%:
desiredReplicas = ceil[3 * (60 / 70)] = ceil[2.57] = 3 (不变,防止震荡)K8s 有冷却机制(--horizontal-pod-autoscaler-downscale-stabilization 默认 5 分钟),防止频繁的扩容和缩容。
ConfigMap 和 Secret:配置管理的 K8s 方式
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: atlas-config
data:
LOG_LEVEL: "INFO"
DATABASE_HOST: "postgres-cluster"
CACHE_TTL_SECONDS: "300"挂载到 Pod:
spec:
containers:
- envFrom:
- configMapRef:
name: atlas-configSecret 类似,但数据以 base64 编码存储,并有加密选项。
常见陷阱
容器内运行多个进程。 Pod 级别的设计鼓励"一个容器一个进程"。虽然可以在容器内用 supervisor 跑多个进程,但这样会失去健康检查的精度(你检查的是 supervisor 活着,不是具体进程活着)。
忽略 Pod 的资源 requests 和 limits。 如果不设置,Pod 默认无限制——可能导致一个容器耗尽 Node 资源。生产环境一定要设置 requests(调度保证)和 limits(运行时上限)。
镜像标签用 latest。 每次
docker pull得到不同的镜像,无法复现环境。指定具体的语义化版本标签。直接用 IP 访问 Pod。 Pod IP 在重启后变化。始终通过 Service 访问。如果是 Kubernetes 内部,使用 DNS 名称。
在 StatefulSet 中假设 Pod 是无状态的。 StatefulSet 保证每个 Pod 有固定的网络标识和存储——但如果你没有设计好处理有状态的复杂性(备份、恢复、节点间数据同步),StatefulSet 只会暴露这些问题而不是解决它们。
通关挑战
热身:把你的一个 Java 服务容器化。编写 Dockerfile(使用多阶段构建:用 maven 镜像编译,用 jre 镜像运行)。构建并运行容器,验证健康检查路径返回 200。
挑战:用 kind(Kubernetes in Docker)在本地启动一个 3 节点的 K8s 集群。部署一个 Deploymen(3 个副本)+ Service(ClusterIP)+ ConfigMap。模拟 Pod 故障(
kubectl delete pod),观察 K8s 如何自动重建 Pod。
# 安装 kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
# 创建集群
kind create cluster --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
# 部署应用
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f configmap.yaml- 观察:用
kubectl get pods -w观察滚动更新过程。在更新过程中发送持续请求,看是否有请求丢失。调整maxUnavailable的值,观察对可用性的影响。
旅人笔记
容器化不是"在 K8s 上跑 Docker"。底层是 Linux Namespace+Cgroups 的隔离,上层是 Pod-Service-Deployment 的三层管理模型。Docker 解决了"可移植的打包和运行",K8s 解决了"大规模的编排和治理"。理解了这套抽象栈,你看到的不是"YAML 配置"——你看到的是声明式的期望状态,以及 kube-controller-manager 里不断运行的"当前状态→期望状态"的控制循环。
下一站预告
20 个微服务运行在 3 节点的 K8s 集群上。你可以通过 kubectl logs 查看日志、kubectl top pods 查看资源利用率。但当一个请求穿越 5 个服务、最终响应变慢时——你怎么定位瓶颈?调用链跑到哪个服务了?哪个数据库查询慢了?这些问题需要可观测性来回答。下一章,也是本卷的最后一章,我们装上分布式系统的"眼睛"。