☰
RHEL 9 本地源与国内源配置实战:双源共存、优先级与排错
2026/10/27 20:58:13 网站建设 项目流程

1. 先把“源”这件事拆开看:RHEL 9 的包管理到底在干什么

上个月帮一个做内网交付的朋友收尾,机房里三十多台 RHEL 9 的机器,全部不允许出外网,装个编译工具链都能卡半天。这种场景绕不开的一件事,就是把本地源和国内源同时配起来:本地源负责确定性和审计,国内源负责覆盖面和更新包。这篇文章就把我在 RHEL 9 上搭本地源和国内源的两套流程完整拆一遍,包括 repo 文件里每一行为什么这么写、双源共存时优先级怎么排、以及那些报错信息背后真正的原因。

先把结论摆前面:本地源和国内源解决的是两个完全不同的问题。本地源解决的是“我手上必须有一套确定的、可复现的软件包集合”,国内源解决的是“ISO 里没有的包我总得有地方拿”。很多资料把它们当成二选一来讲,实际上生产环境里这两个是互补关系,只有把优先级和屏蔽规则安排好,才不会出现“本地源装一半、远端源又装个更高版本”的混乱。

适合谁看:手上有一台能连内网的 RHEL 9 或它的社区重建版,需要给一批机器做统一软件基线的人;也适合刚接手离线环境、对着dnf install报错不知道怎么下手的新手。下面所有路径和参数都是按常见实践给出的,主机名、IP、镜像站域名你按自己环境替换就行,结构不用改。

1.1 仓库、repo 文件与 dnf 的解析链路

RHEL 9 的包管理是 dnf 这一套,底层还是 rpm。rpm 只管单个包能不能装、依赖满不满足,它不知道去哪里找包;dnf 负责拿元数据、算依赖、下载、调用 rpm 落地。所谓“源”,本质是一份 repodata 元数据加上一堆 rpm 文件,dnf 通过 repo 文件里的baseurl找到 repodata 的位置,把repomd.xml及其索引文件拉到本地缓存,之后所有依赖计算都在本地做。

一个仓库目录长这样,以 RHEL 9 的 DVD 为例:

/mnt/iso/ ├── BaseOS/ │ ├── Packages/ # 实际 rpm 文件 │ └── repodata/ # 元数据,dnf 真正读的东西 ├── AppStream/ │ ├── Packages/ │ └── repodata/ ├── media.repo # 介质自带的 repo 模板 └── RPM-GPG-KEY-... # 签名公钥

BaseOS里放的是系统启动和运行必须的最小集合,AppStream里放的是应用、语言运行时和各种版本化的模块流。RHEL 9 把这两个拆开是有意为之:BaseOS 的包生命周期跟着整个大版本走,AppStream 的包可以按 module stream 给多个版本,两者更新节奏不一样,混在一个仓库里会互相拖累。搞明白这个拆分,后面配 repo 时就不会疑惑“为什么我要写两段而不是一段”。

repo 文件放在/etc/yum.repos.d/,后缀必须是.repo,dnf 启动时会把这个目录下所有.repo文件全读一遍,再叠加/etc/dnf/dnf.conf里的全局配置。所以排查问题的第一步永远是dnf repolist -v,看清楚 dnf 到底认了几段仓库、每段的 baseurl 解析成了什么。

1.2 为什么我坚持“本地源 + 国内源”双轨而不是二选一

只用国内源的问题是:内网机器根本出不去,而且就算能出去,远端仓库的包版本是滚动的,今天装的 gcc 和下周装的 gcc 可能不是同一个 build,出问题时你没法复现。做交付和做审计的团队对这一点特别敏感,同一批机器必须能证明“我装的这一套包来自同一个被验证过的介质”。

只用本地源的问题是覆盖度不够。DVD 里的包大概几百到一千多个,实际业务里要装的容器运行时、监控组件、开发库,很多不在里面。而且安全更新是持续发布的,ISO 发版那天之后的所有补丁,本地介质里一律没有。你要么每季度重新做一次介质,要么就得有个远端源兜底。

