Skip to content

元数据卡

  • 前置知识:Vol 4 网络(Linux network namespace)、第6章(微服务架构)
  • 预计时间:45 分钟
  • 核心难度:进阶
  • 阅读模式:高度专注
  • 完成标志:理解 Docker 镜像是如何分层的,能说清 Pod 与容器的关系,理解 Deployment 的发布策略,知道 HPA 的工作原理

你的进度

20 个微服务,100 个实例。每个实例的部署流程是:

  1. 申请一台虚拟机
  2. 安装 JDK 17
  3. 配置环境变量
  4. 复制 jar 包
  5. 启动
  6. 健康检查

如果一切顺利,一个实例部署耗时 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 控制容器能"用多少":

bash
# 限制一个容器最多使用 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 中的每一行 RUNCOPYADD 创建一个新层:

dockerfile
# 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"]     # 启动命令

层的好处:

  1. 缓存复用:修改了 jar 包,但 JDK 层不变——可以复用
  2. 空间节省:10 个服务都基于 eclipse-temurin:17-jre,这个基础层只存一份
  3. 增量传输:升级只传输差异层,不是整个镜像

docker history 查看镜像的层结构:

bash
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 副本"。

yaml
# 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: 3

Deployment 的控制循环:

用户写入 YAML → API Server → etcd 持久化

Deployment Controller 监听到变更 → 创建 ReplicaSet

ReplicaSet Controller 启动/停止 Pod

Scheduler 把 Pod 调度到具体的 Node 上

Kubelet 在 Node 上启动容器

健康检查 → 就绪检查 → 注册到 Service

滚动更新策略:

yaml
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.3

Deployment 会逐渐替换 Pod:先创建一个 1.3 的新 Pod,等待它就绪后,停止一个 1.2 的旧 Pod。如此反复,直到所有 Pod 都更新到 1.3。

Service:稳定的访问端点

Pod 的 IP 是变化的,但 Service 提供一个稳定的虚拟 IP(ClusterIP)和 DNS 名称:

yaml
# 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/内存使用率或自定义指标自动调整副本数。

yaml
# 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 方式

yaml
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: atlas-config
data:
  LOG_LEVEL: "INFO"
  DATABASE_HOST: "postgres-cluster"
  CACHE_TTL_SECONDS: "300"

挂载到 Pod:

yaml
spec:
  containers:
  - envFrom:
    - configMapRef:
        name: atlas-config

Secret 类似,但数据以 base64 编码存储,并有加密选项。


常见陷阱

  1. 容器内运行多个进程。 Pod 级别的设计鼓励"一个容器一个进程"。虽然可以在容器内用 supervisor 跑多个进程,但这样会失去健康检查的精度(你检查的是 supervisor 活着,不是具体进程活着)。

  2. 忽略 Pod 的资源 requests 和 limits。 如果不设置,Pod 默认无限制——可能导致一个容器耗尽 Node 资源。生产环境一定要设置 requests(调度保证)和 limits(运行时上限)。

  3. 镜像标签用 latest。 每次 docker pull 得到不同的镜像,无法复现环境。指定具体的语义化版本标签。

  4. 直接用 IP 访问 Pod。 Pod IP 在重启后变化。始终通过 Service 访问。如果是 Kubernetes 内部,使用 DNS 名称。

  5. 在 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。

bash
# 安装 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 个服务、最终响应变慢时——你怎么定位瓶颈?调用链跑到哪个服务了?哪个数据库查询慢了?这些问题需要可观测性来回答。下一章,也是本卷的最后一章,我们装上分布式系统的"眼睛"。

Built with VitePress | Software Systems Atlas