主题
界面
主题
从本节起进入部署与运维。第一个问题是:容器到底省下了什么。
★ 本节的实测把三件事量出来 ✓✓:
① 分层复用:10 个镜像共享基础层 → 磁盘从 2000 MB 降到 380 MB(省 81%)
② 写时复制:改一个文件只复制那一个(约 13 字节,而不是 180 MB)
③ 构建缓存:Dockerfile 指令换一下顺序 → 重建从 332 秒降到 30 秒(快 11 倍)✓✓★ 必须说清的边界 ✓:本机没有 Docker,所以本类实现的是最小镜像模型(层、写时复制、构建缓存)—— 它没有模拟 Linux 命名空间与 cgroups(那才是容器的另一半,见 5.4)✓
.dockerignore 解决什么 ★★★;先看一个"镜像为什么这么大"的真实场景 ✗:
现象:CI 每次构建要 5 分钟,镜像仓库存储一直在涨
排查:Dockerfile 是这么写的 ——
COPY . /app ← 代码一改,这层缓存就失效
RUN mvn dependency:resolve ← 于是重新下载全部依赖(几分钟)✗✗
RUN mvn package★ 本节的实测就是这个问题 ✓:
① 常见写法(先 COPY 全部代码,再装依赖): 约 332 秒 ✗✗
② 优化写法(先 COPY pom,再装依赖,最后 COPY src):约 30 秒 ✓✓
→ 快了 11 倍 ✓✓实测 ✓✓:
场景:基础镜像(JDK + 常用工具)180 MB,每个服务的应用层 20 MB,共 10 个服务
① 不分层(每个镜像各存一份完整文件系统): 2000 MB ✗✗
② 分层(基础层共享): 380 MB ✓✓
→ 省下 81% 的磁盘(层复用命中 9 次)★ 分层的收益不止磁盘(三项一起才是重点) ✓✓:
| 收益 | 原因 |
|---|---|
| 省磁盘 ★★ | 相同层只存一份 ✓✓ |
| 省网络 ★★★ | pull 时已有的层不下载(CI 机器上尤其明显)✓✓ |
| 省构建时间 ★★★ | 没变的层直接复用缓存(见 5.3)✓✓ |
★ 一条实践推论 ✓✓:
基础镜像要尽量统一 ✓ ——
如果 A 服务用
openjdk:17-slim、B 服务用eclipse-temurin:21, 它们的基础层完全不同 → 分层的收益就没了 ✗✗所以「统一基础镜像」应该当成团队规范来管 ✓✓
★ 为什么"层"能共享 ✓:
层是不可变的(immutable) ✓ ——一旦生成就不再改动 ✓
所以同一份内容的层可以被任意多个镜像引用 ✓✓
这一点是后面"写时复制"和"构建缓存"的共同基础 ✓
实测 ✓✓:
镜像初始状态:2 层,逻辑大小 200 MB
容器启动后:新增可写层(初始 0 字节)→ 3 层
① 读 /app/config.yaml → 直接读下层
代价:0 字节(不复制)✓✓
② 改 /app/config.yaml → 只把这一个文件复制到可写层
代价:约 13 字节(不是复制整个 180 MB 的基础层)✓✓
下层仍是旧值,上层是新值 —— 这就是「写时复制」★ 三个由此推出的结论 ✓✓:
| # | 结论 | 说明 |
|---|---|---|
| ① | 容器里的修改不影响镜像 ✓✓ | 所以"删掉容器重跑"就回到干净状态(不可变基础设施的底气)✓ |
| ② | 容器启动几乎是瞬时的 ✓✓ | 因为可写层初始是空的,不用复制任何文件 ✓ |
| ③ | 同一镜像可以起 N 个互不干扰的容器 ✓✓ | 各自有独立的可写层 ✓ |
★★ 但写时复制有一个"反直觉"的坑 ✗✗:
上层改大量文件时,每个文件都要复制一次 → 可写层迅速膨胀 ✗
更常见的坑:日志直接写在容器里 ✗✗ —— 可写层越来越大,直到磁盘满(而镜像本身还是小的)✗
正确做法:日志输出到 stdout,数据写到挂载的卷 ✓✓
(这也是为什么
docker logs能看到日志 —— 它读的是容器外的日志文件 ✓)
★ 一条判断标准 ✓✓:
| 数据 | 放哪 |
|---|---|
| 日志 ★★★ | stdout(由容器运行时接管)✓✓ |
| 业务数据(数据库文件、上传文件)★★★ | 挂载卷 ✓✓ |
| 临时文件 ✓ | 可写层(随容器删除)✓ |
实测 ✓(改一行代码后重新构建):
① 常见写法:
COPY . /app ← 代码一改,这层缓存失效 ✗✗
RUN mvn dependency:resolve ← 缓存失效 → 重新下载全部依赖 ✗✗
RUN mvn package ← 重新编译
约 332 秒 ✗✗
② 优化写法:
COPY pom.xml /app ← pom 没变 → 缓存命中 ✓✓
RUN mvn dependency:resolve ← 缓存命中(0 秒)✓✓
COPY src /app ← 只有这层失效
RUN mvn package ← 只重新编译
约 30 秒 ✓✓(快了 11 倍)★★★ 一条规则(记住它就能省下大量 CI 时间) ✓✓:
Dockerfile 里的指令要按「变化频率」从低到高排列 ✓✓
text基础镜像 → 系统依赖 → 语言依赖(pom / package.json)→ 源码 → 构建产物1改动越频繁的东西越靠后,缓存命中率就越高 ✓✓
★ 另外三个常用优化 ✓✓:
| 手段 | 省下什么 |
|---|---|
| 多阶段构建(multi-stage) ★★★ | 构建工具链不进最终镜像(省几百 MB)✓✓ |
.dockerignore ★★★ | 不把 target/、.git 打进构建上下文 ✓✓ |
| 合并相关 RUN ★ | 减少层数(但别硬合并,会牺牲缓存)⚠️ |
★ 多阶段构建的样子 ✓:
# 阶段一:构建(用完整的 JDK 镜像)
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline # ★ 依赖层可缓存
COPY src ./src
RUN mvn package -DskipTests
# 阶段二:运行(只用 JRE,镜像小得多)
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /build/target/app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]★★ 收益 ✓✓:最终镜像从"JDK + Maven + 源码 + 依赖"(几百 MB) 变成"JRE + 一个 jar"(几十 MB)✓✓
★★★ 一条权衡 ✓✓:层数不是越少越好 —— 把 10 条 RUN 合并成 1 条确实少了层,但任何一处变化都会让这层全部重跑 ✗✗ 所以:相关操作用 && 合并,不相关的分开 ✓
★★ 必须说清 ✓✓:
容器 ≠ 虚拟机,也 ≠ 只有文件分层 ✓
它 = 文件系统分层(本节实测) + 命名空间隔离 + cgroups 资源限制 ✓✓
★ 隔离的六个维度 ✓:
| 隔离维度 | 靠什么实现 | 效果 |
|---|---|---|
| 进程视图 ★★ | PID namespace | 容器里看到自己是 PID 1 ✓ |
| 网络 ★★★ | Network namespace | 独立的网卡、IP、端口空间 ✓✓ |
| 主机名 ★ | UTS namespace | 容器有自己的 hostname ✓ |
| 挂载点 ★★★ | Mount namespace | 看到自己的文件系统 ✓✓ |
| 用户 ★★ | User namespace | 容器内 root ≠ 宿主 root ✓ |
| CPU / 内存 ★★★ | cgroups | 限制用量,超了被杀 ✓✓ |
★★ 与虚拟机的本质区别(一句话) ✓✓:
容器与宿主共享内核(所以启动是毫秒级、开销极小)✓✓
虚拟机各自跑一个内核(所以启动是分钟级、开销大)✓
★★★ 由此推出一条安全结论 ✓✓:
共享内核意味着「内核漏洞会穿透隔离」 ✗✗
· 多租户场景(跑别人的代码)→ 要用虚拟机,或 gVisor / Kata 这类沙箱 ✓
· 跑自己的服务 → 容器就够了 ✓✓
(这也是为什么 K8s 集群里不跑不可信镜像)✓
★ 常用命令(本模型做的事 vs 真实 docker) ✓:
| 本模型做的事 | 真实命令 |
|---|---|
LayerStore.acquire()(复用层) | docker pull(已存在的层会跳过)✓ |
Image 的层列表 | docker history <image>(看每层)✓✓ |
writableLayer | docker diff <container>(看改动了啥)✓ |
buildCache() | docker build(--no-cache 可禁用)✓ |
★ 四条最常用的排障命令 ✓✓:
docker logs -f <container> # 看日志(读的是 stdout 重定向的文件)✓✓
docker exec -it <container> sh # 进容器看现场 ✓✓
docker stats # 看 CPU/内存(读的是 cgroups)✓✓
docker inspect <container> # 看配置与网络 ✓| # | 做法 | 原因 |
|---|---|---|
| ① | 按变化频率排序指令 ★★★ | 缓存命中率(5.3)✓✓ |
| ② | 用多阶段构建 ★★★ | 最终镜像小、不含构建工具 ✓✓ |
| ③ | 配 .dockerignore ★★★ | 构建上下文小、避免把密钥打进去 ✓✓ |
| ④ | 用固定版本的基础镜像(别用 latest)★★★ | 可重复构建(《CI/CD 流水线设计》 的原则)✓ |
| ⑤ | 不用 root 运行(USER app)★★★ | 降低容器逃逸后的影响 ✓ |
| ⑥ | 只 COPY 需要的文件 ★★ | 少一层、少泄漏 ✓ |
| ⑦ | HEALTHCHECK 或由编排层配探针 ★★★ | 见 《Docker Compose 与多容器编排》、《Kubernetes 入门》 ✓ |
★★ 第 ④ 条容易被忽视 ✓✓:
FROM openjdk:latest意味着"今天构建和明天构建可能得到不同的镜像" ✗这直接违反"可重复"原则 ✓ —— 于是会出现"昨天还好好的,今天重新构建就挂了" ✗✗
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
USER 1001 # ★ 不用 root
ENTRYPOINT ["java", "-jar", "/app/app.jar"]见 5.3 ✓
# ✗ 日志写文件(可写层只增不减)→ 磁盘满
ENTRYPOINT ["java", "-jar", "/app/app.jar", "--logging.file.name=/app/logs/app.log"]
# ✓ 输出到 stdout(由运行时接管),数据写挂载卷
ENTRYPOINT ["java", "-jar", "/app/app.jar"]代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/container/ContainerModelLesson.java
运行:
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.container.ContainerModelLesson实测输出(关键部分) ✓:
===== 1. 分层复用 =====
① 不分层(每个镜像各存一份完整文件系统): 2000 MB ✗✗
② 分层(基础层共享): 380 MB ✓✓(物理占用 19 个层)
→ 省下 81% 的磁盘(层复用命中 9 次)✓✓
===== 2. 写时复制 =====
容器启动后:新增可写层(初始 0 字节)✓✓ → 3 层
① 读 /app/config.yaml → 代价:0 字节 ✓✓
② 改 /app/config.yaml → 代价:约 13 字节(不是 180 MB)✓✓
===== 3. 构建缓存 =====
① 常见写法:改动一行代码后重建约 332 秒 ✗✗
② 优化写法:改动一行代码后重建约 30 秒 ✓✓
→ 快了 11 倍 ✓✓★ 三条可落地的结论 ✓:
| # | 结论 | 依据 |
|---|---|---|
| ① | 统一基础镜像,让分层复用最大化 | 省 81% ✓ |
| ② | 日志出 stdout,数据挂卷 | 可写层会膨胀 ✗ |
| ③ | Dockerfile 按变化频率从低到高排 | 快 11 倍 ✓✓ |
docker history <image>:看每一层有多大 ✓✓(往往能发现"某层莫名其妙很大");docker diff <container>:看容器改了什么(找出写满磁盘的元凶)✓✓;docker stats:看 CPU/内存(这是 cgroups 的数据)✓;COPY . /app 换成只 COPY 需要的文件:对比镜像大小与构建时间 ✓✓;latest 改成固定版本:验证构建可重复 ✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | COPY . 放在装依赖之前 ✗✗ | 缓存全失效(332 秒 vs 30 秒)✓✓ |
| 2 | 没有 .dockerignore ✗✗ | 把 target/、.git、密钥打进镜像 ✓✓ |
| 3 | 用 latest 标签 ✗✗ | 构建不可重复 ✓ |
| 4 | 日志写容器内文件 ✗✗ | 可写层膨胀到磁盘满 ✓ |
| 5 | 数据不挂卷 ✗✗ | 容器删了数据没了 ✓✓ |
| 6 | 用 root 跑应用 ✗✗ | 逃逸后影响整个宿主 ✓ |
| 7 | 以为容器 = 轻量虚拟机 ✗ | 共享内核,隔离强度不同 ✓✓ |
目标:亲手量出"镜像有多大、构建有多慢"。
步骤:
ContainerModelLesson,记录省下的磁盘比例 ✓;.dockerignore,看构建上下文变小多少 ✓;COPY . 提前,量一次构建时间的退化 ✓✓;docker diff 一个跑了一天日志的容器,看可写层涨了多少 ✓。验收方式:能说出"你的镜像有几层、哪层最大、构建时的瓶颈在哪一步"。
扩展任务:把镜像从 500 MB 优化到 100 MB 以内:
FROM openjdk:latest
COPY . /app
RUN mvn package
ENTRYPOINT ["java", "-jar", "/app/target/app.jar"]这个 Dockerfile 有几个问题? 5. (设计)给你的 Spring Boot 应用写一个 Dockerfile:要求最终镜像尽量小、构建尽量快。
latest 不可重复 ✗;② COPY . 在装依赖之前 → 缓存全失效 ✗✗; ③ 没有多阶段构建 → 镜像里带了 Maven 与源码 ✗✗;④ 没有 .dockerignore(会把 target/、.git 打进去)✗dependency:go-offline,最后 COPY src ✓✓; 固定版本标签、USER 非 root、.dockerignore ✓;日志出 stdout ✓.dockerignore 解决什么 ★★★;《Docker Compose 与多容器编排》:为什么"本地能跑、CI 里偶发失败"。