容器化

当「在我机器上能跑」变成「一次构建,到处运行」,软件开发进入了新的纪元。Docker 带来的不只是技术变革,更是一种思维方式的转变:从管理机器到管理应用,从手动部署到自动化交付,从烟囱式架构到云原生架构。

这个模块聚焦容器化的核心知识体系:从镜像优化、Java 应用容器化、JVM 内存配置,到容器化改造实战。无论你的团队正在经历容器化转型,还是希望深入理解容器技术的底层原理,这里都能找到有价值的内容。

模块结构

本模块涵盖容器化的关键知识领域:

章节核心内容解决什么问题
镜像优化与瘦身多阶段构建、极简基础镜像、层优化策略镜像太大、构建太慢、拉取超时
Java 容器化Jib vs Dockerfile、内存配置、Docker ComposeJava 应用进容器就 OOM
JVM 内存配置容器感知、MaxRAMPercentage、G1/ZGCJVM 不认识容器限制
容器化改造实战12-Factor、配置外部化、有状态应用如何把老系统迁移到容器

核心演进路径

容器化改造是一个渐进的过程,每个阶段都有其核心挑战:

flowchart TD
    A["传统部署\n物理机/虚拟机"] --> B["阶段一:Docker 化\n编写 Dockerfile"]
    B --> C["阶段二:镜像优化\n多阶段构建 + 极简镜像"]
    C --> D["阶段三:K8s 迁移\nDeployment + Service"]
    D --> E["阶段四:云原生特性\n灰度 + 可观测 + GitOps"]

    style A fill:#ffcccc
    style B fill:#fff3cd
    style C fill:#d4edda
    style D fill:#d1ecf1
    style E fill:#e2d9f3

各阶段关键能力

阶段一:Docker 化

  • 理解容器与镜像的关系
  • 编写基础 Dockerfile
  • 容器内运行和调试

阶段二:镜像优化

  • 多阶段构建
  • 基础镜像选型(alpine/distroless/scratch)
  • 层缓存优化

阶段三:K8s 迁移

  • Deployment/Service/ConfigMap 编写
  • 健康检查配置
  • 资源限制与调度

阶段四:云原生

  • 服务网格(Istio)
  • GitOps 流水线
  • 可观测性体系

Java 容器化的独特挑战

Java 应用与脚本语言不同,JVM 的内存管理和 GC 机制在容器环境中需要特殊处理:

flowchart LR
    subgraph Physical["物理机部署"]
        JVM1["JVM\n-Xmx8g"] --> MEM1["8GB 堆"]
        MEM1 --> TOTAL1["宿主机 32GB"]
    end

    subgraph Container["容器部署"]
        JVM2["JVM\n-Xmx8g"] --> MEM2["8GB 堆"]
        MEM2 --> TOTAL2["容器限制 2GB"]
    end

    TOTAL1 -. "OK" .-> TOTAL2
    TOTAL2 -. "❌ OOM" .-> JVM2

核心问题:JVM 默认以物理机为假想敌配置内存,进入容器后必须适配 cgroup 限制。

学习建议

从问题出发,而非从概念出发

容器化不是「学 Docker」「学 K8s」,而是「解决容器化过程中的问题」。建议带着问题学习:

  • 镜像太大 → 学习多阶段构建和基础镜像选型
  • 容器 OOM → 学习 JVM 容器感知配置
  • 部署太慢 → 学习构建缓存和 CI/CD 集成
  • 环境不一致 → 学习配置外部化和 12-Factor

渐进式实践

不要试图一次性完成所有改造:

  1. 先让应用跑起来:一个能工作的 Dockerfile 比完美的 Dockerfile 更有价值
  2. 再优化:构建速度、镜像体积、运行时配置
  3. 最后迁移:K8s 迁移可以在本地 minikube/kind 上先行试验

关注边界情况

容器化的很多坑都藏在边界情况里:

  • JVM 内存配置不只是 -Xmx
  • 健康检查要等待应用真正就绪
  • 优雅关闭要处理请求处理中的情况
  • 有状态应用的容器化要慎之又慎

延伸阅读

容器化只是云原生之旅的第一步。在掌握容器化之后,可以继续探索:

思考题

问题 1:为什么 Java 应用在容器中容易 OOM,但物理机上运行正常?

参考答案

物理机上,JVM 可以使用全部可用内存;容器中,JVM 必须遵守 cgroup 限制。如果使用固定的 -Xmx 而不是容器感知的 -XX:MaxRAMPercentage,JVM 可能分配超过容器限制的内存。同时,JVM 的内存消耗不只是堆,还包括元空间、直接内存、线程栈、代码缓存等。这些加起来很容易超过容器限制。

问题 2:多阶段构建的「构建阶段」和「运行阶段」分别需要什么?

参考答案

构建阶段需要完整的 JDK、Maven/Gradle、源代码、依赖库,用于编译和打包。运行阶段只需要 JRE(或极简运行时)、最终产物(JAR/二进制)、运行时依赖(如 CA 证书)。两者分离后,运行时镜像可以非常小。

问题 3:StatefulSet 和 Deployment 的本质区别是什么?

参考答案

核心区别在于「Pod 标识」和「存储」:

  1. StatefulSet 的 Pod 有固定、稳定的标识(如 mysql-0、mysql-1),Deployment 的 Pod 标识是随机的
  2. StatefulSet 每个 Pod 有独立的 PVC,Deployment 的 Pod 共享存储
  3. StatefulSet 按顺序启动和终止,Deployment 并行处理

这使得 StatefulSet 适合数据库、消息队列等需要固定网络标识和持久化存储的应用。

准备好开始容器化之旅了吗?让我们从镜像优化开始。