🧠 第十章 管理和部署
📄 一句话概括

5 层镜像共 705 MB,改一行代码只要重传 2 MB; 不分层打成一个大文件则要重传 705 MB,差 352 倍。容器最实际的收益是分层复用,不是隔离。

📝 本节和前面几节的关系

前三种部署讲的是「算力放在哪」。这一节讲的是另一件事:怎么保证「我这跑得通,你那儿也跑得通」。

一、容器真正解决的是环境一致

「在我机器上是好的」这句话背后是一堆版本号:Python 3.10 还是 3.12、numpy 1.26 还是 2.0、 glibc 什么版本、CUDA 驱动哪一版。 容器把这些全部打包进镜像:操作系统层、运行时、依赖、代码、模型。 跑的时候不管宿主机装了什么,用的都是镜像里的那一套。 所以它的第一个收益是可移植:开发机、测试机、生产机跑的是同一个镜像,不会因为环境差异出岔子。 第二个收益才是隔离:容器之间看不到对方的文件系统和进程,一个容器崩了不影响别的。

二、分层复用的算术

镜像不是一个大文件,是一叠只读的层:基础系统一层、Python 运行时一层、依赖一层、你的代码一层、 模型权重一层。 关键规则:层按内容寻址,内容没变就不重新传输。 所以改一行代码,只需要重传代码那一层。 Demo 里 5 层合计 705 MB:基础系统 120、Python 85、numpy 18、代码 2、权重 480。 改代码 → 只重传 2 MB;换权重 → 重传 480 MB; 如果打成一个大文件,改一行也要重传 705 MB,差 352 倍。 这就是容器化在 CI 里快的原因,也是回滚只换一层就能完成的原因。

三、实测的口径要交代

这五个层大小是我按常见发行版的量级填的,不是实测——沙箱里没有装 Docker。 所以 705 MB 这个绝对值不用记,要记的是比例关系:代码层占千分之三,权重层占三分之二。 这个比例很典型:真正占地方的从来不是你的代码,是模型权重和运行时。 所以「镜像太大」的解法通常是换更小的基础镜像、把权重挂载出去,而不是优化代码。 另一个常被忽略的点:层顺序影响缓存命中。 把变化频繁的层(代码)放最后,把稳定的层(系统、依赖)放前面,构建缓存才能最大化利用。 反过来写,每次改代码都会让后面所有层失效。

四、代价与常见坑

有状态的东西很难塞。数据库、缓存放进容器,数据要么挂卷、要么随时会丢。 所以通常是「应用容器化,数据用托管服务」。 镜像会膨胀。构建时临时装的编译工具如果没清理,会永久留在那一层里。多阶段构建就是为这个存在的。 容器保证环境一致,但不保证依赖版本一致。 镜像里的 numpy 是 2.0,训练时用的是 1.26,推理结果的细微差别照样存在。 所以依赖必须钉死版本,不能写 numpy>=1.20。 最后一条最容易踩:镜像打了、跑起来了、结果和训练时对不上,查半天发现是一个底层库的小版本差异。 钉版本这个动作只要一行,但大多数人是在出过事之后才补上的。

五、动手 Demo:分层算术:改代码重传 2 MB,不分层要 705 MB

按典型量级列出一个推理镜像的 5 个层(基础系统、Python 运行时、numpy、应用代码、模型权重), 算出总大小;再分别算三种场景下的实际传输量:只改代码、只换权重、以及打成一个大文件后改代码。

layers = [(u"base-os", 120), (u"python-runtime", 85),
          (u"numpy", 18), (u"app-code", 2), (u"model-weights", 480)]

total = sum(sz for _, sz in layers)        # 705
total / 2.0                                # 与单文件重推的倍数

实跑输出:

--------------------------------------------------------------------
容器化部署:共享层让改代码只重拉 2 MB,不是 700 MB
--------------------------------------------------------------------
  镜像总大小               705 MB(5 层)

  三个镜像的层清单:
  场景              需要传输的层                      本次传输量   累积占用
  ------------------------------------------------------------------------
  v1 首次           全部 5 层                             705 MB     705 MB
  v2 改了代码       复用前 3 层,重推 app-code              2 MB     705 MB
  v3 换了权重       复用前 3 层,重推 model-weights       480 MB     705 MB
  不分层单文件 v2   整个镜像重推                          705 MB     705 MB

  改一行代码,共享层让它只需要传 **2 MB**;如果打成一个大文件,要传 **705 MB**,差 **352 倍**。
  这是容器化最实际的收益:不是「隔离」,是**分层复用**。CI 里构建快、部署时拉取快、回滚时只换一层,都是它带来的。

  代价也要说清:层数越多,管理越复杂;有状态的东西(数据库、缓存)塞进容器会很难受;而且容器只保证**运行环境一致**,不保证**依赖版本真的一致**——那要靠锁文件。

  最后一条最容易踩:镜像里的 numpy 版本和训练时不一样,推理结果可能就有细微差别。所以依赖必须钉版本,不能写 numpy>=1.20。

  要说明的是:这五个层大小是我按常见发行版的量级填的,不是实测——沙箱里没有装 Docker。所以 705 MB 这个绝对值不用记,要记的是**比例关系**:代码层占千分之三,权重层占三分之二。

done -> _out_38n.txt

改代码 2 MB、换权重 480 MB、单文件重推 705 MB。 三者的比例决定了日常迭代快不快:绝大多数提交都只改代码,所以日常只传 2 MB。 第一次拉取仍然要 705 MB,这一点没法省。所以容器化改善的是第二次以后的传输,不是第一次。 「累积占用」那列都是 705 MB,因为共享层在本地只存一份。 这也是分层的好处之一:同机的多个镜像共享基础层,磁盘不会按镜像数翻倍。 再强调一次:这五个层大小是量级估计,不是实测。 换成真实的镜像,绝对值会差很多,但「代码层极小、权重层极大」这个结构不会变。

30 秒实验

  1. 把权重层从 480 MB 改成 2400 MB(一个 7B 模型的量级),看重传比例怎么变。
  2. 算一算:如果基础层从 120 MB 换成 5 MB 的精简镜像,首次拉取省多少。
  3. 想一想:为什么层次顺序要「稳定的放前面、易变的放后面」?

常见误区

  1. 「容器化主要是为了隔离」——最实际的收益是分层复用和可移植。
  2. 「镜像越大越慢」——首次拉取慢,但日常迭代只传变化的那一层。
  3. 「有容器就不用管依赖版本」——容器只保证环境一致,版本还得自己钉。
自测三问(点开对答案) 1. 容器化的两个主要收益是什么? —— 可移植(环境一致)和隔离(互不影响),实际收益里分层复用也很关键。 2. 为什么改一行代码只需要重传很小的层? —— 层按内容寻址,内容没变的层直接复用,只传变化的那一层。 3. 为什么依赖必须钉死版本? —— 容器保证环境一致,但不保证库版本与训练时相同,版本差异会改变推理结果。

四种部署方式讲完了。最后一节是本章的实战:从零发布一个 ChatGPT 应用。

相关笔记