所以我这边的标准做法是三层:第一层本地 ISO 源,priority=1,负责基础系统;第二层内网自建的同步源(用createrepo_c做的),priority=10,负责业务定制包;第三层国内镜像源,priority=99,负责兜底和获取更新。三层的数字越小优先级越高,dnf 在同一批候选包里会优先从数字小的仓库选,只有在高优先级仓库找不到这个包名时,才落到下一层。

这个分层的关键在于“包名级别”的优先,而不是“版本级别”。也就是说,如果本地源里有nginx,那么即使远端源有更新的nginx,dnf 默认还是装本地那个。这正是我们要的确定性。反过来,如果本地源里压根没有nginx,dnf 会老老实实去远端找,不会因为优先级高就报“找不到”。理解了这条规则,后面的所有配置才有意义。

2. 动手前的盘点:RHEL 9 的源有哪几条路可走

在敲命令之前,先要把自己手上有什么、能拿到什么盘清楚。RHEL 9 这个体系下,“源从哪来”其实有三条路,每条路的授权边界和技术代价都不一样,选错了后面会一直别扭。

第一条是官方订阅源。装了订阅的机器,subscription-manager会自动在/etc/yum.repos.d/redhat.repo里生成一堆仓库定义,baseurl指向官方的 CDN 地址,带证书认证。这条路最省事,但要求机器能出网、订阅状态正常,内网隔离环境下基本走不通。而且这个redhat.repo是自动生成的,你手动改它,下次刷新订阅就被覆盖,这是很多人踩过的坑。

第二条是用安装介质当本地源。这是离线环境唯一可靠的路。RHEL 9 的 DVD ISO 里 BaseOS 和 AppStream 都是完整可用的仓库,挂上去就能装。缺点是版本固定在介质发版那天,后续更新包拿不到。

第三条是换成社区重建版的仓库地址。RHEL 的下游 rebuild 发行版在包结构上和 RHEL 9 高度一致,它们的仓库地址可以直接被 RHEL 9 的 dnf 消费。这条路在法律和授权上要自己评估清楚,商业发行版的订阅权益和技术支持边界是你自己负责的,我不在这里替任何人做决定。技术上要注意的是混用之后,redhat-release这类包不要被覆盖,否则系统版本识别会出问题。

2.1 三条路线的取舍对比

路线覆盖度更新及时性内网可用主要约束
官方订阅源完整跟随官方发布节奏否需要订阅有效且有出网通道
ISO 本地源仅介质内包固定在发版日是更新包拿不到,需定期换介质
社区重建版仓库完整跟随上游重建节奏部分可用授权与兼容性需自行评估

实际项目里,我基本是“ISO 本地源打底 + 重建版仓库兜底”这个组合。本地源保证了系统能起来、能装编译工具、能满足审计要求;兜底源负责那些 ISO 里没有的业务依赖。两层都用 file 协议或 https 协议,dnf 不需要任何额外插件就能处理。

2.2 环境与前置条件清单

开工前把这几项确认掉,能省掉后面一大半的排查时间。系统版本用cat /etc/redhat-release确认,内核架构用uname -m确认(现在基本都是x86_64,ARM 机器上是aarch64,两者仓库路径不同,混了就是 404)。磁盘空间看一眼df -h /var,dnf 的缓存目录/var/cache/dnf在大批量装包时会涨到几个 GB,空间不够会在解包阶段报No space left on device,这个错误和网络无关,但很多人第一反应是去查网络。

网络方面,能出网的话先测一下镜像站的连通性和证书链,curl -I https://<镜像站域名>看返回码。时间同步一定要做,timedatectl看System clock synchronized是不是yes,https 源对时间偏差很敏感,偏差超过几分钟就会报证书校验失败,而报错信息里写的是SSL certificate problem,很容易被误判成镜像站挂了。

cat /etc/redhat-release uname -m df -h /var timedatectl | grep -i synchronized dnf --version

