KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
01 · 为什么「私有化」不是「多买一台服务器」 — keel 龙骨
这一章先不装任何东西。它要解决的是另一件事:把「交付一套系统」拆成几段可以分开判断的路—— 运行它所需的东西从哪来、怎么进得去、进去之后按什么顺序落地、出了事谁能自己查。 拆不开这四段,交付就只能是那句话:「在我机器上明明是好的」。
这一章先不装任何东西。它要解决的是另一件事:把「交付一套系统」拆成几段可以分开判断的路——
运行它所需的东西从哪来、怎么进得去、进去之后按什么顺序落地、出了事谁能自己查。
拆不开这四段,交付就只能是那句话:「在我机器上明明是好的」。
一、为什么会有「私有化交付」这件事
任何一个跑在公网环境里的系统,都有一句代价很小的解释:它依赖的那些东西,是你替它准备好的。
镜像从公共仓库拉,依赖包从公网装,证书有服务自动签,日志有平台自动收。这些动作没有一条写在你自己的代码里,却每一条都是它跑起来的前提。当同一套系统被送进一个**没有出口的机房时,这些前提会一条一条消失——不是变慢、不是偶尔失败,是没有**。
于是「部署」这个词的含义变了:
| 你以为的部署 | 真正的私有化交付 |
|---|---|
| 把代码放到服务器上、跑起来 | 把跑起来所需的一切放进一个能被搬动的介质里 |
| 缺什么就装什么 | 装什么必须提前就在介质里,现场没有可装的东西 |
| 出问题看日志、查文档、找人 | 出问题要客户自己先能查出一部分答案 |
| 回滚 = 把上一次的包再部署一遍 | 回滚 = 镜像、库结构、数据三样都要能退回去 |
这里有一条界线必须先划清:私有化交付不是「部署到更远的一台机器」。 部署的前提是那张网还在——你能从它上面拿到需要的东西;交付的前提是它不在了。这条差别的后果会贯穿这门课的每一章:从镜像里放什么、包有多大,到安装脚本为什么不能问问题、回滚脚本第一行该取哪个镜像。
还有一条更早的界线:选定「私有化」是上一门课的判断,不是这一课的。 什么场景该做成内网交付、什么场景该做成多客户共用一套服务,那是一个形态选择问题,见《构建打包与部署上线》的形态演进一章。本课从「已经选定私有化」这句话之后开始往下走,只讲执行。
二、为什么今天更值钱
这不是行情问题,是三个可核查的事实堆出来的:
事实一:能跑起来的最小环境,比你的代码重三个数量级。
一个 python:3.12-slim 基础镜像 179MB,一个只装了 psycopg[binary]==3.2.9 的虚拟环境 28.2MB,而应用自己的代码是 61.4kB。所以一份「能跑起来」的制品是 215MB。要说清这件事只需要一句话:交付的重量不在你的代码上。 这句话决定了整条交付链路的形状——包的体积、搬运的方式、校验的必要性,全都由它推出来。
事实二:「从仓库拉」这件事本身是有前提的。
构建镜像时遇到过这样一次失败:
unexpected status from HEAD request to http://hub-mirror.c.163.com/v2/library/python/manifests/3.12-slim: 502 Bad Gateway
换一次重试就成功了。但这一点很重要:镜像仓库会挂,网络会抖,代理会拦。 在能上外网的机器上,这些是「重试一次就好」的小事;在客户内网里,连「重试」这个动作都不存在——那条出口本来就不通。
事实三:一次交付要经过的环节,每一个都要能独立成立。
从我们这边打出包,到客户机器上跑起来,中间至少四个环节:打包、搬运、安装、自检。交付物 160M,要跨过一条只能靠介质(U 盘、摆渡机、刻录盘)通过的缝隙。这条缝隙里没有任何一方能「顺手看一眼」——所以每一个环节都必须自带验证,而不能依赖上一环没问题。
三、它长什么样
先给一眼形态。一份交付包解开之后,就是这些东西:
board-delivery-1.0.0/
MANIFEST.sha256 每个制品的哈希,现场靠它判断包有没有在搬运里坏掉
images/
board__1.0.0.tar 应用镜像(docker save 直出,未压缩)
postgres__16-alpine.tar 数据库镜像 —— 别漏了这个
compose.yml 客户内网版编排文件,被直接使用的那一份
env.example 配置模板;真的 .env 由安装脚本照它现场生成
install.sh 客户唯一需要知道的那条命令
upgrade.sh rollback.sh selfcheck.sh
现在只给这一眼,逐项展开在后面各章。 这一眼要带走的是:私有化交付的「产物」长得不像一份文档,它长得像一个装了两样不可再生的制品、一份编排文件、四个脚本的目录。
回到第一节那句「把跑起来所需的一切放进一个能被搬动的介质里」,这个目录里每一行都在兑现它:
| 你看到的 | 它兑现了第一节的哪一句 |
|---|---|
images/*.tar 两个未压缩镜像 |
「所需的一切」里最重的是运行环境,而不是代码 |
| 连数据库镜像也在包里 | 「现场没有可装的东西」——少一个镜像,装机就停在那里 |
MANIFEST.sha256 |
跨过那条介质缝隙之后,第一件要能做的事是「这份包有没有坏」 |
compose.yml 只有 image:、没有 build: |
「现场没有可装的东西」的另一面:现场连构建都不做 |
env.example 里密码位是空的 |
密钥不能跟着介质一起流转,它只能在现场产生 |
install.sh 是唯一的入口 |
少一样要背的命令,就少一样现场会记错的事 |
四、怎么解决:把一条交付拆成四段能独立成立的路
4.1 一次交付要经过哪几道环节
把上面那个目录展开成一次完整交付,形状是这样的:
flowchart TD
subgraph OURS["我们这边(有外网)"]
O1["构建应用镜像 board:1.0.0"]
O2["备好数据库镜像 postgres:16-alpine"]
O3["算 MANIFEST.sha256 并打包"]
end
subgraph MEDIA["可搬运介质(U 盘 / 摆渡机)"]
M1["board-delivery-1.0.0.tar.gz"]
end
subgraph SITE["客户机房(无外网)"]
S1["① 前置检查:docker 在不在、daemon 活不活、磁盘够不够"]
S2["② 校验制品:sha256 对 MANIFEST"]
S3["③ docker load 两个镜像"]
S4["④ 生成现场配置(密钥就地产生)"]
S5["⑤ 起栈并自检"]
end
O1 --> O2 --> O3 --> M1
M1 --> S1
S1 -->|"① 通过"| S2
S2 -->|"② 通过"| S3
S3 -->|"③ 通过"| S4
S4 -->|"④ 通过"| S5
S1 -.->|"① 失败:docker 命令在,但连不上 daemon"| X1["就地停下,不产生任何副作用"]
S2 -.->|"② 失败:制品校验失败,不要继续安装"| X1
S3 -.->|"③ 失败:No such image"| X1
这张图里有两个必须看清的东西:
- 现场那五步的顺序不能换,而且失效点全部前移到了「产生副作用之前」。② 校验在 ③ 导入之前,③ 导入在 ④ 生成配置之前——任何一步失败,前面已经成立的东西都不受影响。这是交付脚本和普通部署脚本最大的区别:部署可以失败一半再来一次,交付不能。
- 虚线那三条是同一个动作:停下。不是「记录一下继续」,也不是「跳过这一项」。因为客户现场没有我们的人,一次半成功的安装留下的状态,比一次干净的失败难查得多。
4.2 把镜像送进去,其实只有三条路
「怎么让客户机器上有这个镜像」这个问题,可选的动作比看上去少得多。逐一试一遍就知道只剩一条:
| 路 | 现场需要什么 | 结论 |
|---|---|---|
| 现场构建 | 外网(拉基础镜像、装依赖)+ 构建工具链 | 走不通。现场两样都没有,docker build 的第一步就会停 |
让现场去 docker pull |
到公共仓库的出口 | 走不通,而且失败形态最坏——它不报「没有网络」,它挂在那里等超时 |
| 随包直送镜像 | 只有 docker load |
唯一成立的一条 |
第二行值得多看一眼,因为它的失败长得很像网络问题。默认行为下,本地找不到镜像时 compose 会去仓库取:镜像名被打印成 Pulling,然后是一段超时,最后报一句 failed to resolve reference,看起来像「客户网络不稳」。真相是这个镜像根本没从包里导进来。04 章会把「怎么把这种失败变成一句立刻能懂的话」讲清楚。
4.3 与相邻课程的分工
私有化交付站在好几门课的接缝上,这里一次划清,后面各章不再重复:
| 相邻课程 | 它管什么 | 本课只讲什么 |
|---|---|---|
| 《构建打包与部署上线》 | 制品是什么、部署策略、发布回路、交付形态怎么选 | 不重讲选型。选定私有化之后,怎么执行这条线 |
| 《Docker 应用课》 | Dockerfile 语法、层与缓存、compose 解析、信号与排障 | 不重讲语法。只讲「面向交付」那部分约束,以及它在这里的后果 |
| 《CI 流水线与质量门禁》 | 流水线、门禁、供应链安全 | 不重讲 CI。本课从「制品已经打好了」开始 |
| 《Kubernetes 应用课》 | 声明式、探针、滚动更新 | 不重讲 K8s。私有化现场多数不装 K8s,这一点在第七章点明 |
一句话记住分工:那四门课回答「怎么做出来的、怎么发出去的」,本课回答**「怎么让它在没有你的地方跑起来,并且出事时客户能自己查」**。
4.4 逐行兑现第三节那一眼
| 第三节里的那一行 | 对应本节哪条结论 |
|---|---|
images/board__1.0.0.tar |
4.2 的第三行:镜像只能随包直送,所以它必须出现在包里 |
images/postgres__16-alpine.tar |
4.2 的第二行:少了它,现场会去 pull,然后挂在那里等超时 |
MANIFEST.sha256 |
4.1 的第 ② 步:跨过介质缝隙之后,先证明包没坏,再谈安装 |
compose.yml 只有 image: |
4.2 的第一行:现场不构建,所以编排文件里也不能有 build: |
env.example 里密码留空 |
4.1 的第 ④ 步:密钥在生成现场配置那一刻才存在 |
install.sh 包住①②③④⑤ |
4.1 的整张图:五步顺序固定,任何一步失败都在产生副作用之前停 |
动手:可观察结果
交付包已经在仓库里生成好了。先只做一件事——把它解开看清单,不安装:
tar -tzf bundle/board-delivery-1.0.0.tar.gz
要观察三件事,而不是「看到一堆文件名就算跑过」:
- 顶层只有两样东西是「不可再生」的:
MANIFEST.sha256和images/下那两个.tar。
其余全是文本文件,你随时能再生成一份;那两个 tar 一旦丢了,就必须回到有外网的那台机器上重打一次。 images/下是几个镜像包? 如果只有一个,说明数据库镜像漏了——这是这类交付最常见的漏项,
而它的症状要等到客户机器上才出现(4.2 第二行那种「挂在那里等超时」)。- 有没有
.env? 交付包里不应该有它。看到它说明某台开发机上的现场配置被误打进了包,
而那份配置里带着上一个客户的密码。
完成标志:你能不看表就说出「这个包里哪几样东西丢了要重来、哪几样东西随时能再生」。
故障注入
把 MANIFEST.sha256 里任意一行改动一个字符(模拟介质搬运途中坏掉一个字节),然后重新校验:
sha256sum -c MANIFEST.sha256
预期结果是一条 FAILED 加一句 WARNING: 1 computed checksum did NOT match。
这一条值得停一下,因为它是整条链路里唯一一处「不信任任何人」的设计:校验不通过时,
脚本不是「记一条警告继续装」,而是当场停住并说清下一步该干什么。原因在 4.1 那张图的虚线里——
现场没有我们的人,一次半成功的安装留下的状态,比一次干净的失败难查得多。
生产边界
| 教学里这样 | 真实交付要换成 | 不换会出什么事 |
|---|---|---|
| 一个交付包,靠 U 盘搬 | 按客户规模选介质:摆渡机、内网文件服务、刻录介质 | 包越大(本课是 160M),搬运途中的截断概率越高,MANIFEST 从「可选校验」变成必需 |
| 镜像与编排文件放在同一层目录 | 制品库 / 离线仓库 + 编排文件分开版本化 | 编排文件的修改无法独立发布,改一行端口就要重搬一整个包 |
| 一台机器一套实例 | 一台机器多实例,靠资源名前缀隔离 | 装第二个客户时容器名、网络名、卷名全部撞车(05 章的解法) |
人工读 install.sh 的输出判断成败 |
退出码被监控系统接走 | 自检的结论停在屏幕上,没有人知道昨晚那次升级到底成没成 |
自测题
- 用一句话说清「部署到更远的一台机器」和「私有化交付」的区别,并指出这个区别
在第一节那张对照表里对应哪两行。 - 一个应用镜像 215MB,其中应用代码 61.4kB。这两句话各自会改变交付链路上的哪一步?
如果代码变成 6MB,包的体积会变成多少量级? - 4.2 的三条路里,为什么「让现场去
docker pull」的失败形态比另外两条更坏? - 「交付要能停在任何一步」与「部署可以失败一半再来一次」这两句话的差别是什么?
举出一个「半成功」比「干净失败」更难查的具体场景。
现在能解释什么
- 为什么「私有化交付」不是部署的加强版,而是前提变了一种情况;
- 为什么交付的重量不在代码上,以及这句话怎么一路推出整条链路的形状;
- 一份交付包最小长什么样,以及那份清单里每一行各自在兑现哪一条约束;
- 一次交付要经过哪五道现场环节、为什么它们的顺序不能换、失败为什么要停住;
- 把镜像送进内网只有三条路,以及另外两条为什么走不通——它们将在 03、04 章被逐个打开。