Linux 包管理进化史:每个答案,都是上一个问题逼出来的(下篇)17认证网

正规官方授权
更专业・更权威

Linux 包管理进化史:每个答案,都是上一个问题逼出来的(下篇)

下篇 · 把模型用起来:看穿真实的坑,与一张命令地图

接前两篇:

Linux 包管理进化史:每个答案,都是上一个问题逼出来的(上篇)
Linux 包管理进化史:每个答案,都是上一个问题逼出来的(中篇)

前两篇像一部历史剧,我们看着这套机器在一次次”问题逼出答案”中长成今天的模样。但读懂历史的真正价值,不在于谈资,而在于——当你撞上一个诡异的包管理故障时,能用脑子里的模型瞬间推演出它的根因,而不是慌乱地上网搜命令。

这一篇就来兑现这件事。我们先用前面建立的模型,去解剖几个真实世界里高频的坑,每一个都配上可以照着敲的命令;再把 rpm 和 dnf 的常用命令,按它们在模型里的”身份”重新组织成一张地图。你会发现,当你真正理解了机器,命令就不再需要死记——你只要问自己”我现在的问题属于哪一层”,答案自然浮现。

说明:下面实战里的命令都可以直接敲,但输出为节省篇幅做了精简、是示意性的,你在自己机器上跑到的具体包名和版本号会有出入——重要的是看懂”该问什么、怎么读结果”,而不是记住某一行输出。

九、用模型看穿真实世界的坑

下面每一个坑,结构都一样:先看现象,再上手诊断,然后回响前面某一章的模型点破根因,最后给解法。 你会一次次体验到那种”啊,原来文章里讲的那个机制,就是我上次踩坑的元凶”的快感。

9.1 坑一:dnf 替你做了你没要的”降级”

现象

你只想装个小工具 mytool,dnf 却在事务清单里,列出要把一个本来好好的、已装的包降级,或者给你选了一个并非最新的版本:

$ dnf install mytool
...
Downgrading:
 libcommon   x86_64   1.2-3   some-thirdparty-repo   →(你明明没动它)
Installing:
 mytool      x86_64   2.0-1   some-thirdparty-repo

你心里一紧:我没让你动 libcommon 啊,怎么还给我降级了?

上手诊断

别急,先问清楚这些候选到底来自哪个仓库——很多时候问题就出在”源”上:

# 看 mytool 和它牵连的包,各自来自哪个仓库
dnf repoquery --qf '%{name}-%{version} 来自 %{reponame}' mytool

# 反过来看,是谁要求了那个被降级的版本
dnf repoquery --requires mytool --resolve

如果你发现 mytool 来自某个第三方源,而它要求的 libcommon 版本恰好和官方源里的不一致,真相就浮出来了。

根因(回响「引擎革命」的求解器模型)

还记得吗——dnf install 不是”按你说的做”,而是在所有启用的仓库的约束下,解一道数学题(SAT),找出一个它认为”全局自洽”的方案。当某个第三方源提供的 mytool 死死要求一个较低版本的 libcommon,求解器为了让”所有约束同时成立”,就只能把 libcommon 降级——这在它看来是唯一的合法解,不是 bug,是它忠实履职的结果,只是不合你意

解法

既然根子在”多个源的约束打架”,对策就是收窄它的选择空间:用 --repo 限定这次只从可信的源里选;或者给仓库设 priority(数字越小越优先),让官方源压过第三方源;真不需要那个第三方源,就直接 dnf config-manager --disable 关掉它。一句话:当 dnf 的决定让你意外,先别怪它,去看看你给了它哪些自相矛盾的源。

9.2 坑二:改过的配置,升级后”失效”了

现象

你给某个服务改过配置(调了端口、加了参数),跑得好好的。一次例行升级之后,你发现新版的功能没生效,或者诡异的是——你改的那些参数像是”没保存”一样,服务行为又变回了默认。

上手诊断

第一反应不该是改回配置,而是先扫一眼系统里有没有”升级时被搁置的新配置”:

# 揪出所有升级遗留的待处理配置
find /etc -name '*.rpmnew' -o -name '*.rpmsave'