dnf --version输出里能看到版本号,RHEL 9 上是 dnf4 这一代,config-manager子命令需要装dnf-plugins-core才有。这个包如果本地源里没有,你就得先想别的办法把它弄进去,是个典型的“先有鸡还是先有蛋”问题,后面会讲怎么破。

3. 本地源实操:从 ISO 到可用的 BaseOS/AppStream

本地源这块,我见过太多人图省事直接把 ISO 里的 rpm 全拷到一个目录里,然后createrepo_c重新生成元数据。这个做法能跑,但没必要,而且容易把 GPG 签名校验搞乱。ISO 里现成的 repodata 是官方签过名的,直接挂载复用最干净。

3.1 挂载 ISO 并确认仓库目录结构

先把 ISO 传到机器上,建议放在一个不会被随手清理的目录,比如/opt/iso/:

mkdir -p /opt/iso /mnt/iso # 假设 ISO 已经放在 /opt/iso/ 下 mount -o loop,ro /opt/iso/rhel-9.4-x86_64-dvd.iso /mnt/iso ls /mnt/iso

loop,ro这两个参数要写全。loop是把文件当成块设备挂,ro是只读。只读这个不是可选项,ISO 是只读文件系统,写成可写挂载在某些内核版本上会给你一个警告然后照样只读,不如一开始就写明白。

挂上之后ls应该能看到BaseOS、AppStream、media.repo这几项。如果只看到Packages而没有repodata,说明你拿到的是精简版或者被别人重新打包过的介质,这种介质不能直接当源用,得自己createrepo_c。确认一下 repodata 里的关键文件在不在:

ls /mnt/iso/BaseOS/repodata/ | head # 应该能看到 repomd.xml 以及一堆 hash 开头的 xml.gz / sqlite 文件

repomd.xml是整个仓库的入口,dnf 第一步只拉这个文件,里面记录了各个索引文件的位置和校验值。这个文件缺失或者损坏,dnf makecache会直接报Failed to download metadata for repo。

3.2 写 repo 文件:每一行参数为什么这么填

RHEL 9 的 DVD 里其实自带了一个media.repo模板,可以直接拷出来改,省得手写。我个人习惯还是自己写一份,名字清楚、注释清楚,交接给别人的时候不用猜。

vi /etc/yum.repos.d/local-iso.repo
[local-baseos] name=Local ISO - BaseOS baseurl=file:///mnt/iso/BaseOS/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release metadata_expire=-1 cost=500 [local-appstream] name=Local ISO - AppStream baseurl=file:///mnt/iso/AppStream/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release metadata_expire=-1 cost=500

逐行说一下为什么。

baseurl用的是file:///三个斜杠,这个写法坑过很多人。两个斜杠的file://mnt/iso会被解析成主机名mnt加路径/iso,dnf 会去尝试网络访问然后超时。三个斜杠才是“本机绝对路径”的标准写法。另外结尾那个/建议带上,某些版本的 dnf 在拼接相对路径时对结尾斜杠比较敏感。

gpgcheck=1是必须的。离线环境不等于可信环境,ISO 在传输过程中损坏、被人替换,都靠 GPG 校验兜住。gpgkey指向的路径是 RHEL 系发行版预置的公钥位置,ls /etc/pki/rpm-gpg/看一眼就知道你机器上到底有哪几个 key,名字对不上就改成实际的。

metadata_expire=-1表示元数据永不过期。这一条专门针对静态介质,因为 ISO 内容不会变,让 dnf 每次去比对时间戳纯属浪费。如果不开这一项,dnf 在缓存过期后会尝试重新拉 repodata,虽然 file 协议本地读取很快,但那种“每次操作都多几百毫秒”的体感很烦。

cost=500是成本参数,数字越小成本越低。它的作用场景是:当多个仓库都有同一个包、且包版本相同时,dnf 会优先从 cost 低的那个下载。注意 cost 和 priority 是两回事,priority 决定“选哪个包”,cost 决定“同一个包从哪下”。很多人把它们搞混,结果配了个 priority 却发现版本还是被覆盖。

