0%

Linux 包管理番外篇一次把三篇理论全部踩实的真实事故

Linux 包管理番外篇 · 一次把三篇理论全部踩实的真实事故

写在前面:为什么要有这篇番外

前面三篇,我们站在三十年演化史的高度,把 Linux 包管理的”为什么”讲透了——为什么会有那条”一个文件只能属于一个包”的铁律,为什么共享库的升级牵一发而动全身,为什么发行版宁愿冻结版本号也要稳定。你现在脑子里,应该已经有了一整套心智模型。

但是,懂原理和会救火,是两回事。

原理是平静的、抽象的;而真实的故障,往往在某个你没准备好的时刻,以一个具体而棘手的形式砸到你面前。那一刻,你不会想起”铁律”这个词,你只会看到一行冷冰冰的报错、一个起不来的服务、和一台你可能正在失去控制的服务器。能不能在那一刻,从一团乱麻里认出”哦,这就是书上说的那个共享库连锁升级”,并冷静地一步步把它拆开——这才是真正的功夫。

陆游有句诗:”纸上得来终觉浅,绝知此事要躬行。”这篇番外,就是那次”躬行”。我会带你完整地走一遍一桩真实事故:我本想给一台服务器升级 nginx 修个漏洞,结果一步步把自己逼到差点登不上服务器。 前三篇所有的抽象概念——铁律、共享库、ABI、backport——都会在这个故事里一个个”活”过来。更重要的是,我会把每一步排查时脑子里在想什么、为什么用这条命令、它的输出该怎么读,全部摊开给你看。因为真正值钱的不是命令本身,而是命令背后那套”破案的思维”。

故事开始。

第一幕:一个看似无害的升级念头

环境:Rocky Linux 9.1,上面跑着系统源安装的 nginx 1.20.1。某天做安全巡检,想确认并修复 nginx 的已知 CVE。

我先查了一眼系统源里能升到什么版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dnf info nginx
# (示意输出,版本号为真实场景)
Installed Packages
Name : nginx
Epoch : 1
Version : 1.20.1
Release : 14.el9
...
Available Packages
Name : nginx
Epoch : 2
Version : 1.20.1
Release : 28.el9_8.2.rocky.0.1
...

我盯着那个 1.20.1 直皱眉:”都什么年代了还 1.20.1?上游 nginx 都出到 1.27 了,这也太旧太不安全了吧!我要去 nginx 官网装最新的 1.30!”

——就是这个念头,把我拖进了后面整整一晚上的麻烦。

这里必须先停下来,讲清一个被无数人误解的概念,否则你会和当时的我一样,做出错误的判断。

概念补课:什么是 backport(补丁回移植)

RHEL、Rocky、openEuler 这类企业级/国产化发行版,遵循一条铁规矩:在一个大版本的整个生命周期里,上游软件的版本号是”冻结”的、不动的;但安全补丁会被单独”摘”过来,打到这个冻结的版本上。 这个动作就叫 backport(回移植)。

举个例子:上游 nginx 早已出到 1.27,但 Rocky 9 偏偏把它锁死在 1.20.1。当 1.20.1 爆出某个 CVE 时,Rocky 不会把你升级到 1.27,而是把那个 CVE 的修复代码,单独移植回 1.20.1 的源码里,重新编译,然后通过抬高 RPM 的 Release 号来发布——你看到的 -14 变成 -28,就是这么来的。

所以,关键结论是:1.20.1-28 这个看起来”很旧”的版本,实际上已经包含了截至目前所有重要 CVE 的修复。版本号没变,绝不代表没修。

为什么发行版要这么折腾?因为它在用一种”反直觉的智慧”换取稳定性:版本号不变,意味着软件的行为、配置格式、API 都不变,你的业务不会因为一次安全升级而崩掉;而安全性又通过 backport 得到了保证。稳定与安全兼得,代价是放弃新功能。 对生产环境来说,这往往是最优解。

那怎么知道某次更新到底修了什么?

讲到这里,你一定会冒出一个非常实际的问题:既然版本号被冻结、看不出新旧,那我怎么知道某次更新到底修了什么、有没有包含我关心的那个安全补丁? 这正是 backport 模式下最该掌握的一项技能——学会读 changelog 和安全公告,而不是盯着版本号猜。

第一,看一个包到底改了哪些东西:changelog。