很可能你会看到这样的东西:

/etc/myservice/myservice.conf.rpmnew

根因(回响「账本时代」的文件归属模型)

这个 .rpmnew 就是答案。还记得吗——你改过的配置文件被 %config(noreplace) 保护着,升级时 RPM 不敢覆盖你的心血,于是保留了你的旧文件,把新版自带的那份改名成 .rpmnew 搁在旁边。所以系统此刻用的还是你那份旧配置:如果新功能需要新配置项,它自然不生效。反过来,若你看到的是 .rpmsave,则是另一种场景下你的旧文件被备份了。这一切的根子,都是那本账本在恪尽职守——它清楚每个配置归谁、谁动过,于是在”尊重你的修改”和”交付新版”之间小心翼翼。

解法

把新旧两份对比,手动合并:

diff /etc/myservice/myservice.conf /etc/myservice/myservice.conf.rpmnew
# 把新版引入的新配置项,合并进你正在用的那份,然后删掉 .rpmnew

养成升级后扫一眼 .rpmnew/.rpmsave 的习惯,能躲掉无数”明明升级了却没生效”的玄学故障。

9.3 坑三:关键服务被”偷偷”升级了大版本

现象

一次再普通不过的 dnf update 之后,某个关键服务起不来了——一查,它被升级到了一个不兼容的大版本。你从没主动要求过这次大版本跳跃,它却自己发生了。

上手诊断

先回看这次到底动了什么——升级是一笔可追溯的事务:

dnf history                    # 找到刚才那笔 update 的事务 ID
dnf history info <ID>          # 看清这一笔到底升了哪些包、从什么版本到什么版本

你大概会看到 nginx 从一个保守版本,被升到了某个第三方源提供的、高得多的版本。

根因(回响「大求解时代」的版本比较 + 仓库模型)

你启用了一个提供更高版本的第三方源(典型如 nginx 官网的 mainline 主线源),而 **dnf 的天职就是”找最高版本”**。于是每次 dnf update,它都忠实地把你往那条更高的线上拽——你以为只是例行打补丁,它却顺手把大版本也换了。

解法。 既然 dnf 永远追最高版,那就明确告诉它”这个包别动”——用 versionlock 把它焊死(注意它是个插件,需先安装):

dnf install python3-dnf-plugin-versionlock      # 先装插件
dnf versionlock add nginx                        # 给 nginx 上锁
# 之后任何 dnf update 都会绕开 nginx,直到你 dnf versionlock delete nginx 解锁

万一已经被升坏了,别慌——升级是事务,可以整笔退回:

dnf history undo <ID>          # 撤销那笔闯祸的升级

(前提是旧版本的包在源里还找得到。这也是为什么严肃生产环境值得留一份带历史版本的本地源。)

9.4 坑四:两个软件,抢同一个库的不同版本

现象

你要装 app-C,dnf 报依赖冲突死活装不上:app-C 要某个库的 2.x,可系统里 app-B 正占着这个库的 1.x,两边僵住。

上手诊断——破案三连

这种”版本打架”,核心是揪出到底是谁、把版本写死成了什么。三条命令依次问下去:

# 1. 先看 app-C 到底要求哪个版本
dnf repoquery --requires app-C
#    假设输出含:libfoo >= 2.0

# 2. 再揪出系统里谁正死死占着旧版本
dnf repoquery --installed --whatrequires libfoo
#    假设输出:app-B、some-old-tool

# 3. 最关键:确认这个库本身,到底支不支持多版本共存
dnf repoquery --provides libfoo
#    若同时提供 libfoo.so.1 和 libfoo.so.2 → 库本身支持共存

根因(回响「共存纪元」的 soname 模型)

还记得吗——如果一个库做了规范的 soname 版本化,libfoo.so.1 与 libfoo.so.2 各占各的路径,本可以和平共存。

所以第 3 步是分水岭:若库明明提供了两个 soname,冲突就不是库的错,而是某个包(比如 app-B)把依赖写死成了”我就要这一个具体版本号”,亲手堵死了共存的可能。

这正是”依赖地狱”在共享库层面的现代残留。

解法