3.3 开机自动挂载与 SELinux 上下文

手动mount的挂载点重启就没了,机器一重启 dnf 就全线报错。这一步必须做持久化,两种做法。

第一种写/etc/fstab:

echo '/opt/iso/rhel-9.4-x86_64-dvd.iso /mnt/iso iso9660 loop,ro 0 0' >> /etc/fstab mount -a

写完之后一定要mount -a验证一次。fstab 写错最怕的是重启后进 rescue,所以改完立刻验证是习惯。mount -a没报错、df -h /mnt/iso能看到挂载点,才算过。

第二种用 systemd 的 mount unit,适合对启动顺序有要求的场景。文件名要按挂载路径编码,/mnt/iso对应mnt-iso.mount:

# /etc/systemd/system/mnt-iso.mount [Unit] Description=Mount RHEL9 DVD ISO [Mount] What=/opt/iso/rhel-9.4-x86_64-dvd.iso Where=/mnt/iso Type=iso9660 Options=loop,ro [Install] WantedBy=multi-user.target
systemctl daemon-reload systemctl enable --now mnt-iso.mount systemctl status mnt-iso.mount

SELinux 这块,file://方式读本地文件基本不会触发拒绝,因为 dnf 是以unconfined_r跑的,读iso9660_t上下文没问题。真正出问题的是后面要把这个目录通过 HTTP 发布出去的时候,上下文不对会直接 403,日志在/var/log/audit/audit.log里能看到avc: denied。这个坑留到 3.4 节细说。

3.4 局域网共享:用 HTTP 发布本地源

只有一台机器用,挂载就够了。要给一个机房几十台机器用,每台都传一份 ISO 不现实,这时候把本地源通过 HTTP 发布出去更省事,也更好管理——ISO 换版本只需要动服务端一处。

dnf install -y httpd systemctl enable --now httpd mkdir -p /var/www/html/repo mount --bind /mnt/iso /var/www/html/repo semanage fcontext -a -t httpd_sys_content_t "/var/www/html/repo(/.*)?" restorecon -Rv /var/www/html/repo firewall-cmd --permanent --add-service=http firewall-cmd --reload

这里有两个必须讲的细节。

第一个是mount --bind的 SELinux 上下文。bind mount 默认会继承源目录的上下文,而/mnt/iso是iso9660_t,httpd 根本读不了。semanage fcontext加restorecon是在策略里登记规则并刷新标签,但 bind mount 的标签是在挂载时确定的,restorecon对 bind mount 出来的节点可能不生效。更稳的写法是挂载时直接指定上下文:

mount --bind -o context="system_u:object_r:httpd_sys_content_t:s0" /mnt/iso /var/www/html/repo

或者干脆别用 bind mount,用rsync把 ISO 内容同步到磁盘目录里,机器硬盘够的话这是最省心的方案,SELinux 和权限都是普通文件路径那一套,没有特殊性。

第二个是持久化。bind mount 也不进 fstab 就会丢,要写的话用:

echo '/mnt/iso /var/www/html/repo none bind,context="system_u:object_r:httpd_sys_content_t:s0" 0 0' >> /etc/fstab

服务端配好之后,客户端只需要把baseurl从file:///mnt/iso/BaseOS/换成http://<服务端IP>/repo/BaseOS/,其余不变。局域网内 HTTP 拉包的速度通常在几百 MB/s 量级,比走公网镜像快得多,几十台机器同时装包也不会互相拖慢。

注意:httpd默认的DocumentRoot是/var/www/html,如果你把仓库放到别的路径,记得改/etc/httpd/conf.d/下的配置或者加个Alias,否则访问是 404 而不是 403,排查方向完全不一样。

4. 国内源实操:把远端仓库配稳、配快

本地源解决的是“有没有”,国内源解决的是“快不快、全不全”。这一节的配置思路和本地源不太一样:本地源追求确定性,远端源追求可用性和容错。

4.1 baseurl 写法与 $releasever/$basearch 的坑

