Skip to content

Docker 容器化 ​

Java 高级 · 云原生基础

Docker 用容器封装"应用 + 依赖 + 运行环境",让"在我机器上能跑"成为历史。但很多人只停留在 docker run。真正理解 Docker,要回答:容器和虚拟机差在哪?为什么镜像能复用又秒级启动?容器是怎么被"隔离"和"限制"的? 本文讲清 namespace/cgroup 隔离、镜像分层与写时复制(UnionFS)、容器网络模型,以及 Java 应用落地的坑。

一、容器 vs 虚拟机:共享内核的轻量隔离 ​

虚拟机(VM)跑一个完整 Guest OS,由 Hypervisor 虚拟化硬件,重(GB 级、秒分级启动)。容器共享宿主机内核,只做进程级隔离——这就是"轻量"的本质。

容器不是"伪虚拟化",而是 Linux 内核两大能力:
Namespace(隔离"看到什么")+ Cgroups(限制"能用多少")。

二、Namespace:六类隔离 ​

Namespace 让每个容器觉得自己独占一套资源:

Namespace隔离内容
PID进程号(容器内 PID 从 1 开始)
Mount挂载点(独立文件系统视图)
Network网络栈(独立 IP、端口、网卡)
UTS主机名/域名
IPC进程间通信(共享内存等)
User用户/组(容器内 root 可映射为宿主机普通用户)

docker run 本质是 clone() 系统调用带上这些 namespace 标志,创建出一个"隔离视图"的进程。docker exec 则是 setns() 进到目标容器的 namespace。

三、Cgroups:资源限制 ​

Cgroups(Control Groups)限制容器能用多少 CPU、内存、IO、PID 数。你设的 --memory=512m 会写入 cgroup 的 memory.max,内核 OOM 时优先 kill 超限额的容器进程。这正是"容器 OOM 被 kill"的来源——限额是硬约束。

DANGER

-Xmx 设超过容器 memory limit 会 OOM Kill:JVM 若不开 UseContainerSupport(JDK 8u191+ 默认开),读不到 cgroup 限制,按宿主机物理内存分配堆,实际占用超过容器 limit → 被 cgroup kill。务必开 UseContainerSupport 并给元空间/直接内存/线程栈留余量。

四、镜像分层与写时复制(UnionFS) ​

4.1 分层结构 ​

镜像是**只读层(Layer)**的叠加,每条 Dockerfile 指令生成一个层。层可被多个镜像共享(如多个镜像都基于 eclipse-temurin:17,共享基础层,省空间、省传输)。

  • 共享:基础层多容器/多镜像复用,下载一次处处用。
  • 缓存:Dockerfile 中靠前的指令不变则复用缓存层,因此"变动频繁的指令放后面"能大幅加速构建。

4.2 写时复制(Copy-on-Write) ​

容器启动时,在镜像只读层之上加一个可写层。容器读文件时从各层找;改文件时,把该文件复制到可写层再改,原镜像层不变。这保证:

  • 多个容器共享同一镜像,但各自修改互不可见。
  • 镜像本身不可变(便于分发与版本管理)。

4.3 多阶段构建(瘦身关键) ​

dockerfile
# 阶段1:编译(含 Maven + JDK,体积大)
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -q dependency:go-offline        # 先拉依赖, 利用层缓存
COPY src ./src
RUN mvn -q package -DskipTests

# 阶段2:运行(仅含产物 + JRE,体积小、攻击面小)
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/app.jar app.jar
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseContainerSupport"
EXPOSE 8080
ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar app.jar"]

多阶段构建:编译阶段用完整 JDK,运行阶段只拷产物到精简 JRE 镜像——最终镜像不含 Maven/源码/构建工具,体积小数倍、更安全。

五、容器网络模型 ​

Docker 提供多种网络驱动:

  • bridge(默认):每个容器分到独立网络命名空间,通过 docker0 网桥 + iptables NAT 与宿主机/外部通信。容器间用 --network 同桥互联(服务名解析靠内置 DNS)。
  • host:容器共享宿主机网络栈,无 NAT,性能最好但无隔离(端口冲突)。
  • none:无网络,纯隔离。
  • overlay(Swarm/K8s):跨主机容器网络(VXLAN 隧道),多节点集群用。
yaml
# docker-compose.yml 一键编排
version: "3.8"
services:
  app:
    build: .
    ports: ["8080:8080"]
    environment: { TZ: Asia/Shanghai }
    depends_on: [mysql, redis]
    networks: [backend]
  mysql:
    image: mysql:8.0
    environment: { MYSQL_ROOT_PASSWORD: "***" }
    volumes: ["mydata:/var/lib/mysql"]     # 数据持久化
    networks: [backend]
  redis:
    image: redis:7-alpine
    networks: [backend]
volumes: { mydata: {} }
networks: { backend: {} }

DANGER

  • 容器内跑 root + 敏感信息写死:镜像应 USER 非 root,密钥用 secret/环境变量注入,避免权限过大与泄露(容器逃逸风险)。
  • 把数据存容器可写层:容器销毁数据即失,数据库/日志必须挂 volume 持久化。
  • latest 标签进生产:镜像版本漂移导致环境不一致,生产必须锁定具体 tag/digest(如 redis:7.2.4 或 @sha256:...)。
  • -Xmx 超容器 limit:见第三节,必开 UseContainerSupport。

六、Java 应用容器化落地清单 ​

  • 多阶段构建 + 精简基础镜像(alpine/jre),显著减小体积与攻击面。
  • Dockerfile 指令顺序"稳定在前、易变在后",最大化缓存命中。
  • 资源设 --memory/--cpus 限制,配合 JVM 容器感知(UseContainerSupport),避免单容器吃满宿主机。
  • 推送到私有 Harbor + 镜像扫描(Trivy)+ 镜像签名,保障供应链安全。
  • 数据、日志挂 volume;时区、编码显式设置(避免容器内 UTF-8/Locale 问题)。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 用多阶段构建为你的 Spring Boot 应用写一个 Dockerfile,说明为什么分两阶段(编译 vs 运行)、为何先 COPY pom.xml 再 COPY src。
  2. 解释 -Xmx 与容器 memory limit 不一致会导致什么问题(OOM Kill),如何用 UseContainerSupport 避免。
  3. 用 docker-compose 编排"Web + MySQL + Redis",指出哪些数据必须挂 volume,为什么。
  4. 简述 namespace 与 cgroup 各自负责什么(隔离视图 vs 限制资源),容器相比虚拟机的本质差异是什么?

本节小结

Docker 用"Namespace(六种隔离视图)+ Cgroups(资源硬限制)"实现轻量可移植;用"镜像分层(只读层叠加 + 共享复用)+ 写时复制(CoW 可写层)"实现秒级启动与空间节省。Java 落地要点:多阶段构建瘦身、开 UseContainerSupport 对齐内存防 OOM Kill、数据挂 volume、镜像锁版本 + 非 root 运行。它是云原生的第一块基石,K8s 之上皆以此为基础。