看破案结果下药:如果是某个包依赖写得太死,换一个版本要求更宽松的同类包,或换打包更规范的源;如果死占旧库的是个你其实用不上的老古董(some-old-tool),评估能否直接卸掉,枷锁就解了。但如果两个都是关键业务、又真的共存不了——那就是依赖地狱的死结,这时该请出终章那一跳:别在一个系统里硬解,用容器给它们各自一个世界。

9.5 坑五:隐藏的最高位Epoch,版本号里看不见的那张王牌

前面排查升级问题时,我们一直默认”版本号大的就是新的”。但有一个隐藏字段,能凌驾于版本号之上、悄悄改写这个判断——它就是 Epoch。不认识它,你迟早会撞上一桩”版本号明明一样,dnf 却非要升级”的怪事。

先说它为什么存在。正常情况下比较版本天经地义:1.20 比 1.18 新。可总有意外——比如某个软件上游改了版本号的命名方式,新版本号在字符串比较里反而”变小”了(典型如从日期号 20231001 改成语义号 1.0,而 1.0 按位比较居然小于 20231001)。这时只看版本号,包管理器会犯傻:它认为”新版更旧”,于是死活不肯升级,甚至想给你降级。Epoch 就是为这种尴尬准备的一张王牌。

它的规则极其简单粗暴:比较两个包谁新谁旧时,先比 Epoch,Epoch 大的直接获胜,后面的版本号根本不看。 完整的比较顺序是 E-V-R 三段依次比——先 Epoch(没写默认为 0),Epoch 相同才比 Version(上游版本号),Version 还相同才比 Release(发行版打包次数,就是 .el9 前那个 -28)。Epoch 处在最高位,一票否决。所以打包者只要把新包的 Epoch 抬高一级,就等于强行宣布:”别管版本号字面大小了,我说了算,这个就是更新的。”

这套机制,正是你可能踩过的那个坑的真相。还记得在 Rocky 9 上 dnf info nginx 看到的对照吗:

已装:  Epoch: 1   Version: 1.20.1   Release: 14.el9
可用:  Epoch: 2   Version: 1.20.1   Release: 28.el9_8.2.rocky.0.1

注意——两个的 Version 一模一样,都是 1.20.1。如果只看版本号,它俩”一样新”,dnf 根本无从判断。可关键就在那个隐藏字段:可用版的 Epoch 是 2,已装的是 12 > 1,于是 dnf 一锤定音判定”可用版更新”,要把你从 Epoch 1 拉到 Epoch 2——哪怕主版本号一个字都没变。Rocky 的打包者用这一手,就是在强制声明:”我这个回移植了安全补丁的 -28 包,优先级高于任何 Epoch 1 的 nginx,请务必升上来。”

最后是它最坑人的地方:Epoch 平时是隐身的。 你 nginx-1.20.1-28.el9 这样写、这样看,完全看不到它的踪影。只有几个地方能让它现形——dnf info 里那一行 Epoch:、带冒号的完整写法 1:1.20.1-14.el9(冒号前那个数字就是 Epoch)、或者你主动去查:

bash

rpm -q --qf '%{epoch}:%{version}-%{release}\n' nginx

所以,当你遇到”两个版本号看起来一样、dnf 却坚持说有新版”这类怪事时,第一个该去看的就是 Epoch。它是版本号里那张看不见、却最大的牌——平时归零隐身,一旦打包者需要在版本号本身会误导的情况下”钦定谁更新”,它就被翻出来,一锤定音。

9.6 所以,别再无脑 dnf update 了

走完这五个场景,你或许已经回过味来:它们的肇事者,很多时候是同一个习惯——对着系统无脑敲 dnf update,然后祈祷一切安好。

很多教程会告诉你”定期 dnf update 保持系统最新就好”。这话对个人桌面无妨,但放到生产环境,它悄悄撤掉了你和系统之间最重要的一道防线。

因为一次 dnf update,可能同时引爆前面所有的坑:

它会顺着第三方源把关键服务偷偷升上不兼容的大版本(坑三);

会让一批配置悄悄变成 .rpmnew 而你浑然不觉(坑二);

会在求解时做出你没预料的降级或替换(坑一、坑四)。