一份标准的远端仓库配置长这样:

[remote-baseos] name=Remote - BaseOS baseurl=https://mirrors.example.com/repo/rhel9/BaseOS/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release metadata_expire=3600 skip_if_unavailable=1 [remote-appstream] name=Remote - AppStream baseurl=https://mirrors.example.com/repo/rhel9/AppStream/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release metadata_expire=3600 skip_if_unavailable=1

$basearch由 dnf 自动替换成x86_64或aarch64,这个变量基本不会出问题。$releasever才是真正的雷区。RHEL 9 上$releasever的值来自redhat-release包的版本,多数情况下是9,但有些环境里会被解析成9.4这种带小版本号的形式。如果镜像站的目录结构是按大版本组织的(rhel9/而不是rhel9.4/),$releasever一展开就 404。

判断方法很简单,dnf repolist -v会把每个仓库的 baseurl 展开后的完整地址打出来,直接看就行。如果发现展开结果不对,两种修法:一是把 baseurl 里的$releasever写死成9,二是加一行releasever=9覆盖:

[remote-baseos] ... releasever=9

我更倾向写死,少一层变量替换就少一个排查点。代价是将来升级大版本的时候得记得手动改,这个写进自己的运维清单里就行。

metadata_expire=3600给远端源用,一小时过期一次。远端仓库内容是滚动的,缓存太久会拿不到新包;缓存太短又会让每次dnf操作都去拉一遍元数据,几十兆的 repodata 拉一次好几秒。一小时是个比较平衡的值,批量部署前手动dnf makecache一次,后面一小时内的操作都是本地读缓存。

skip_if_unavailable=1是容错开关。远端镜像站偶尔会有单个仓库同步延迟,某些子目录暂时不存在,这种情况下 dnf 默认会整个操作失败;加上这一项,坏掉的仓库会被跳过并给个警告,其他仓库照常工作。这个参数在生产环境强烈建议开,尤其是把远端源当兜底的时候,兜底源本身不稳定不该拖垮整个安装流程。

4.2 GPG 校验与密钥导入

远端源的签名校验和本地源是同一套机制,区别在于你需要确认公钥文件确实来自可信渠道。用发行版预置的/etc/pki/rpm-gpg/下的 key 最稳妥,因为那是随系统安装包一起装进去的,链路可追溯。

如果用的是社区重建版的仓库,它们的签名 key 名字不一样,需要单独导入:

rpm --import /etc/pki/rpm-gpg/<对应的KEY文件>

导入之后用rpm -qa gpg-pubkey*能看到已导入的 key 列表。这里有个容易忽略的点:rpm --import导入的 key 和 repo 文件里gpgkey=指定的 key 是两套机制。rpm --import是把 key 装进 rpm 数据库,全局生效;gpgkey=是让 dnf 在这个仓库的包校验时使用指定 key。两者都配了不冲突,但如果只配了gpgkey=而指向的文件不存在,报错是Public key for xxx.rpm is not installed,看起来像签名问题,实际是路径写错了。

真遇到过签名校验失败又急着装包的情况,临时手段是gpgcheck=0或者命令行加--nogpgcheck。我的建议是只在自己完全清楚包来源的时候这么干,而且干完立刻改回来,别留在配置文件里。离线环境里“临时关一下”最后变成“一直关着”的情况太常见了。

4.3 关闭 rhsm 仓库管理(RHEL 特有)

这一节是 RHEL 独有的坑,用社区重建版的可以跳过。

如果你在 RHEL 9 上手动写了 repo 文件指向第三方地址,会发现过一段时间配置就“失灵”了,dnf repolist里冒出一堆你没见过的仓库,或者你改的 baseurl 被改回去了。原因在subscription-manager的rhsm插件:它每次执行都会根据订阅状态重新生成/etc/yum.repos.d/redhat.repo,这个文件里的仓库定义优先级很高,而且会覆盖同名仓库。

要彻底避免这个干扰,可以把 rhsm 的仓库管理关掉:

subscription-manager config --rhsm.manage_repos=0

