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

要观察三件事,而不是「看到一堆文件名就算跑过」:

  1. 顶层只有两样东西是「不可再生」的:MANIFEST.sha256 和 images/ 下那两个 .tar。
    其余全是文本文件,你随时能再生成一份;那两个 tar 一旦丢了,就必须回到有外网的那台机器上重打一次。
  2. images/ 下是几个镜像包? 如果只有一个,说明数据库镜像漏了——这是这类交付最常见的漏项,
    而它的症状要等到客户机器上才出现(4.2 第二行那种「挂在那里等超时」)。
  3. 有没有 .env? 交付包里不应该有它。看到它说明某台开发机上的现场配置被误打进了包,
    而那份配置里带着上一个客户的密码。

完成标志:你能不看表就说出「这个包里哪几样东西丢了要重来、哪几样东西随时能再生」。

故障注入

把 MANIFEST.sha256 里任意一行改动一个字符(模拟介质搬运途中坏掉一个字节),然后重新校验:

sha256sum -c MANIFEST.sha256

预期结果是一条 FAILED 加一句 WARNING: 1 computed checksum did NOT match。

这一条值得停一下,因为它是整条链路里唯一一处「不信任任何人」的设计:校验不通过时,
脚本不是「记一条警告继续装」,而是当场停住并说清下一步该干什么。原因在 4.1 那张图的虚线里——
现场没有我们的人,一次半成功的安装留下的状态,比一次干净的失败难查得多。

生产边界

教学里这样 真实交付要换成 不换会出什么事
一个交付包,靠 U 盘搬 按客户规模选介质:摆渡机、内网文件服务、刻录介质 包越大(本课是 160M),搬运途中的截断概率越高,MANIFEST 从「可选校验」变成必需
镜像与编排文件放在同一层目录 制品库 / 离线仓库 + 编排文件分开版本化 编排文件的修改无法独立发布,改一行端口就要重搬一整个包
一台机器一套实例 一台机器多实例,靠资源名前缀隔离 装第二个客户时容器名、网络名、卷名全部撞车(05 章的解法)
人工读 install.sh 的输出判断成败 退出码被监控系统接走 自检的结论停在屏幕上,没有人知道昨晚那次升级到底成没成

自测题

  1. 用一句话说清「部署到更远的一台机器」和「私有化交付」的区别,并指出这个区别
    在第一节那张对照表里对应哪两行。
  2. 一个应用镜像 215MB,其中应用代码 61.4kB。这两句话各自会改变交付链路上的哪一步?
    如果代码变成 6MB,包的体积会变成多少量级?
  3. 4.2 的三条路里,为什么「让现场去 docker pull」的失败形态比另外两条更坏?
  4. 「交付要能停在任何一步」与「部署可以失败一半再来一次」这两句话的差别是什么?
    举出一个「半成功」比「干净失败」更难查的具体场景。

现在能解释什么

进入 keel 阅读