而最致命的是”批量”——它一次动几十上百个包,真出了事,你几乎无法第一时间定位到底是哪一个干的。

那该怎么办?其实前面这台精密的机器,早就把工具都给你备好了:

  • 升级前,先 dnf check-update 看清这次要动哪些包,心里有数,而不是闭眼回车;
  • 关键生产组件,用 dnf versionlock 焊死,把”绝不能动的”挡在 update 之外;
  • 把每次升级当成一笔事务:出事就 dnf history undo 整笔退回(前提是本地留着旧版本);
  • 国产化、离线、内网场景尤其如此——用一个版本可控的本地源,有节制地升级,而不是对着公网一把梭。

说到底:无脑 dnf update 的危险,不在命令本身,而在于它撤掉了”我清楚我在改什么”这道防线。

我前面两篇讲的那台机器——事务、铁律、账本、求解器——自始至终都在保护你;但它保护的前提是,你得知道自己在让它做什么。

这,才是这五个实战真正想交给你的东西:不是五条命令,而是一种”动手前先想清楚”的态度。

而当你需要具体某条命令时,下面这张地图,随时备查。

十、一张按”模型”组织的命令地图

动手前先说明一个版本前提:下面的命令以 RHEL 8/9 的 DNF4 为基准(国产化环境如 openEuler、Kylin 多数也基于此)。RHEL 10、Fedora 41+ 改用了 DNF5,绝大多数命令照旧能用,但少数命令的子命令或旗标形态有变化——关键处我会在该条下注明。

如果你在 RHEL 10 上敲某条命令报”未知参数”,多半就是 DNF4 与 DNF5 的差异,留意对应注释即可。

最后,是这份命令清单。但请注意,它刻意不按”安装/卸载/查询”那种烂大街的分法——那种清单网上有一万份。

我们按前面建立的心智模型来组织:每一条命令,都告诉你它”住在哪一层、回答什么问题”。这样你查命令时,顺带又复习了一遍模型;而懂了模型的人,看分类就知道该用哪一类命令。

第一类 · 查”财产清单”(rpm 读本地数据库 /var/lib/rpm)

这一类全是 rpm -q(query)开头,问的都是”这台机器已经装了什么”。它们只读本地账本,不联网、不碰仓库。

  • rpm -qa:列出已安装的全部包
  • rpm -qi <包>:看某个包的身份卡(版本、来源、安装时间……)
  • rpm -ql <包>:这个包往系统铺了哪些文件
  • rpm -qf <文件路径>:反查这个文件归哪个包所有(铁律的体现)
  • rpm -qc <包> / rpm -qd <包>:只看它的配置文件 / 文档文件
  • rpm -q --requires <包> / --provides <包>:它需要谁 / 它提供什么
  • rpm -V <包>:校验文件是否被改动过(完整性核查,合规利器)

第二类 · 查”商品目录”(dnf 读远端仓库元数据)

这一类问的是”世界上存在什么、能装什么”,dnf 会去读仓库的 primary.xml

  • dnf search <关键词>:按关键词搜包
  • dnf info <包>:看仓库里某个包的详情
  • dnf list --installed / dnf list --available:列已装的 / 可装的
  • dnf provides <文件或命令>:”哪个包能提供这个文件/命令?”(装某个缺失命令时极有用)
  • dnf repolist:看当前启用了哪些仓库

第三类 · 解依赖、动系统(dnf 求解器 + 事务)

这一类会真正改变系统状态,每一次都是一笔可追溯的”事务”。

  • dnf install <包> / dnf remove <包>:安装 / 卸载(自动解依赖)
  • dnf upgrade [<包>]:升级全部或指定包
  • dnf downgrade <包>:降级
  • dnf check-update:只看有哪些可升级,不动手(升级前先心里有数)
  • dnf install --setopt=install_weak_deps=False <包>:只装硬依赖,不要弱依赖(最精简安装)
  • dnf history:查看事务流水账
  • dnf history undo <ID>:撤销某一笔事务
  • dnf history rollback <ID>:撤销此 ID 之后的所有事务(注意:不是”回到这一笔”,而是”抹掉这一笔以后的”)