关掉之后redhat.repo不再被自动生成,你手写的 repo 文件就稳定了。如果机器本来就没有有效订阅,这一步是必须的,否则 dnf 会因为redhat.repo里那些指向官方 CDN 的仓库全部失败而拖慢每一次操作。

需要说清楚的是,这个操作只是停止自动生成仓库定义,不改变订阅状态本身。商业发行版的授权边界和技术支持范围,你自己按合同评估,我这里只讲技术动作。

4.4 加速参数:cache、超时、并发

远端源慢,多数不是带宽问题,是参数配得保守。/etc/dnf/dnf.conf里这几项值得调:

[main] gpgcheck=1 installonly_limit=3 clean_requirements_on_remove=True max_parallel_downloads=10 timeout=30 retries=3 keepcache=1

max_parallel_downloads默认是 3,改成 10 之后批量装包的耗时能砍掉三分之一以上,尤其是在装桌面环境这种几百个包的场景里差别很明显。timeout=30和retries=3是给不稳定链路准备的,默认值偏保守,一遇到瞬时抖动就整批失败,重试两次往往就过去了。

keepcache=1这个选项有争议。开着的好处是同一个包重复装(比如给多台机器做镜像)不用重复下载,/var/cache/dnf里的 rpm 可以直接复用;坏处是磁盘会一直涨,需要定期dnf clean packages。我这边离线环境是开着的,配个 cron 每周清一次包缓存,元数据缓存保留。

# 每周日凌晨清理包缓存,保留元数据 0 3 * * 0 root /usr/bin/dnf clean packages >/dev/null 2>&1

5. 双源共存:优先级、屏蔽与模块流

配完两套源,真正的难点才来:怎么让它们按你想要的顺序工作。这一节是整个搭建过程里最容易出问题的地方,因为 dnf 的选择逻辑是分层的,不同参数作用在不同的层上。

5.1 priority 与 cost 的真实差别

先把三层逻辑讲清楚。

第一层是“仓库里有没有这个包名”。dnf 会先用各仓库的元数据建一个虚拟的包索引,然后按包名去找。这一步跟 priority 无关,找不到就是找不到。

第二层是“同一个包名在多个仓库里都有,选哪个版本”。这一层才轮到priority。priority默认值是 99,数字越小优先级越高。如果仓库 A 的 priority 是 1、仓库 B 是 99,那么即使 B 里有更新版本的同一个包,dnf 也会选 A 里的版本。

第三层是“选定了版本,从哪个仓库下载”。这一层由cost决定,cost默认 1000,数字越小越优先。它的作用是在包内容和版本完全相同时,优先从成本低的源下载,比如优先走局域网 HTTP 而不走公网。

所以正确的配法是:本地源priority=1、cost=500;内网同步源priority=10、cost=800;远端源priority=99、cost=1000。这样本地源既在版本选择上占优,又在下载路径上占优,双重保障。

[local-baseos] ... priority=1 cost=500

要提醒的是,priority这个参数在老版本 yum 上需要插件支持,在 RHEL 9 的 dnf4 上是内建支持的,直接写就行,不用装额外插件。这一点很多老资料没更新,还在教人装yum-plugin-priorities,在 RHEL 9 上是多余的。

5.2 exclude 与 module_hotfixes 的使用边界

有些包是必须锁死不动的,典型的是kernel、redhat-release、systemd这几个。内核跟第三方仓库的驱动模块有严格版本对应关系,被远端源偷偷升上去,机器重启就起不来。锁的方式是在远端源里加exclude:

[remote-baseos] ... exclude=kernel* redhat-release* systemd* glibc*

exclude支持通配符,可以写多个用空格分隔。写在哪个仓库里,就只对该仓库生效;写在[main]段里则全局生效。我倾向写在各仓库里,全局的exclude一旦漏了某个包,排查起来很麻烦。