每个 RPM 包内部,打包者都维护着一份 changelog(变更日志),逐条记录”这一版相比上一版,修了什么 bug、补了哪个 CVE”。查看方式分两种情况:

1
2
3
4
5
6
7
# 已经装在系统上的包,用 rpm 查(最通用,任何 RHEL 系都有):
rpm -q --changelog nginx | head -40

# 还没装、只想看看仓库里那一版改了啥,用 dnf changelog:
dnf changelog nginx
# 只看相比当前已装版本"新增"的那部分变更(最实用):
dnf changelog --upgrades nginx

这里有两个坑要提醒:

其一,dnf changelog 来自 dnf-plugins-core 插件,某些最小化安装的系统上可能要先 dnf install dnf-plugins-core;老一些的系统若没有这个子命令,就退回用 rpm -q --changelog(查已装)或 dnf repoquery --changelogs nginx(查仓库里未装的)。

其二,changelog 是否详尽、CVE 编号写没写全,取决于打包者——RHEL/Rocky 这种企业级发行版通常写得很规范,每条 backport 都会标注对应的 CVE 号和内部跟踪号。

读 changelog 时,你会看到类似这样的条目(示意):

1
2
3
* Mon May 20 2024 Rocky Builder <...> - 1:1.20.1-28
- Resolves: CVE-2024-7347 (backported fix for mp4 module)
- Security fix backported from upstream

看到这种 CVE-xxxx-xxxx 字样,就坐实了:这一版虽然还叫 1.20.1,但确实把那个 CVE 的修复打进来了——这就是 backport 留下的、看得见的证据。

第二,直接问”有哪些安全更新待装”:updateinfo。

如果你不想逐个包翻 changelog,而是想从全局问一句”我这台机器现在有哪些安全更新可以装”,dnf 有专门的安全公告查询:

1
2
3
4
5
6
7
8
# 列出所有可用的安全公告(及对应 CVE)
dnf updateinfo list --security

# 看某个安全公告的详情
dnf updateinfo info <公告ID>

# 干脆只安装安全类更新,别的不动:
dnf update --security

dnf updateinfo list --security:列出仓库里有、但本机还没装的安全更新(即”还差哪些安全补丁没打”);想查”已经打过哪些”,加 --installed

dnf update --security 在生产环境尤其顺手——它只挑安全修复来装,不会顺手把一堆功能性更新也带上,改动面小、风险低,正合 backport 模式”只补安全、不动功能”的脾性。

第三,权威核对:查发行版的 errata(勘误)数据库。

最权威的方式,是去发行版官方的安全公告库核对。Rocky 的在 errata.rockylinux.org、RHEL 的在 access.redhat.com 的 errata 页、openEuler 也有自己的安全公告中心。你拿着一个 CVE 编号去搜,就能查到”哪个版本的 RPM 修复了它”——再和你机器上 rpm -q 出来的版本一对,是否已修,一目了然。

掌握了这三招,你就彻底摆脱了”靠版本号大小猜安全性”的误区:版本号是冻结的表象,changelog 和安全公告才是 backport 留下的真凭实据。

接下来,我们干脆把 1.20.1-28.el9_8.2.rocky.0.1 这一长串名字逐段拆开——它看着唬人,其实每一段都在讲述这个包的身世,读懂它,你就再也不会把 backport 的版本号看错了。