第四类 · 管模块与流(共存纪元的遗产)

注意:RHEL 10 起模块化已被弃用,以下命令主要用于 RHEL 8/9 及仍在用模块的系统。

  • dnf module list:列出所有模块及其流、状态
  • dnf module enable/disable <模块>:启用 / 禁用一个模块
  • dnf module install <模块:流/profile>:安装某条流的某个 profile
  • dnf module switch-to <模块:流>:切换到另一条流(注意:switch-to 需较新版本 dnf;老系统上切流需先 dnf module reset 再 enable)
  • dnf module reset <模块>:重置模块到初始状态

第五类 · 诊断与破案(把模型变成探照灯)

这一类不改变系统,专门用来”看清真相”,是上一节那些坑的解药。

  • dnf repoquery --whatrequires <包>:谁依赖了它(判断能不能安全删;加 --recursive 连间接依赖一起查,加 --installed 只看已装的)
  • dnf repoquery --requires <包> --resolve:它依赖谁(--resolve 显示实际对应的包)
  • dnf repoquery --whatprovides <能力>:谁提供了某个能力
  • dnf repoquery --provides <包>:它提供了哪些能力 / soname(判断库能否多版本共存)
  • dnf versionlock add/delete <包>:给包上锁 / 解锁(需先装 python3-dnf-plugin-versionlock)
  • find /etc -name '*.rpmnew' -o -name '*.rpmsave':升级后,揪出待合并的配置

第六类 · 仓库与离线源(把”商品目录”搬到本地)

  • dnf config-manager --add-repo <url>:添加一个新仓库(添加后默认启用)启用 / 禁用某个已有仓库:RHEL 8/9(DNF4)用 dnf config-manager --set-enabled <repo> / --set-disabled <repo>;RHEL 10(DNF5)语法已变,改用 dnf config-manager setopt <repo>.enabled=1(=1 启用,=0 禁用)
  • createrepo_c <目录>:把一个装满 rpm 的目录建成仓库(离线/内网源的基石)
  • createrepo_c --update <目录>:增量刷新仓库元数据
  • dnf clean all / dnf makecache:清空 / 重建本地元数据缓存

为什么 RHEL 10 要改成 setopt?

这不是随手换了个写法。老的 --set-enabled 干的事,是跑去你的 .repo 文件里把 enabled=0 原地改成 1——直接改写你的原始配置。隐患你现在应该一眼能看穿:原件被升级覆盖时改动可能丢。DNF5 的 setopt 换了思路:不碰原文件,而是在专门的覆盖目录里叠加一层设置,读取时”原件 + 覆盖”叠起来生效。于是原始配置不可变、你的调整可追溯、想撤销很干净。 你品品——这跟 rpmdb 从 BDB 换 SQLite、模块化退回普通 RPM、乃至 Nix”不在原地改”是同一种韵律:软件工程在一次次教训里,越来越不肯”在原地改全局状态”,而是宁可”加一层、不碰原件”。 连改个仓库开关这种小事,都逃不过这条进化的引力。

最后请记住这张地图的用法:遇到问题,先别想”用哪个命令”,先想”我的问题属于哪一层”。

这,就是这部进化史最终想交到你手里的东西:不是一堆待背诵的命令,而是一副能看穿机器的眼睛,和一张随时可查的地图。

十一、终极复盘:敲下 dnf install 之后,到底发生了什么?

读到这里,你已经分别认识了这套机器的每一个零件:记录”装了什么”的本地账本、缓存”世界上有什么”的仓库元数据、解依赖数学题的求解器、保证安全的事务、守护文件归属的铁律。现在,是时候把它们串成一条完整的流水线了。

我们用全文的老主角做例子——dnf install nginx。当你按下回车,屏幕上不过是滚动几行进度,然后提示成功。但在这短短几秒里,七个角色按一套精密的时序协同工作了一遍。下面这张时序图,把整个过程拆成了 16 步,你可以对照着看;我把它讲成一个连贯的故事。

第一幕:dnf 先搞清楚”世界上有什么”(步骤 1~5)。