module_hotfixes=1是另一个常被误用的参数。RHEL 9 的 AppStream 里有模块流(module stream),dnf 在处理有模块元数据的仓库时,会把非模块化的包过滤掉,只留模块里的。当你的本地源是普通仓库、远端源有模块元数据时,混合使用会出现“本地源里的包突然看不到了”或者No available modular metadata for modular package这种报错。

module_hotfixes=1的作用是让这个仓库里的包绕过模块过滤,当成普通包处理。它适合用在你自己用createrepo_c生成的定制仓库上,不适合无脑加在远端源上,因为远端源的模块依赖关系是官方设计好的,绕过去之后 dnf 的依赖解析可能给你拼出一套不兼容的组合。

模块这块的日常操作就三条,记一下够用:

dnf module list # 看有哪些模块和流 dnf module list nginx # 看某个包的可用流 dnf module reset nginx # 把模块状态重置回默认 dnf module enable nginx:1.22 # 显式指定版本流

注意:dnf module reset不会卸载已安装的包,它只是把“用户选择的流”清掉。切换流之后要配合dnf distro-sync才会真正把包降到目标流的版本,这一步在做之前一定要确认清楚影响范围。

6. 常见问题排查实录与速查表

下面这些报错,基本覆盖了我这几年在离线环境里遇到的九成情况。每条都写清楚“报错长什么样”“真正原因是什么”“怎么验证”“怎么修”,按这个顺序排查,比漫无目的地改配置快得多。

6.1 五类高频报错逐条拆解

第一类:Failed to download metadata for repo 'xxx': Cannot download repomd.xml。这是最常见的,但原因有四五种。先curl -I <baseurl>/repodata/repomd.xml看返回码:404 说明路径结构不对,要么$releasever展开错了,要么结尾斜杠多了或少了;403 说明服务端有访问控制,本地源的话八成是 SELinux 上下文问题;超时说明网络不通或者镜像站挂了。dnf repolist -v能看到展开后的完整地址,这是最快的定位手段。

第二类:Public key for xxx.rpm is not installed。这不是网络问题,是签名校验没过。分三种情况:gpgkey路径写错(文件不存在);key 文件存在但不是签这个包的 key(用错发行版的 key 了);包本身被重新打包过签名丢失。用rpm -qp --qf '%{SIGPGP:pgpsig}\n' -p <包文件>看包实际用的 key ID,再和rpm -qa gpg-pubkey*里的对比,一眼就知道对不对。

第三类:Error: Cannot find a valid baseurl for repo: xxx。这个和第一类看着像,实际是 dnf 连 baseurl 都没解析出来。常见原因是 repo 文件里写了mirrorlist=而不是baseurl=,或者两个都写了但 mirrorlist 先被解析且失败了。把mirrorlist那一行注释掉,只留baseurl试试。

第四类:No available modular metadata for modular package。这就是 5.2 节说的模块问题。临时解法是dnf clean all && dnf makecache重建缓存;如果还不行,看是不是某个仓库缺 modular metadata,用dnf module list确认哪些仓库参与模块解析,必要的时候用--disablerepo把那个源临时排除。

第五类:装到一半报No space left on device。这个不用查网络,直接df -h。/var/cache/dnf和/var/lib/rpm是两个增长点,前者清dnf clean packages,后者是 rpm 数据库,别乱动。另有种情况是/var单独分区且容量给小了,这个只能在规划阶段解决。

6.2 一套十分钟定位法

每次遇到 dnf 出问题,我固定按这五步走,一般十分钟内能定位。

第一步看仓库状态,dnf repolist all列出所有仓库的启用/禁用状态,确认你期望启用的那几个确实是enabled,很多人写了 repo 文件但里面enabled=0自己忘了改。

第二步看连接和路径,dnf repolist -v把每段的 baseurl 展开结果打出来,对着检查路径拼接对不对。这一步能解决掉大半的 404 问题。

第三步清缓存重来,dnf clean all && dnf makecache。缓存脏了会导致元数据和实际仓库对不上,表现是“明明仓库里有这个包,就是搜不到”。

