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 多阶段构建(瘦身关键)
# 阶段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网桥 +iptablesNAT 与宿主机/外部通信。容器间用--network同桥互联(服务名解析靠内置 DNS)。 - host:容器共享宿主机网络栈,无 NAT,性能最好但无隔离(端口冲突)。
- none:无网络,纯隔离。
- overlay(Swarm/K8s):跨主机容器网络(VXLAN 隧道),多节点集群用。
# 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 问题)。
📚 官方文档与参考资料
- Docker 官方文档:https://docs.docker.com/
- Dockerfile 参考:https://docs.docker.com/reference/dockerfile/
- Docker 网络概述:https://docs.docker.com/network/bridge/
✏️ 练习与自测
- 用多阶段构建为你的 Spring Boot 应用写一个 Dockerfile,说明为什么分两阶段(编译 vs 运行)、为何先
COPY pom.xml再COPY src。 - 解释
-Xmx与容器 memory limit 不一致会导致什么问题(OOM Kill),如何用UseContainerSupport避免。 - 用 docker-compose 编排"Web + MySQL + Redis",指出哪些数据必须挂 volume,为什么。
- 简述 namespace 与 cgroup 各自负责什么(隔离视图 vs 限制资源),容器相比虚拟机的本质差异是什么?
本节小结
Docker 用"Namespace(六种隔离视图)+ Cgroups(资源硬限制)"实现轻量可移植;用"镜像分层(只读层叠加 + 共享复用)+ 写时复制(CoW 可写层)"实现秒级启动与空间节省。Java 落地要点:多阶段构建瘦身、开 UseContainerSupport 对齐内存防 OOM Kill、数据挂 volume、镜像锁版本 + 非 root 运行。它是云原生的第一块基石,K8s 之上皆以此为基础。