前半截 1.20.1上游版本号(nginx 自己的版本),这个被冻结了。而 - 之后的 28.el9_8.2.rocky.0.1 这一整串,在 RPM 术语里叫 Release(发行号),是接在版本号后面、由发行版打包者掌控的那部分。我们从左到右拆:

  • 28 —— 这是核心发行号,表示”这个上游版本(1.20.1),被发行版打包/重新构建的第 28 次”。这才是 backport 的计数器:每 backport 一个补丁、重新打一次包,这个数字就 +1。从 -14-28,意味着中间经历了十几次打包迭代,补丁是一点点累积进来的。它跟上游版本号毫无关系,纯粹是发行版自己的迭代次数。

  • el9 —— enterprise linux 9(企业级 Linux 9)的缩写,标明这个包是为 RHEL 9 这条产品线构建的(Rocky、AlmaLinux、openEuler 的部分包都沿用这个标记,因为它们与 RHEL 二进制兼容)。这是包的”目标平台户口”。

  • _8 —— 紧跟在 el9 后面的这个,表示 RHEL/Rocky 9 的小版本(minor release)是 9.8。也就是说,这个包是针对 9.8 这个小版本构建发布的。还记得这场惨案的关键吗?那台机器表面是 9.1,但仓库已经走到了 9.8 这一支——_8 就是这个小版本的烙印,正是它对应的那次 9.8 库升级,把 OpenSSL 从 3.0 推到了 3.5。

  • .2 —— 在 el9_8 这个小版本基线之内的再次构建/修订号。同一个 9.8 基线下,如果这个包又因为某个紧急修复重打了一次,就用这个数字区分。

  • .rocky —— 标明这是 Rocky Linux 出品的构建,而不是上游 Red Hat 的原版。Rocky 在重新构建 RHEL 源码时,会打上自己的厂商标记,以示区分(AlmaLinux 会打 .alma,以此类推)。这是包的”出品方签名”。

  • .0.1 —— Rocky 自己在重新构建这个包时的内部修订号。Rocky 拿到 RHEL 的源码后重新编译,偶尔需要做极小的调整(比如改个签名、修个构建脚本),这个尾号就用来标记 Rocky 侧的这种微调次数。

把这一长串连起来读,它其实是一句完整的话:“这是 nginx 1.20.1,由 Rocky(.rocky)为企业级 Linux 9(el9)的 9.8 小版本(_8)构建,是该上游版本的第 28 次打包(28)、9.8 基线内的第 2 次修订(.2)、Rocky 侧的第 0.1 次微调(.0.1)。” 你看,版本号 1.20.1 三十年没变,可后面这一长串 Release,密密麻麻记录着它被打磨、被 backport 了多少次——这串”啰嗦”的后缀,恰恰是企业级发行版”稳定外壳下,安全内核一直在更新”的最好证明。

可惜,当时的我没参透这一层。我执意要追上游的 1.30。于是,故事进入第二幕。

第二幕:铁律拦路——原来这不是”升级”,是”换源”

我配好了 nginx 官网(nginx.org)的仓库源,然后习惯性地敲下升级命令:

1
dnf upgrade nginx

结果,dnf 在事务测试阶段直接报出文件冲突,升级被中止,一个文件都没动

如果你读过正篇,这一幕你应该秒懂:这正是那条铁律在发威。系统源的 nginx,它的 /usr/sbin/nginx 等文件归 nginx-core 这个包所有(.el9 标记);而 nginx 官网那套包是另一个完全独立的打包体系(.ngx 标记),它也要往一模一样的路径放自己的文件,**却没有声明 Obsoletes(没有”我取代你”的所有权交接书)**。在 RPM 眼里,这不是合法的版本升级,而是”两个互不相识的包,抢同一批文件的所有权”——铁律不容,当场中止。

这里有个认知必须扭转过来:我想做的根本不是”升级”,而是”换源”。 “升级”是在同一个打包体系内、版本号往上走;而我这是要从”发行版体系”跳到”上游官方体系”,是两套互不相识的体系之间的迁移。铁律决定了:换源不能靠 upgrade 硬来,只能先把旧的彻底请出去、腾空文件归属,新的才能入住

于是正确的操作是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 先备份你改过的配置(卸载会带走它们)
# 这里用了 bash 花括号展开,等价于 cp -a /etc/nginx /etc/nginx.bak.2026-06-07
# 带上日期戳,多次备份不会互相覆盖,一眼能看出哪天备的
cp -a /etc/nginx{,.bak.$(date +%F)}

# 2. 卸掉系统源那套 nginx —— 不用去背它有哪些子包,
# 敲下面这条后,dnf 会把要卸的全家(nginx-core、nginx-filesystem 等)列出来让你确认
dnf remove nginx
# 若发现清单里子包没被带上,改用通配符一次卸干净(务必看清清单再确认):
dnf remove 'nginx*'

# 3. 配好 nginx.org 官网源后,从它安装新版
dnf install nginx
# 此时 /usr/sbin/nginx 已经"无主",1.30 顺利入住

装完,nginx -v 一看,1.30 跑起来了。我长舒一口气,以为大功告成——直到我退出终端、想重新 SSH 登录上来。

第三幕:晴天霹雳——sshd 起不来了

新开的 SSH 连接,登不上了。