第四步单仓库验证,用dnf --disablerepo="*" --enablerepo="local-baseos" install <包名>只启用一个源试装。这一步的价值在于排除多源干扰,把问题范围从“一堆源”缩到“一个源”。

第五步看日志,/var/log/dnf.log和/var/log/dnf.rpm.log里有完整的执行记录和错误堆栈,比终端上刷过去的那几行信息全得多。SELinux 相关的问题去/var/log/audit/audit.log,ausearch -m avc -ts today能筛出当天的拒绝记录。

现象最可能原因验证命令修复动作
404 on repomd.xml路径或版本变量错误dnf repolist -v修正 baseurl,写死 releasever
403 Forbidden服务端权限或 SELinuxcurl -I <url>修上下文或 httpd 配置
GPG 校验失败key 不匹配或路径错rpm -qp --qf '%{SIGPGP:pgpsig}'导入正确 key
模块元数据冲突模块化仓库混用dnf module listreset 模块或加 module_hotfixes
磁盘写满缓存或分区过小df -hdnf clean packages,扩容
操作极慢仓库过多或缓存配置差dnf repolist all禁用无用源,调 parallel downloads

7. 我踩过的坑与几条实操心得

先说一个最容易被忽略的点:copr 那类第三方仓库千万别在生产环境随手加。dnf config-manager --add-repo用起来太方便了,一条命令就加上,但这些源里的包很多是个人打包的,签名策略、依赖声明、构建环境都没保证。我在一个项目里吃过亏,为了装一个新版本的某个工具加了第三方源,结果它顺带把系统的openssl-libs升了个小版本,另一台依赖特定行为的老应用当场起不来。后来我定了个规矩:第三方源一律只在测试机用,生产环境要么自己用createrepo_c重打包成内网源,要么就别装。

第二个是dnf clean all之后一定要makecache。这两个命令很多人当成一回事,其实clean all只删缓存,不重建。删完直接装包,dnf 会重新拉元数据,但如果那次拉取因为网络抖动失败了一半,缓存里就是残缺状态,后续操作会报各种奇怪错误。分开执行能让你清楚看到“重建成功”这个信号。

第三个是关于ISO 版本的定期更新。本地源最大的问题不是技术难度,是容易“配完就不管了”。我见过跑了两年没换过介质的机器,系统里装的还是发版那天的包,安全补丁一个没有。建议把介质更新写进季度任务,换 ISO 的时候流程很简单:新 ISO 传到/opt/iso/,改/etc/fstab里的文件名,umount旧挂载点,mount -a,dnf clean all && dnf makecache。五分钟的事,但需要有人记得做。

第四个是内网 HTTP 源的容量规划。一开始只想着发布 BaseOS 和 AppStream,后来业务部门要装一堆开发包,又加了 EPEL 类仓库,目录越堆越大。建议一开始就把/var/www/html/repo/放在独立的大容量分区上,或者挂个 NFS。RHEL 9 的 DVD 解压后大概十几个 GB,加上后续同步的仓库,预留 100GB 比较从容。用 bind mount 的话注意源目录和挂载点要在同一分区,跨分区的 bind mount 在扩容时会有额外的搬运工作。

最后分享一个小技巧,用于快速确认“某个包到底来自哪个源”:

dnf repoquery --location nginx dnf repoquery --qf '%{name}-%{version}-%{release} from %{repoid}\n' nginx

第二条命令的输出直接带上 repoid,一眼就能看出 dnf 最终从哪个仓库选的这个包。排查“为什么装的是老版本”这类问题时,比翻配置文件快得多。如果发现 repoid 不是你期望的那个,回到第 5 节检查 priority 配置,基本都能对上。

这套东西配下来,一个小型离线机房从裸机到能批量装包,我实际操作的耗时大概在两小时以内,其中一小时是等 ISO 传输和第一次makecache。剩下的时间基本花在确认网络路径和 SELinux 上下文上,这也是我认为新手最容易卡住的地方——技术本身不难,难的是报错信息和真实原因之间隔了一层。把上面那张速查表对着用,能省掉很多来回试的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询