你敲下 dnf install nginx(步骤 1),dnf 这个前端调度器接过指挥棒。它做的第一件事不是上网,而是先读 /etc/yum.repos.d/ 下的 .repo 配置(步骤 2)——搞清楚”我手上有哪些仓库可用”。然后它去看本地缓存 /var/cache/dnf 里的仓库元数据还新不新(步骤 3):如果过期了,就向远端仓库发起 HTTP 请求,把 repomd.xml 和 primary.xml(图里画的是经典的 .gz 压缩,较新的源也可能是 .zst/.zck)这套”商品目录”拉下来(步骤 4),写进本地缓存(步骤 5)。到这里,dnf 手里就有了那本”商品目录”——世界上存在哪些包、它们彼此什么依赖关系,全在里面。

第二幕:dnf 再搞清楚”这台机器现状如何”(步骤 6)。

光知道”世界上有什么”还不够。dnf 转身查询本地数据库 /var/lib/rpm(步骤 6),把”这台机器已经装了哪些包”这本财产清单读进来。现在,它两本账都齐了——这正是前面反复强调的:dnf 比 rpm 聪明,就聪明在它一手攥着远端的商品目录,一手攥着本地的财产清单。

第三幕:把难题交给求解器(步骤 7~9)。

接下来是整台机器最烧脑的一步。dnf 把”已装清单 + 仓库依赖图”两份数据,一起喂给专业的 SAT 求解器 libsolv(步骤 7)。libsolv 在内部解这道我们讲过的、属于 NP 完全的依赖数学题(步骤 8)——在所有约束下,算出一个”全局自洽”的最小变更集:到底要装哪些、升哪些、会不会和已装的东西冲突。算完,它把这份变更清单交回给 dnf(步骤 9)。

第四幕:征求你的同意,然后下载(步骤 10~12)。

dnf 拿到变更集,不会擅自动手。它把”将要安装/升级哪些包”列出来给你看,停下来等你敲 y 确认(步骤 10)——这是你最后的刹车机会。你一旦同意,dnf 才向远端仓库下载那些选中的 .rpm 包(步骤 11),存进本地缓存 /var/cache/dnf(步骤 12)。注意:到此刻为止,你的系统还没有被改动分毫,一切都还在”准备”阶段。

第五幕:真正动手,而且全程受铁律与事务保护(步骤 13~16)。

万事俱备,dnf 把最终的执行权,交给最底层的 rpm(步骤 13)。rpm 按依赖顺序,把每个包解开、运行其中的 pre/post 安装脚本(步骤 14)——这一步如果发现要铺的文件已经名花有主,铁律就会开火、整个事务回滚(这正是那场冲突惨案的发生点)。一切顺利的话,rpm 把新装包的元数据、文件清单、以及那个让 rpm -qf 能反查归属的索引,统统写进本地数据库 /var/lib/rpm(步骤 15)——账本被更新,这台机器从此”记得”自己多了一个 nginx。 最后,成功的回执一路返回到你的终端(步骤 16),屏幕上打印出 Complete!

回头看这一条流水线,你会发现它就是前两篇所有概念的一次集体亮相:

.repo 配置决定了”能去哪进货”;仓库元数据是”商品目录”;本地 /var/lib/rpm 是”财产清单”;libsolv 是解依赖难题的”大脑”;那句 y/N 确认背后是”事务”的审慎;rpm 落盘那一刻,既在执行铁律的检查,也在更新账本。所谓”敲一条 dnf install”,其实是这七个角色,沿着一条三十年演化出来的精密流水线,合力跑完的一场接力。 你之所以能云淡风轻地敲下回车、然后只看几行进度滚过,正是因为这套机器,已经把所有的复杂、所有的”上一个问题”,都默默替你消化在了这十六步里。

转自:张先生的深夜课堂

版权申明:内容来源网络,版权归原创者所有,如有侵权请联系删除

想了解更多干货,可通过下方扫码关注

可扫码添加上智启元官方客服微信👇

未经允许不得转载:17认证网 » Linux 包管理进化史:每个答案,都是上一个问题逼出来的(下篇)
分享到:0

评论已关闭。

400-663-6632
咨询老师
咨询老师
咨询老师