(这里先插一句保命的话:幸亏我那条用来操作的旧 SSH 会话还连着没关。这个习惯后面会专门讲,此刻它是我唯一的救命稻草。)

我在那条还活着的会话里,赶紧查 sshd 的状态:

1
2
3
4
systemctl status sshd
journalctl -u sshd | tail
# 日志里赫然一行:
sshd: OpenSSL version mismatch. Built against 30000010, you have 30500050

看到这行,我的第一反应是:我明明只装了个 nginx,怎么扯到 OpenSSL 了?又怎么会把 sshd 干崩?

先把这行报错翻译成人话。那两个十六进制数字,是 OpenSSL 的版本编码(格式是 0xMNNFFPPS,主版本、次版本、补丁号各占几位):

  • 30000010 → OpenSSL 3.0.1(sshd 编译时对接的版本)
  • 30500050 → OpenSSL 3.5.5(系统里现在实际的版本)

翻译过来就是:sshd 这个程序,是当年照着 OpenSSL 3.0.1 编译的;可它现在运行时,系统里的 OpenSSL 却变成了 3.5.5。两者对不上,sshd 一启动自检发现不对,干脆罢工。

要理解这里为什么会”对不上”,得讲清一个底层概念。

概念补课:什么是 ABI(应用二进制接口)

你可能听过 API(应用编程接口)——那是源码层面的约定:函数叫什么名、收几个参数。

而 ABI(Application Binary Interface,应用二进制接口)是更底层的、二进制层面的约定:编译好的程序和它依赖的共享库之间,在内存里怎么对接——函数的参数怎么传递、数据结构每个字段占几个字节排在什么位置、版本号怎么校验。

关键在于:这套 ABI 约定,在程序被编译的那一刻,就”焊死”进了二进制文件里。

sshd 在它被编译出来的那天,就照着 OpenSSL 3.0 的 ABI 把对接方式固化了下来——它内部认定”我要调用的那个 OpenSSL,长成 3.0 这个样子,接口在这些位置”。

而 OpenSSL,是一个被极多程序共享的动态库(.so 文件)。nginx 用它做加密,sshd 用它做密钥交换,curl、wget、python、postfix……整台机器上一大片程序,全都动态链接着同一份 OpenSSL 库。

现在你应该能猜到悲剧是怎么发生的了:

我装 nginx 1.30 时,它要求一个更高版本的 OpenSSL。dnf 在解依赖时,为了满足 nginx,把系统的 openssl / openssl-libs 一起升级了——这个所有人共享的库,被原地从 3.0.1 换成了 3.5.5。可 sshd 这个二进制,还死死记着”我对接的是 3.0 的 ABI”。它脚下的共享库已经被换了,内存布局、接口位置全变了,sshd 一启动,自检发现”我以为的 3.0 怎么变成 3.5 了”,ABI 对不上,拒绝运行。

这不是 sshd 坏了,是它脚下的地基被人(其实是被我装 nginx 的动作)悄悄换掉了。

这正是正篇反复强调的、”共享可变的全局库”这个地基的致命之处:openssl 只有一份,谁都用它;它一升级,所有链接它的程序——不管你有没有打算动它们——全被无差别牵连。 我本想只动 nginx,却因为它们共用 openssl,把八竿子打不着的 sshd 也一起带崩了。

(顺带一提:这也正是之前终章里 Nix、容器想从根上解决的问题——让每个程序拥有自己独立、不可变的依赖副本,谁也不和谁共享一份可变的全局库,自然也就没有这种连锁崩塌。)

第四幕:破案——一场”假设、验证、推翻、再假设”的推理

知道了”是 OpenSSL 被升到 3.5.5 害的”,但还不能动手修。因为同样是”OpenSSL 变成了 3.5.5”,背后可能有完全不同的原因,而原因决定了截然相反的解法。这一幕,我想完整还原我当时真实的推理过程——它不是一条直线,而是走了好几个弯、推翻了好几个假设。因为我相信,学会怎么思考,比记住最终那条命令重要得多。

假设一:是不是我以前手动编译过 OpenSSL,污染了库路径?

我最初的怀疑是:会不会某次我手动 make install 过一个 OpenSSL,把 .so 文件丢进了 /usr/local/lib,导致 sshd 加载到了那个”野生”的 3.5.5?

