装完一台 Rocky Linux 9 的机器第一次敲dnf install却卡在 “Cannot download repomd.xml” 或者转上几十秒才超时这个体验想必不少人都遇到过。Rocky Linux 9 作为与 RHEL 9 二进制兼容的下游发行版默认仓库指向的是官方 mirrorlist 服务在境内网络环境下经常会出现握手慢、元数据拉取失败、GPG key 下载不下来等一系列问题。同时很多人手上还有 RHEL 9 的机器想配个在线源结果发现照着 Rocky 的教程抄一通dnf repolist出来一片红。这篇文章就是把这两件事放在一起讲透Rocky Linux 怎么把在线镜像源换得干净利落RHEL 9 的仓库体系为什么和 Rocky 不是一回事以及在不能直接照抄的情况下有哪些稳妥的替代路线。内容面向刚接触 RHEL 系发行版的运维新手也面向需要给一批机器做统一源配置的老手命令、参数、报错、坑点都会给到位你可以直接拿去改改就用。1. 先把关系理清楚Rocky Linux 9 和 RHEL 9 到底谁是谁1.1 一次“装完机器没源可用”的真实场景我之前接手过一批测试机系统是同事用 Rocky Linux 9 的 DVD ISO 装的装的时候没勾选网络装完之后/etc/yum.repos.d/底下躺着一堆 repo 文件看着挺完整但dnf install vim-enhanced直接报错。原因很简单ISO 安装默认生成的是指向官方 mirrorlist 的配置而当时那台机器只配了内网 DNS压根解析不到外网域名。后来我干脆把源换成了内网自建的仓库一条命令下去秒装这个问题才彻底解决。这个场景其实很典型。很多人以为“换源”就是改个网址实际上它牵扯到三层东西配置文件里的地址写在哪、这个地址背后的服务能不能连上、连上之后包里带的签名能不能验过。任何一层没打通表现都是“装不上”但排查方向完全不同。所以真正开始改文件之前先把 Rocky Linux 9 和 RHEL 9 的关系搞明白会让你后面少走很多弯路。Rocky Linux 是从 RHEL 的公开源码重新构建出来的发行版目标是做到与对应版本的 RHEL 在二进制层面兼容也就是同一个 RPM 包理论上在两者上都能装、都能跑。这个特性带来的直接好处是为 RHEL 9 编译的第三方软件在 Rocky Linux 9 上基本不用改就能用。但兼容的是包本身不是仓库服务。这两者之间的差别就是很多人踩坑的起点。1.2 二进制兼容 ≠ 仓库可以互换一个很常见的误解是既然 Rocky Linux 9 和 RHEL 9 这么像那我直接把 RHEL 9 的机器指向 Rocky 的源不就行了从技术上说短期内确实可能有部分包能装上但这属于典型的“能跑起来不代表没问题”。真正的隐患有三处。第一处是签名体系不同。两个发行版各自有自己的 GPG 密钥Rocky Linux 9 的密钥放在/etc/pki/rpm-gpg/目录下文件名里带 Rocky 字样RHEL 9 的包签名用的是另一套。你把 RHEL 9 的gpgcheck开着指向 Rocky 的源校验必然失败把gpgcheck关掉等于把所有包的完整性校验都扔了这在任何有安全要求的环境里都不能接受。第二处是包版本策略不同。虽然源码同源但两个发行版的更新节奏、补丁回溯策略、包的重构建时间点都不完全一致。混合使用的时候很容易出现依赖解析出一个“半新半旧”的组合系统看起来正常跑到某个特定功能时才炸而且极难定位。第三处是元数据格式和仓库划分的细微差异。Rocky Linux 9 的仓库拆分方式BaseOS、AppStream、CRB、extras 等和 RHEL 9 大体对应但具体到某个包落在哪个仓库里偶尔会有出入。依赖链一旦跨仓库断裂dnf给出的报错往往指向一个看起来毫无关系的包名新手很容易被带偏。所以我的基本立场很明确Rocky Linux 9 就用 Rocky 的源RHEL 9 就走 RHEL 自己的分发渠道两边不要互相串。下面所有关于“换源”的操作都是在各自体系内部做优化而不是跨体系搬运。1.3 认准版本号和仓库代号别在第一步就错Rocky Linux 9 的仓库配置文件里大量用到了两个变量$releasever和$basearch。前者是发行版主版本号后者是 CPU 架构x86_64、aarch64 等。这里有个细节值得单独拎出来说在 Rocky Linux 9 上$releasever通常解析成9而不是 9.4 这种带小版本号的形式。为什么这么设计因为小版本升级比如 9.4 升到 9.5不应该导致仓库地址失效。如果地址里写死了小版本号每次系统更新之后你还得手动改一遍源非常容易忘。用主版本号的好处是仓库地址在小版本之间保持稳定你换个镜像站也只需要改域名部分。想确认自己的$releasever到底是多少可以这样查# 查看 dnf 解析出来的变量值 dnf -q --eval %{releasever} # 或者间接看列出仓库时注意 repo id 后面跟随的版本 dnf repolist -v | grep -i Repo-baseurl | head -5另外要注意仓库的代号。Rocky Linux 9 的核心仓库是BaseOS和AppStream这两个前者放的是系统运行必需的基础包内核、glibc、systemd 这类后者放的是应用层软件和模块流各种语言运行时、Web 服务、数据库客户端之类。再往上是CRBCodeReady Builder很多开发库和头文件在这里、extras、以及highavailability、resilientstorage这类特殊用途仓库。新手最容易犯的错是只配了 BaseOS 就以为万事大吉结果装个开发包死活找不到。所以第一步不是急着改地址而是先把当前系统上到底有哪些 repo 文件、每个文件里有哪些 section 摸清楚# 列出所有 repo 文件 ls -l /etc/yum.repos.d/ # 列出所有 section 名字方括号里的内容 grep -h ^\[ /etc/yum.repos.d/*.repo # 看某个仓库当前是否启用、指向哪里 dnf repolist --all跑完这三条你对自己的仓库版图就有底了。dnf repolist --all会输出所有仓库enabled 那一列是enabled还是disabled一目了然。这一步花两分钟后面能省两小时。2. 换源的整体思路为什么我不建议上来就改 baseurl2.1 官方源、公共镜像站、内网源三条路怎么选“换源”这个词听起来简单实际上有三种完全不同性质的选择各自适用的场景差别很大。官方源指的是 Rocky Linux 官方维护的镜像分发体系。它的优势是元数据权威、更新最及时、仓库划分最标准劣势是对境内网络不够友好元数据文件和包体下载经常慢到让人怀疑人生。默认安装出来的配置基本都是走这条路也就是为什么很多人第一次装包就卡住。公共镜像站是各类组织提供的 Rocky Linux 全量镜像路径结构和官方保持一致只是服务节点离你更近。它的优势是速度快、覆盖全劣势是同步可能有延迟通常是几小时级别而且不同镜像站的完整度、稳定性参差不齐你得挑一个靠谱的长期用。选择的时候建议优先看是否提供完整的 BaseOS、AppStream、CRB、extras 四件套只镜像了 BaseOS 的站点基本没法用。内网源是自己搭的仓库服务从公共镜像站或者官方同步下来用 HTTP 服务在内网分发。它的优势是完全可控、速度快到飞起、不受外网波动影响适合机器数量多或者网络环境受限的场景劣势是需要额外维护一台机器和定期同步任务。我个人的经验是机器超过十台就值得搭内网源省下的时间和排查成本远超搭建成本。这三条路的选择逻辑可以用一张表概括方案适合场景前期成本长期维护成本速度表现官方源临时测试、能直连外网极低低波动大公共镜像站单机或少量机器低低较好内网自建源十台以上、内网隔离环境中中最好对绝大多数个人和小团队场景公共镜像站是性价比最高的选择后面的实操也主要围绕它展开。2.2 repo 文件里那几个字段到底在干嘛在动手之前把 repo 文件的字段含义吃透比抄十份模板都管用。一份典型的 Rocky Linux 9 仓库配置长这样[baseos] nameRocky Linux $releasever - BaseOS mirrorlisthttps://mirrors.example.org/rocky/$releasever/BaseOS/$basearch/os/ # baseurlhttps://mirrors.example.org/rocky/$releasever/BaseOS/$basearch/os/ enabled1 countme1 gpgcheck1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9逐个说[baseos]是 repo id全局唯一dnf --enablerepobaseos这类命令用的就是它。改名会影响脚本引用非必要别动。name是给人看的描述随便写但建议保留版本信息方便排查时一眼认出。mirrorlist和baseurl是互斥的两种寻址方式同时存在时mirrorlist优先。这个细节很多人不知道改动的时候只改了baseurl却忘了注释mirrorlist结果配置完全没生效还以为是缓存问题。enabled1表示默认启用0表示默认不启用但可以临时用--enablerepo打开。CRB 仓库我一般设成enabled0需要时再开避免默认把大量开发包暴露出来。gpgcheck1是包签名校验开关。这个绝对不能关关掉之后你装的每一个包都可能是被篡改过的。gpgkey指向公钥文件可以是本地路径file://也可以是远程 URL。本地路径的好处是不依赖网络稳定性更好建议优先用本地。还有一个容易被忽略的字段是countme1它用于统计访问量对功能没有影响但在某些镜像站上会额外产生一个计数请求。内网环境下建议删掉少一次无效请求。提示修改 repo 文件之前一定先备份。cp -a /etc/yum.repos.d /etc/yum.repos.d.bak.$(date %F)这一条命令能救你无数次回滚的时候直接rm -rf再mv回来就行。2.3 metalink 和 baseurl 的取舍这是换源环节最值得展开讲的一个技术决策因为它直接决定了你后续维护的省心程度。mirrorlist和metalink都是“让客户端自己去问一组候选地址”的机制。区别在于mirrorlist返回的是一个纯文本列表客户端按顺序尝试metalink返回的是带元信息的 XML里面包含每个镜像的协议、优先级、地理位置、更新时间等字段客户端可以更智能地选择。而baseurl是写死一个具体地址简单直接没有协商过程。那为什么不干脆全用baseurl因为写死地址意味着你放弃了自动容灾。假设你写死了某个镜像站的地址某天这个站点维护或者限流你所有的dnf操作都会失败而且报错信息不会告诉你“换个源就行”只会告诉你“连不上”。机器一多挨个改配置的痛苦指数直线上升。反过来全用metalink也有问题境外网络环境下获取 metalink 文件本身可能就是那个最慢的环节有时候要等十几秒才超时失败。所以我实际的做法是折中对主仓库BaseOS、AppStream用固定的baseurl指向一个我验证过的、长期稳定的镜像站追求速度。对次要仓库CRB、extras、addons保留metalink反正用得少容错优先。在内网源场景下全部用baseurl指向内网地址因为内网服务的稳定性是可控的。这个思路的核心是稳定性由你掌控的部分用 baseurl不由你掌控的部分用 metalink。想清楚这一点你的源配置策略就成型了。3. 实操Rocky Linux 9 换国内公共镜像源3.1 动手前必须做的三件确认我在给任何机器换源之前固定会做三件事缺一件都可能白忙一场。第一件确认系统版本和架构。这不是多余的谨慎而是因为 Rocky Linux 8 和 9 的仓库路径结构不完全一样x86_64 和 aarch64 的路径里也带架构段。路径拼错了报错是 404看着像镜像站挂了实际是你自己写错了。cat /etc/rocky-release uname -m输出类似Rocky Linux release 9.5 (Blue Onyx)和x86_64这两个信息后面拼 URL 要用。第二件确认网络能通到目标镜像站。直接curl -I打一下你要用的镜像站根路径看返回码。这一步能提前排除掉 DNS 解析失败、出口被限、镜像站临时故障这几种情况。如果这里就不通后面配什么都是白配。curl -sI -m 8 https://mirrors.example.org/rocky/ | head -3-m 8是超时时间8 秒足够判断连通性了避免卡太久。第三件备份现有配置。前面已经提过这里再强调一次。很多人觉得“我就改一行不用备份”然后一个 sed 命令写错把整个文件覆盖了最后只能重装rocky-repos包来恢复默认配置。备份的成本是三秒钟不备份的成本是半小时。cp -a /etc/yum.repos.d /etc/yum.repos.d.bak.$(date %F_%H%M)做完这三件事才进入真正的修改环节。3.2 两条命令完成批量替换Rocky Linux 9 默认的 repo 文件里地址部分通常写成mirrorlist的形式指向官方或某个默认镜像。批量替换的思路是把mirrorlist行注释掉同时在每个 section 里插入一个指向新镜像站的baseurl。手工一个个改太慢用sed更实际。但sed用不好很容易把文件改坏所以我建议分两步走每步都能验证。第一步先统一处理 mirrorlist 和 metalink 行。sed -i.bak \ -e s|^metalink|#metalink|g \ -e s|^mirrorlist|#mirrorlist|g \ /etc/yum.repos.d/rocky*.repo这里用了-i.baksed会顺手生成一份.bak后缀的备份比事后手动恢复方便。|作为分隔符是因为 URL 里有/用/当分隔符得转义一大堆容易出错。第二步为每个 section 补上 baseurl。这一步没法用一个通用sed搞定因为不同 section 对应的路径不一样BaseOS 走 BaseOS 目录AppStream 走 AppStream 目录。我的做法是先看清楚每个 section 名字和它应该对应的路径再用带 section 匹配的脚本处理。# 先看清楚有哪些 section grep -h ^\[ /etc/yum.repos.d/rocky*.repo | sort -u假设输出里有[baseos]、[appstream]、[crb]这几个就可以针对性地插入。下面这段用awk处理逻辑是遇到目标 section 的头部就在紧跟的name行后面插入baseurlawk /^\[baseos\]/ { ins1 } /^\[appstream\]/ { ins1 } /^\[crb\]/ { ins1 } /^\[/ !/^\[(baseos|appstream|crb)\]/ { ins0 } { print if (ins /^name/ !done) { if (prev ~ /baseos/) print baseurlhttps://mirrors.example.org/rocky/$releasever/BaseOS/$basearch/os/ if (prev ~ /appstream/) print baseurlhttps://mirrors.example.org/rocky/$releasever/AppStream/$basearch/os/ if (prev ~ /crb/) print baseurlhttps://mirrors.example.org/rocky/$releasever/CRB/$basearch/os/ done1 } if (/^\[/) { prev$0; done0 } } /etc/yum.repos.d/rocky.repo /tmp/rocky.repo.new说实话这段脚本对不同小版本的 repo 文件结构不一定完全兼容。如果你的目标是长期稳定我强烈建议直接手写一份干净的 repo 文件而不是靠脚本去改原始文件。下面就是模板。3.3 手搓一份干净的 repo 文件模板这是我实际在用的做法不去改官方的rocky*.repo而是把它们全部改名为.repo.disabled或者挪到一个备份目录然后新建一份rocky-custom.repo把需要的仓库显式写清楚。好处是配置完全可控出问题一眼能看出哪一行错了而且系统更新时不会因为包升级把你的自定义配置覆盖掉。[baseos] nameRocky Linux $releasever - BaseOS baseurlhttps://mirrors.example.org/rocky/$releasever/BaseOS/$basearch/os/ enabled1 gpgcheck1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 metadata_expire3600 [appstream] nameRocky Linux $releasever - AppStream baseurlhttps://mirrors.example.org/rocky/$releasever/AppStream/$basearch/os/ enabled1 gpgcheck1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 metadata_expire3600 [crb] nameRocky Linux $releasever - CRB baseurlhttps://mirrors.example.org/rocky/$releasever/CRB/$basearch/os/ enabled0 gpgcheck1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 [extras] nameRocky Linux $releasever - extras baseurlhttps://mirrors.example.org/rocky/$releasever/extras/$basearch/os/ enabled1 gpgcheck1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9几个设计上的讲究metadata_expire3600把元数据缓存有效期设成 1 小时。默认值通常是 6 小时或者更短设成 3600 是在“新鲜度”和“网络请求次数”之间取的一个平衡点。内网源场景可以设成 86400因为内网同步本身就是定时的没必要频繁回源。CRB 设为enabled0是我一直坚持的习惯。CRB 仓库里有大量开发库和头文件默认开启会让dnf search的结果变得非常嘈杂而且某些情况下会引入非预期的包。需要编译软件的时候再dnf install --enablerepocrb xxx用完关掉干净。gpgkey用本地file://而不是远程 URL。这一点在批量部署时尤其重要一百台机器同时去拉一个远程 key 文件不仅慢还可能因为限流失败导致整个安装流程中断。本地路径的路径一定要确认存在可以先ls一下/etc/pki/rpm-gpg/目录。路径中的$releasever和$basearch不要手动替换成具体值让dnf自己去解析。手动写死 9.5 和 x86_64 看起来更“明确”实际上等于把配置锁死了小版本一升级就得改。注意镜像站的路径结构不是完全统一的。有的站点在域名后面直接跟rocky/有的会多一层rockylinux/或者其他前缀。配之前一定先用curl把拼好的 URL 打一遍确认返回 200 再写进配置别凭感觉拼。3.4 刷新缓存并逐项验证配置写完不等于生效dnf有本地缓存机制不清理的话它会继续用旧数据你会以为配置没生效然后又去改配置越改越乱。# 清掉所有缓存 dnf clean all # 重建缓存这一步会实际访问每个仓库 dnf makecache # 列出启用的仓库确认地址正确 dnf repolist -vdnf makecache是验证配置最直接的手段。它会把每个 enabled 的仓库都拉一遍元数据哪个仓库配错了这里立刻报错而且报错信息里带的 URL 能直接告诉你是哪个字段写错了。dnf repolist -v的输出比较长但值得完整看一遍。重点看每个仓库的Repo-baseurl是不是你期望的地址Repo-expire是不是你设的值Repo-pkgs是不是一个合理的数字BaseOS 通常几千个包AppStream 也差不多如果显示 0 那肯定有问题。缓存重建完之后再做一次实际安装验证这一步别省# 装一个体积小、依赖少的包验证整条链路 dnf install -y tree # 验证能查到多版本说明元数据完整 dnf --showduplicates list nginx | head -20--showduplicates这个参数在排查“为什么装不到某个版本”的时候特别好用。如果它列出来的版本比你预期的少说明你的源不完整可能是只配了 BaseOS 没配 AppStream也可能是镜像站本身同步不全。3.5 顺手把 EPEL 和 CRB 也安排上Rocky Linux 9 的官方仓库覆盖了大部分基础需求但有些常用工具比如htop、jq的某些版本、各类 Python 第三方库的 RPM 封装在 EPEL 里。EPEL 是面向企业级 Linux 的附加包仓库Rocky Linux 直接兼容。配置 EPEL 的关键是版本对应。Rocky Linux 9 对应的是 EPEL 9装错版本会出现依赖冲突症状是dnf报一堆nothing provides或者版本不匹配。# 安装 EPEL 仓库配置包名带版本号别装错 dnf install -y epel-release # 装完之后检查一下EPEL 的 repo 文件会出现在同一目录 grep -A5 ^\[epel\] /etc/yum.repos.d/epel*.repo装完epel-release之后同样建议把它的metalink改成指向你常用的镜像站理由和主仓库一样。EPEL 的镜像路径通常是在镜像站的epel/9/Everything/$basearch/下面配置方式跟主仓库基本一致。CRB 仓库前面说了默认关闭但有些场景必须开编译 PHP 扩展、装某些开发库、用dnf groupinstall Development Tools的时候。开启方式是临时加参数或者改配置文件把它设成enabled1。我的建议是临时用参数用完就关别让它常驻。# 临时启用 CRB 装开发工具组 dnf groupinstall -y --enablerepocrb Development Tools4. RHEL 9 这边在线源为什么不能照着 Rocky 抄4.1 订阅制和 CDN 的分发逻辑差异这是很多人概念上最容易混淆的地方。Rocky Linux 的仓库是公开可匿名访问的任何能连上镜像站的人都可以直接拉包。RHEL 9 不是这样它的官方仓库走的是订阅授权 CDN 分发的模式系统注册之后订阅服务会给这台机器下发一套客户端证书dnf拿着这套证书去 CDN 节点请求包。这套机制体现在文件系统上就是/etc/pki/entitlement/目录下那几个.pem文件以及/etc/yum.repos.d/redhat.repo这个由订阅服务动态生成的配置文件。这个文件的特点是它是自动生成的手工改了会在下次刷新时被覆盖。所以你在 RHEL 9 上直接编辑redhat.repo加个baseurl短期看着有效过一段时间就失效了而且失效得毫无征兆。未注册的 RHEL 9 系统dnf repolist通常是空的或者只有从安装介质生成的本地源。这时候想装包dnf会明确告诉你没有可用的仓库。这不是配置写错了而是设计如此。理解了这一点就能明白为什么“把 RHEL 9 指向 Rocky 的源”这种做法不可取不只是签名校验过不去的问题而是你绕开了整个授权和分发体系后续的补丁追溯、安全更新、支持服务全都断了。短期能装上包长期是个技术债。4.2 四条相对稳妥的路线对比那 RHEL 9 想做在线装包有哪些正经路子我梳理了四条各有适用场景路线前置条件优点缺点推荐度激活订阅走官方 CDN有可用订阅最规范、更新最全、有支持需要订阅生产环境首选用开发者订阅个人开发者身份免费、合法、功能完整有数量限制、仅限非生产学习测试首选从安装 ISO 建本地源有完整 DVD ISO完全离线可用无安全更新、需定期换 ISO隔离环境可用内网自建仓库一台可同步的机器速度最快、集中管控需要维护同步任务机器多时首选第一条是标准答案有订阅就走这条配好subscription-manager之后redhat.repo会自动生成什么都不用手写。第二条适合个人学习和测试。开发者订阅拿到的权限足够日常开发用注册流程也简单注意别用在生产环境上就行。第三条适合完全隔离的环境。用 DVD ISO 做本地源思路是把 ISO 挂载到某个目录然后写一个指向file://的 repo。缺点很明显ISO 是发布时的快照之后所有的安全更新都拿不到长期用等于把系统暴露在已知漏洞下。我的做法是只把它当作“应急装包”的手段不作为长期方案。第四条是机器数量多时的正解下面单独展开。4.3 内网自建仓库的完整流程内网自建仓库这件事听起来工程量大实际做下来一个下午能搞定而且一次投入长期受益。核心思路是找一台能访问外网的机器或者用离线同步的方式把 Rocky Linux 9 的仓库完整同步下来然后用一个 HTTP 服务在内网分发。第一步准备同步工具和存储空间。# 装同步和建索引的工具 dnf install -y dnf-plugins-core createrepo_c nginx # 看一眼需要的空间Rocky 9 全量四个仓库大概几十 GB 量级 df -h /data空间估算这块要提前做别同步到一半磁盘满了。BaseOS AppStream CRB extras 的全量数据加上历史版本体积不小。如果只是想满足日常装包可以只同步--newest-only体积能压下来一大截。第二步配置同步源并执行同步。# 建目录结构按仓库分开放 mkdir -p /data/repo/rocky/9/{BaseOS,AppStream,CRB,extras}/x86_64/os # 同步 BaseOS只同步最新版本节省空间 reposync \ --repoidbaseos \ --download-metadata \ --newest-only \ --delete \ -p /data/repo/rocky/9/BaseOS/x86_64/osreposync的几个参数值得说明--download-metadata会把仓库元数据一起拉下来这样客户端不用自己生成--newest-only只下载每个包的最新版本能省大量空间--delete会删掉源端已经不存在的包保持同步一致性这个参数配上定时任务很关键否则仓库会无限膨胀。第三步生成仓库元数据。createrepo_c --update /data/repo/rocky/9/BaseOS/x86_64/os--update是增量更新比全量重建快得多。如果是首次建库可以不加这个参数。每次reposync之后都要跑一遍否则客户端看到的是旧索引。第四步起 HTTP 服务对外提供。# nginx 配置片段 cat /etc/nginx/conf.d/repo.conf EOF server { listen 80; server_name _; root /data/repo; autoindex on; location / { try_files $uri $uri/ 404; } } EOF systemctl enable --now nginx第五步客户端指向内网地址。[baseos] nameInternal Rocky 9 - BaseOS baseurlhttp://repo.internal.example/rocky/9/BaseOS/x86_64/os/ enabled1 gpgcheck1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9注意这里的$releasever被我替换成了字面的9。原因是在内网环境下路径完全由我自己控制用字面值反而更直观也避免了某些特殊场景下变量解析出问题。这是我个人的偏好两种写法都可以。第六步把同步做成定时任务。# 每天凌晨同步一次日志留在本地方便排查 cat /etc/cron.d/repo-sync EOF 30 2 * * * root /usr/local/bin/sync-rocky-repo.sh /var/log/repo-sync.log 21 EOF同步脚本里把reposync和createrepo_c --update串起来中间加上错误判断。这里有个经验同步任务一定要加日志和告警否则某天同步失败了你还以为仓库是新的等到装包报错才发现元数据已经半个月没更新排查起来非常费劲。5. 报错现场我踩过的坑和速查表5.1 常见报错对照速查换源这件事报错信息五花八门但归归类其实就那么几种。下面这张表是我这些年攒下来的遇到问题先对号入座能省掉大量瞎试的时间。报错关键词大概率原因优先排查动作Cannot download repomd.xml地址错误或站点不可达curl -I打目标 URL 看返回码Failed to synchronize cache for repo元数据拉取失败多为网络或 DNS检查resolv.conf试nslookup域名Curl error 60: SSL certificate problem系统 CA 证书过期或缺失dnf install ca-certificates或检查时钟repodata/repomd.xml ... 404路径拼错多一层或少一层目录逐段对照镜像站的实际目录结构GPG key retrieval failedkey 地址不可达或路径写错改用本地file://路径Package X is not signed签名校验不通过key 不匹配确认gpgkey指向对应版本的 keyNo matches found for X仓库不全或包在未启用的仓库里dnf search all X检查 CRB 是否需开启nothing provides Y needed by X依赖链断裂源不完整dnf repoquery --whatprovides Y反查Metadata file does not match checksum缓存损坏或镜像站同步中dnf clean all后重试这张表用起来的关键是不要跳步。比如看到Cannot download repomd.xml很多人的第一反应是“镜像站挂了换一个”但实际上可能是自己 URL 里少写了一层目录。先用curl打一遍返回 200 就说明是路径问题不是网络问题方向立刻明确。5.2 GPG 校验失败的三层排查GPG 相关的报错是换源过程中最容易让人放弃的一类因为报错信息通常很抽象不会告诉你是哪一步出了问题。我总结了一个三层排查法从上往下走基本能覆盖所有情况。第一层key 文件存不存在。这是最基础也最常被忽略的。配置文件里写着gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9但如果这个文件压根不存在比如系统是从别的发行版转过来的或者清理时误删了dnf会尝试去下载然后又失败报错绕来绕去。ls -l /etc/pki/rpm-gpg/ | grep -i rocky目录里应该有对应版本的 key。如果没有最稳妥的办法是从官方仓库包里提取或者从可信来源重新导入。第二层key 有没有被 dnf 正确导入。repo 文件里的gpgkey只是告诉dnf去哪找实际生效需要导入到 RPM 数据库。# 查看已导入的 key rpm -qa gpg-pubkey* # 手动导入如果 repo 配置里的自动导入失效 rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9导入之后再试一次安装很多“签名校验失败”的问题在这一步就解决了。第三层包和 key 是否属于同一个体系。如果前两层都没问题校验还是失败那基本可以确定是跨体系混用了拿 RHEL 的 key 去验 Rocky 的包或者反过来。这种情况没有优雅的解决办法唯一正确的做法是把源改回对应体系别在混用的路上继续折腾。注意临时用--nogpgcheck绕过校验来“验证问题”是可以的但绝对不能把这个参数写进生产脚本。绕过签名校验等于放弃了包完整性保护一旦仓库被投毒或者传输被劫持你装进去的东西是什么完全不可控。5.3 网络层排查的正确顺序很多“源配好了但装不上”的问题最后追下去都是网络层。排查顺序错了会在应用层反复折腾浪费时间。我固定的顺序是DNS → TCP 连通 → TLS 握手 → HTTP 响应 → 内容正确性一层层往上走。# 1. DNS 能不能解析 getent hosts mirrors.example.org # 2. TCP 端口通不通80 和 443 分别试 timeout 5 bash -c /dev/tcp/mirrors.example.org/443 echo 443 ok # 3. TLS 握手和证书 curl -sI -m 8 https://mirrors.example.org/rocky/ | head -5 # 4. 具体路径的响应码 curl -sI -m 8 https://mirrors.example.org/rocky/9/BaseOS/x86_64/os/repodata/repomd.xml # 5. 内容是不是真的元数据而不是一个错误页 curl -s -m 8 https://mirrors.example.org/rocky/9/BaseOS/x86_64/os/repodata/repomd.xml | head -3第 5 步特别值得说。有些网络环境会返回一个 HTTP 200但内容其实是个提示页或者被重定向过的页面。curl -I只看状态码看不出问题必须实际拉一下内容确认。我曾经遇到过一台机器所有curl -I全是 200但dnf一直报元数据解析失败最后发现是中间设备对响应做了替换返回了一个格式完全不同的页面。还有一个高频原因是系统时钟不准。TLS 握手对时间敏感时钟偏差超过一定范围会导致证书验证失败报错看起来跟证书本身有关很容易误判。timedatectl status # 如果发现时间偏差大开启时间同步 timedatectl set-ntp true5.4 几条不写在文档里的经验别在同一个系统上堆太多 repo 文件。我见过有人为了图方便从五六个不同来源抄了七八个 repo 文件扔进/etc/yum.repos.d/结果同一个包在多个仓库里都有dnf的依赖解析变得极其混乱。合理的做法是一个体系一个文件Rocky 的归 RockyEPEL 归 EPEL第三方软件源单独一个文件命名上带上前缀方便识别。换源之后一定要验证一次完整的事务。光dnf makecache成功不代表能装包。我习惯装一个依赖链稍微复杂点的包当作冒烟测试比如dnf install -y git它会牵出几个依赖能顺带验证依赖解析是否正常。保留一份可回滚的配置。我的习惯是在/etc/yum.repos.d/同级建一个repos.d.orig目录放一份刚装完系统时的原始配置。任何时候配置乱了把当前目录整体挪走、把原始配置复制回来三十秒恢复出厂状态。镜像站不要只用一个。主用 备用是基本配置。虽然 repo 文件里只能写一个baseurl但你可以另存一份带了备用地址的配置文件出问题的时候mv一下切换比临时找地址快得多。记录你用的镜像站路径。我维护了一个小文本文件记录每个环境的镜像站地址和最后验证日期。听起来很土但排查问题时这个文件能省掉大量“这个地址是哪来的、什么时候配的”的困惑。6. 收尾小抄版本锁定、模块流与日常维护6.1 别让换源打乱你的版本锁定换源之后有个副作用容易被忽视可用版本集合变了。原来可能只有两三个版本可选换了源之后变成十几个dnf update有时候会一口气升到很新的版本把原本稳定的环境打乱。如果你的环境对版本敏感配合versionlock插件把关键包锁住是比较稳妥的做法dnf install -y python3-dnf-plugin-versionlock # 锁定某个包到当前版本 dnf versionlock add nginx # 查看当前锁定列表 dnf versionlock list # 需要升级时先解锁升完再锁 dnf versionlock delete nginx我用这个插件的场景主要是数据库客户端和一些 ABI 敏感的运行库。Web 服务器这类反而不太需要锁跟着仓库走反而能及时拿到安全修复。这个取舍要看你自己的环境特点。6.2 模块流的坑AppStream 里有一类东西叫模块流module stream它允许同一个软件在仓库里存在多个版本系列。比如某个运行时可能有 8 和 9 两个流装了哪个流就决定你后续能装到哪个版本系列。# 看某个模块有哪些流 dnf module list nodejs # 切换到一个特定的流 dnf module reset nodejs dnf module enable nodejs:20 dnf module install nodejs这里的关键动作是reset再enable。很多人只做enable结果发现切换不生效因为旧的流状态还残留着。reset会把模块状态清回初始再enable才能干净切换。模块流和换源叠加的时候还有个坑不同镜像站的模块流内容可能不完全同步你在 A 站能切到某个流换到 B 站就找不到了。遇到这种情况先dnf module list确认流是否存在别怀疑自己的命令写错了。6.3 日常维护几条命令最后整理一组我日常会用的命令频率不高但每次都用得上# 查看哪些仓库被启用了以及包数量 dnf repolist --enabled # 找出某个文件属于哪个包排查依赖问题神器 dnf provides /usr/bin/vim # 查看某个包的所有可用版本和来源仓库 dnf --showduplicates list nginx # 只从指定仓库装包避免多源混淆 dnf install --enablerepocrb --disablerepo* -y some-devel # 查看事务历史确认某次操作装了什么 dnf history dnf history info last # 清理无用依赖慎用先加 --assumeno 看清单 dnf autoremove --assumenodnf history这条我要特别提一句。换源之后如果出现奇怪的问题第一件事应该是看历史记录确认最近一次事务到底动了哪些包。dnf history info last会把那次操作涉及的包全列出来比你在日志里翻半天高效得多。而且dnf history undo id能直接回滚一次事务这个功能在紧急情况下非常救命前提是你得知道事务 id。还有个小习惯值得养成每次大规模换源或者改 repo 配置之后先跑一遍dnf check-update看看有哪些包会被升级。这个命令只检查不执行是观察影响面的最佳手段。如果输出里出现了你不想动的大件比如内核、glibc那就先想清楚再决定要不要继续。我个人的经验是换源之后不要马上dnf update先观察一两天让缓存稳定下来确认新源的元数据没有异常再安排升级窗口。