怎么验证这个假设? 看 sshd 实际加载的是哪个 OpenSSL 库文件:

1
2
ldd /usr/sbin/sshd | grep -i ssl
ldd /usr/sbin/sshd | grep -i crypto

ldd 这个命令,作用是列出一个可执行文件运行时会动态链接哪些共享库、以及每个库的实际路径

为什么用它?因为”ABI 不匹配”的本质是”加载了错误版本的库”,而 ldd 能直接告诉我 sshd 到底牵手了哪个文件——如果它指向 /usr/local/lib 这种非标准路径,假设一就成立。

结果:sshd 链接的就是标准系统路径下的 libcrypto.so.3,没有野生库。假设一被推翻。

假设二:是不是某个第三方源,把 OpenSSL 顶上来了?

第二个怀疑:会不会是我加的 nginx.org 源,或者别的什么第三方源,提供了一个 3.5.5 的 OpenSSL,把官方的顶替了?

怎么验证? 这就要查”系统里现在这个 openssl,到底是从哪个仓库来的”。查来源这种事,用最顺手的 dnf info 就够了:

1
2
3
4
5
6
7
8
9
10
# 看输出里的 "From repo:" 那一行——是 appstream/baseos(官方),还是某个第三方源?
dnf info openssl-libs
# (示意输出)
openssl-libs-3.5.5-1.el9 from appstream

# 如果想看它的"出品方",rpm -qi 也能佐证:Vendor 是不是 Rocky/Red Hat 官方
rpm -qi openssl-libs | grep -i vendor

# 再反查一步:系统里到底谁依赖着 openssl-libs(谁可能把它带上来的)
dnf repoquery --whatrequires openssl-libs --installed

输出显示 from appstream——它来自官方源。假设二也站不住了。而最后那条 dnf repoquery --whatrequires 反查更进一步:它列出了一长串依赖 openssl-libs 的包,nginx 和 openssh-server 赫然都在其中——这就把”它俩共用同一个 openssl、一损俱损”这层关系坐实了。但我还不死心,因为单看来源不够稳,我要再交叉验证一次”版本”。

顺带说清这里用到的两个工具的分工:dnf info 是”快速看一眼”——给人读的包简介,看版本、来源最顺手;dnf repoquery 是”深入查关系”的引擎——--requires(它依赖谁)、--whatrequires(谁依赖它)这类反查,是 info 做不到的,也正是上面那条反查能挖出”谁拽着 openssl”的原因。简单看用 info,挖依赖关系用 repoquery,各司其职。

决定性的验证:官方源里到底还有没有旧版本?

我又敲了那条一锤定音的命令:

1
2
3
4
5
dnf list --showduplicates openssl openssl-libs
# (示意输出)
openssl-libs.x86_64 1:3.5.5-1.el9 appstream
openssl-libs.x86_64 1:3.5.5-1.el9 @System
# ……翻遍输出,全是 3.5.5,再没有任何 3.0 / 3.2 的影子

dnf list --showduplicates 会列出所有启用的仓库里,这个包能拿到的每一个版本(平时 dnf list 只显示最高版,加了 --showduplicates 才会把历史版本全摊开)。

这条输出的含义是决定性的: 如果连官方源里都只剩 3.5.5、连一个旧的 3.0/3.2 都查不到了,那只能说明一件事——Rocky 9.8 这一支,官方确确实实地把 OpenSSL 的主线,从 3.0 整体推进到了 3.5。 这不是污染,不是第三方乱入,而是一次正常的、官方的发行版库升级,只不过 sshd 没跟上这次升级的节奏而已。

(说句题外话:我一开始还想当然地以为”el9 的 OpenSSL 会永远锁在 3.0 系列”,这条输出狠狠打了我的脸——发行版在小版本演进中,确实可能整体抬升某个核心库的主版本。经验主义害死人,还是得用命令去问,而不是凭印象去猜。另外,输出里 1:3.5.5 开头那个 1 是 Epoch,不是版本号的一部分,正确读法是”Epoch 1,版本 3.5.5”。)

方向至此彻底锁定:openssl 已经回不去了(官方源里压根没有旧版可降),所以正解绝不是降 openssl,而是让 sshd 去追上它。

第五幕:修复——让 openssh 对齐到新的 OpenSSL

这一步反而是最简单的。逻辑很清晰:既然官方把 OpenSSL 升到了 3.5,那官方一定会配套发布一个”按 3.5 重新编译”的 openssh(否则它自己的系统也跑不起来)。我要做的,就是把 openssh 也升上去,让它重新对接 3.5 的 ABI。

1
2
3
4
5
6
# 先确认仓库里确实有按 3.5 重建的新 openssh
dnf list --showduplicates openssh openssh-server
# (应该能看到一个比当前已装版本更新的 openssh)

# 升级 openssh,让它对齐到 openssl 3.5
dnf upgrade openssh openssh-server

升级完成后,新的 sshd 二进制就是”针对 3.5 编译”的了,ABI 对上,自检通过,故障解除。

但是——千万别急着重启 sshd!SSH 升级的保命纪律

这是整篇番外里,我最想刻进你肌肉记忆的一段。修 sshd 这种”你正踩在上面的那根树枝”的操作,有一套铁打的纪律,那次能化险为夷全靠它:

第一,操作期间,当前这条 SSH 会话绝对不能关,机器绝对不能重启。 这条还活着的会话,是由”被覆盖之前就已经启动的那个旧 sshd 进程”托着的——它一旦断开,而新 sshd 又起不来,你就彻底失联了。它是你唯一的退路。

第二,修复后,先验证、再重启。 别直接 systemctl restart sshd,先用配置测试命令检验新 sshd 能不能正常起来:

1
/usr/sbin/sshd -t

sshd -t 会做一次”试运行式”的自检——加载配置、检查密钥、验证库链接,但不真正重启服务。只有它安静地通过了(没有任何报错输出),才说明新 sshd 具备正常启动的条件。

第三,通过了,再重启,然后另开窗口验证。

1
systemctl restart sshd

重启后,保持那条旧会话不要关,另开一个全新的终端窗口,尝试 SSH 登录进来。只有当新窗口确认能登录成功,才算真正修好了,这时才可以放心关掉那条一直留着兜底的旧会话。

第四,高危操作前,确保有”带外”退路。 如果是物理服务器,确认能通过 iLO / iDRAC 等带外管理卡进控制台;如果是云主机,确认能用云厂商的 VNC / 远程控制台。万一 SSH 真的全断了,这是你最后的生命线。

一句话总结这套纪律:升级 SSH 时,永远给自己留一条还连着的退路,验证彻底通过,再断开旧路。

尾声:绕了一大圈,最该学到的是什么?

故事到这里,有惊无险。sshd 救回来了,nginx 也是 1.30 了。但当我瘫在椅子上复盘这一整晚——卸载、换源、sshd 崩溃、ldd 排查、repoquery 查来源、showduplicates 锁定方向、再升 openssh、小心翼翼地验证重启——我意识到一个让人哭笑不得的事实:

这一整场惊魂,本来完全可以不发生。

我最初的需求,不过是”修 nginx 的 CVE”。而那个被我嫌弃”太旧”的 1.20.1-28,其实早就通过 backport 把该修的 CVE 全修好了。我真正需要做的,只是一句:

1
dnf upgrade nginx

在系统源里,把 Release 号从 -14 升到 -28,五秒钟收工,稳定、安全、零风险。是我对”版本号必须新”的执念,亲手把自己拽进了”换源 → 共享库连锁升级 → ABI 崩溃 → 差点失联”的连环坑。

所以,这篇番外用一整晚的折腾,换来一条最朴素、却最容易被工程师的”洁癖”和”追新欲”忽视的铁律:

在生产环境,尤其是国产化、信创这类追求稳定的场景里,优先信任发行版的 backport,不要轻易去追上游的版本号。 “版本号旧”不等于”不安全”——它可能只是表面被冻结,而 Release 号里早已默默打满了补丁。真要追上游新版,你就必须做好承担”换源 + 共享库连锁升级”全部风险的觉悟,并提前备好那条永远不会断的退路。

正篇里,我们讲了一条贯穿三十年的主旋律:每个答案,都是上一个问题逼出来的。 而这篇番外想补上的,是这条主旋律的另一面——

有时候,最高级的工程智慧,不是急着去给出一个更新的答案,而是先想清楚:我真的需要制造下一个问题吗?

追新很爽,但稳定,才是生产环境的第一美德。这,大概就是”绝知此事要躬行”之后,那台精密机器留给我的、最深的一课。