先交代一下背景。当时我接手的内网环境有几十台同版本的Linux服务器网络策略不允许直接访问外部软件源每次装软件都成了一项体力活在一台能联网的机器上手动扒依赖包再通过U盘或scp逐台拷结果不是缺了这个包就是版本对不上。装个nginx看起来一条命令的事实际手动处理能折腾一下午。后来我在仓库机上搭了一个本地DNF仓库再用NFS把整个仓库目录共享出去从此所有内网机器装软件都回到了最原始的体验dnf install。这篇就把“本地DNF仓库NFS共享”这套方案的完整部署过程写出来包括每个命令为什么这么用、关键参数怎么选以及实际部署里我踩过的一些坑。1. 为什么偏偏选“DNF仓库NFS”这个组合1.1 部署前的软件分发困局先说痛点。内网机器装软件最麻烦的不是软件本身而是它的依赖链。拿nginx举例我们以为只需要一个rpm实际上它会依赖一堆运行库这些库又可能依赖更基础的glibc、perl模块。手动下载时你必须把整棵依赖树理清楚否则在客户端执行rpm -ivh第一句就是“Package X is needed by Y”心态很容易崩。U盘拷贝也是看似简单实则坑多。依赖顺序错了不行架构不匹配更麻烦。你花半天拷过去的包别人机器一执行就报错最后还是得逐台排查。面对几十台机器这种低效是不能接受的。所以你需要一个“软件源”先把所有软件包集中放到一台机器上生成元数据客户端通过包管理器去读取、解析依存关系从而自动完成安装。1.2 三种向客户端分发仓库的做法集中放好了软件包接下来的问题是怎么让客户端拿到。常见分发手段有三种我直接对比一下分发方式配置复杂度DNF/源适配度适合场景HTTP需要额外维护Web服务规划URL路径高标准做法大规模集群、需要对外开放FTP需要配置vsftpd被动模式还要放行大段端口中低体验一般老旧环境、个别遗留系统NFS只需nfs-utils导出目录即可极高挂载后就是本地路径10100台内网机器最省心HTTP当然是非常标准的方案很多公共镜像站就是HTTP。但问题在于内网环境里你要多维护一个Web服务仓库文件多、体积大还得关注下载完整性和并发配置成本被抬高了。FTP就更不用提被动模式在防火墙规则里简直是个噩梦你永远不知道它要开多少端口。NFS的优势在于客户端把它挂载成一个本地目录dnf根本不需要感知网络协议repo文件里的baseurl直接写成file://指向挂载点就行。这种“把远程目录当本地目录用”的思路让整个客户端侧的配置变得极其简单。局域网内读取速度也远高于HTTP基本是磁盘速率。1.3 这套组合的信息流是怎么串联的整个方案的信息流并不复杂外部官方软件源 → 仓库机reposync同步RPM createrepo_c生成元数据 → NFS导出/repo目录 → 客户端挂载到/mnt/rpmrepo → 客户端dnf读取本地repo文件完成安装。你会发现它把两个问题拆开了软件包的组织由reposync和createrepo负责软件包的分发由NFS负责。任何一层出问题都能单独定位不必为了排查一个硬件故障去翻整个Web服务日志。适用规模上一台仓库机带几十台内网机器完全没问题。客户端通常只是短暂读取软件包不会持续打满带宽。如果你的内网规模到了几百台NFS单点也可能会成为瓶颈那时候再考虑加一层HTTP镜像不迟。现阶段这个组合就是在“省事”和“够用”之间最平衡的答案。2. 动手前先做环境规划很多配置失败都源于这个环节2.1 仓库机的系统、磁盘和目录规划仓库机本身不需要多高的计算配置1核2G做纯软件仓库都绰绰有余但磁盘一定要给足。官方软件源的体积baseos和appstream加起来就是几十GB这还不算extras以及后续持续增量的updates。我的建议是按100GB起步准备如果用LVM管理就更灵活以后空间不够可以直接扩容逻辑卷。文件系统建议选择xfs。目录结构最好是按repo id进行规划这样reposync的--destdir参数可以直接落位。我习惯的标准结构是这样的/repo ├── baseos │ ├── Packages │ └── repodata ├── appstream │ ├── Packages │ └── repodata └── extras如果内网机器混用了x86_64和aarch64两种架构不要把两套包塞进同一个目录。建议在/repo下面再按架构分层比如/repo/x86_64/baseos、/repo/aarch64/baseos否则你以后一定会被“为什么包在客户端装不上”的问题逼疯。系统版本推荐直接用Rocky Linux或AlmaLinux这类RHEL兼容系dnf是默认包管理器reposync、createrepo_c这些工具的生态足够成熟。仓库机的发行版本最好和客户端保持一致的大版本避免元数据格式差异带来的小问题。2.2 防火墙、SELinux和主机名解析动手配NFS之前有三样东西必须先确认否则后面不管怎么调exports都会失败。第一是固定IP和主机名解析。仓库机必须用固定IP并建议在每台客户端的/etc/hosts里写一条记录比如192.168.10.10 repo-serverNFS对反向解析很敏感如果客户端无法反查到服务器的主机名mount操作可能特别慢甚至超时。这个我排障时遇到过不少次先加hosts往往能解决一半诡异问题。第二是防火墙。RHEL系内网机器一般开着firewalldNFS不只是2049端口还依赖111端口的rpcbind以及mountd服务。先查看当前状态systemctl status firewalld firewall-cmd --list-services第三是SELinux。执行getenforce看看输出如果为Enforcing也不用紧张NFS相关布尔值默认是开放的但你心里要有数后文我会专门写一个SELinux挡路的排查案例。这里先记住不要在没看完日志的情况下直接setenforce 0那是最差的做法。仓库机还需要能正常访问外部官方软件源这是同步的前提。执行dnf repolist能列出baseos、appstream等仓库ID就说明基础环境没问题。3. 先搭出DNF仓库本身同步、分组和元数据生成3.1 安装包同步工具仓库机上需要两个工具reposync负责从远程源拉取软件包createrepo_c负责生成本地元数据。在RHEL系8系统中命令很简单dnf install -y createrepo_c dnf-utilsreposync命令由dnf-utils提供。这里有个细节createrepo_c是C语言实现的版本比早期基于Python的createrepo快很多大仓库全量生成元数据时差距尤其明显。你不需要再去找什么额外仓库来安装默认源里就有。3.2 用reposync从官方源同步软件包同步前先确认仓库机的仓库ID执行dnf repolist查看。不同发行版的repo id可能不一样有的叫baseos有的叫rhel-9-for-x86_64-baseos-rpms以实际输出为准。同步命令这样写reposync \ --repoidbaseos \ --repoidappstream \ --repoidextras \ --download-metadata \ --downloadcomps \ --destdir/repo每个参数都值得说明一下。--repoid指定同步哪些仓库可以重复使用--download-metadata会下载远程仓库的元数据关系到后面createrepo--downloadcomps是重中之重没有它你后续就只能用dnf install安装单个包无法使用dnf groupinstall做组安装--destdir指定RPM文件的落盘根目录reposync会自动按repo id生成子目录。首次同步会比较慢几十分钟到几个小时都有可能取决于网络和仓库大小。不过reposync默认是增量同步中途断了重跑会续传已经下载好的包不会被重复拉取。如果你的场景只需要某个特定软件加它的全部依赖用reposync全量同步就太重了。更合适的做法是dnf download --resolve --alldeps --destdir/srv/myrepo nginx这个命令会下载nginx以及依赖树里的所有rpm适合做一个几十MB的小仓库。但今天聊的是标准仓库方案所以我按下不表。3.3 生成repodata元数据同步完成后仓库目录里只有一堆rpm文件客户端dnf还不能直接用必须生成repodata元数据。命令如下cd /repo/baseos createrepo_c --update --groupfile /repo/baseos/comps.xml .--update表示在已有元数据上做增量更新这一点在后面的定时任务里非常关键没有它每次都会全量重建浪费大量时间。--groupfile用于指定分组文件对应就是刚才reposync拉下来的comps.xml。如果reposync之后你发现目录下没有comps.xml可以去官方源下载对应版本的comps文件放进目录再重新执行createrepo。对appstream和extras目录也要分别执行一次createrepo_c。完成后检查一下ls /repo/baseos/repodata/出现repomd.xml就说明元数据生成成功。dnf只认repodata目录如果没有这个目录客户端repo文件写得再对也无济于事。4. 用NFS共享仓库exports参数决定成败4.1 安装并启动NFS服务端仓库机安装nfs-utils并启动服务dnf install -y nfs-utils systemctl enable --now nfs-server systemctl status nfs-server用rpcinfo确认关键服务是否在线rpcinfo -p | grep -E nfs|mountd|rpcbind应该能看到portmapper、nfs、mountd相关信息。需要说明的是NFSv4的主体传输走2049端口但mountd和rpcbind依然会参与到挂载流程中所以在防火墙规划时必须一并考虑。4.2 /etc/exports的推荐写法和参数解读NFS的核心配置是/etc/exports文件。我的推荐写法是把整个/repo目录导出给内网段/repo 192.168.10.0/24(rw,sync,no_wdelay,no_root_squash)参数含义如下表参数含义我的建议rw / ro客户端可读可写 / 只读纯安装场景ro也够留rw方便后续客户端侧自动化推送sync / async写操作是否同步落盘建议sync避免异常掉电丢数据wdelay / no_wdelay是否合并多次写请求延迟落盘仓库场景影响不大默认即可root_squash / no_root_squash客户端root是否映射为nobody内网可信且需要写权限可no_root_squash只读场景保持root_squash更安全anonuid / anongid匿名用户映射的本地ID跨机写共享时用来统一权限很多教程默认推荐root_squash这在安全上是对的。但如果你希望通过NFS让多台机器一起往仓库里写内容比如某些自动化脚本直接从挂载点推送RPM包root会被映射成nobody然后你会发现写入时一直permission denied。所以到底选no_root_squash还是root_squash取决于你愿不愿意在内网可信环境里用一部分安全冗余换操作便捷。改完exports后让配置生效并验证exportfs -r exportfs -v输出里能看到当前导出的目录和参数说明配置已经被NFS服务端加载。4.3 客户端挂载并验证客户端侧同样要装nfs-utilsdnf install -y nfs-utils showmount -e 192.168.10.10showmount能列出服务器导出的目录说明网络和rpcbind是通的。然后创建挂载点并挂载mkdir -p /mnt/rpmrepo mount -t nfs -o nfsvers4.2 192.168.10.10:/repo /mnt/rpmrepo df -hT /mnt/rpmrepodf输出能看到远程文件系统就说明挂载成功。接下来把挂载固化到/etc/fstab我推荐的写法是192.168.10.10:/repo /mnt/rpmrepo nfs4 _netdev,nfsvers4.2,soft,timeo30,retrans2 0 0这里有几个关键点。_netdev必须加告诉系统这块挂载依赖网络启动时不要一开始就强行尝试soft和timeo、retrans是应对网络抖动的参数仓库是只读场景用soft比硬挂载合适得多这个坑后面单独展开。提醒一句先在命令行手动挂载验证过再写进fstab。如果直接写fstab出了错你可能要跑到机房用管理口去抢救系统。5. 客户端DNF配置与首次安装验证5.1 写本地repo文件客户端配置的核心是在/etc/yum.repos.d/目录下新建一个local.repo文件。内容这样写[local-baseos] nameLocal BaseOS Repository (NFS) baseurlfile:///mnt/rpmrepo/baseos enabled1 gpgcheck0 [local-appstream] nameLocal AppStream Repository (NFS) baseurlfile:///mnt/rpmrepo/appstream enabled1 gpgcheck0 [local-extras] nameLocal Extras Repository (NFS) baseurlfile:///mnt/rpmrepo/extras enabled1 gpgcheck0baseurl的前缀常见的有file://和dir://两种写法dnf对本地路径都是认的我用file://更多。这里最容易犯的错是路径写错。如果不确定挂载点下有哪些子目录先用ls /mnt/rpmrepo确认再写repo文件。gpgcheck0是我对内网仓库的态度既然已经改成离线环境包都来自可信官方源的同步再花精力配置GPG公钥链路对多数团队属于过度设计。如果你有强烈的安全审计要求那就把官方公钥装好、gpgkey路径配好也不是不行只是维护成本会高一些。5.2 清理缓存并确认仓库列表配置完repo文件后依次执行dnf clean all dnf makecache dnf repolistrepolist输出里能看到local-baseos、local-appstream等仓库说明dnf已经读取到本地NFS仓库的元数据。此时如果用外网源下发过缓存最好干净清理一次防止dnf优先使用旧缓存的元数据。如果你的客户端系统源文件还没禁用且它还能访问到外部源有可能出现本地源和外部源同时竞争的情况。想让dnf铁了心只用本地源可以临时用这个写法dnf --disablerepo* --enablerepolocal-* install nginx更彻底的办法是把/etc/yum.repos.d/下系统自带的repo文件移到一个备份目录只留local.repo。离线内网机器我就推荐这个做法一了百了。5.3 分别验证单包安装和组安装验证环节我习惯分两步走。第一步单包安装dnf install -y nginx第二步验证组安装这一步能检查出你是否真的同步了comps文件dnf groupinstall Development Tools如果报找不到组基本可以断定是同步时漏了--downloadcomps或者createrepo时没加--groupfile。回到仓库机重新处理即可。这两个验证都通过说明仓库从软件包到元数据到分组信息全链路都没问题。6. 整套方案里最隐蔽的坑按现象、排查、解决整理6.1 防火墙全放行了nfs客户端却一直mount超时这是我第一次部署时遇到的怪问题。防火墙里明明把nfs服务加进去了客户端showmount -e也能正常列出导出目录但真正执行mount命令时卡了十几秒然后直接timeout。排查链路是这样的先看服务端监听端口执行ss -lntp | grep -E 2049|111|20048确认rpcbind和mountd端口都活着。然后检查防火墙firewall-cmd --list-services发现我只加了nfs服务。问题就出在这firewalld里的nfs服务通常只覆盖2049端口但NFS挂载流程里rpcbind的111端口和mountd的端口也必须放行。正确的放行姿势是firewall-cmd --permanent --add-servicenfs firewall-cmd --permanent --add-servicerpc-bind firewall-cmd --permanent --add-port20048/tcp firewall-cmd --reload如果你发现服务器的mountd端口不是固定的20048先看rpcinfo -p的输出再对号放行。固定端口这一步可以在NFS的配置里把mountd端口设死减少后续不可控因素。6.2 挂载成功但dnf报repomd.xml无法获取这个坑比上一个更隐蔽。客户端mount成功后ls能看到RPM包但执行dnf makecache时报错大概意思是无法获取repodata/repomd.xml。第一反应是仓库机权限有问题但仓库机本地检查目录权限一切正常。排查时要分几个方向。先看客户端挂载是否正常mount | grep rpmrepo。再看SELinuxgetenforce显示Enforcing那就去翻审计日志ausearch -m avc -ts recent | tail -20如果日志里有denied记录且关键词和远程目录读取相关基本就是SELinux拦截。处理顺序一定要对先确认具体的avc报告再执行setsebool -P nfs_export_all on或通过semanage调整目录的文件上下文。不要一来就setenforce 0那是拿生产安全给操作便利买单。顺带说一句我把这个问题放在“坑”里是因为很多教程默认SELinux不会拦NFS但实际上不同版本、不同目录位置策略表现会有差异。你只需要掌握看日志定位的方法就不会慌。6.3 硬挂载遇到网络抖动yum命令卡到怀疑人生NFS客户端默认的挂载模式是hard。什么叫hard就是NFS请求发出后服务端没回应客户端会无限重试进程进入不可中断的休眠状态。我遇到过仓库机临时重启维护客户端那边没察觉到有人执行dnf install命令就卡在那里按CtrlC都没反应ps看进程状态是D。对软件源共享来说这完全没有必要。仓库是只读数据就算软挂载超时中断最多报个错不会产生脏数据。在fstab里合理使用soft模式192.168.10.10:/repo /mnt/rpmrepo nfs4 _netdev,nfsvers4.2,soft,timeo30,retrans2 0 0timeo30表示超时300毫秒重试retrans2表示重试2次这样NFS一点问题最多熬几十秒就会报错而不是无限期卡死。已经卡死了怎么救先尝试umount -l /mnt/rpmrepo做懒卸载。如果进程仍然处于D状态最省事的办法是重启客户端不要在生产上硬踢进程反而可能把文件系统搞出问题。这里也补充一句如果挂载的是生产数据盘我不建议用soft模式因为写入过程中断可能造成数据不一致只有软件源这种纯读场景soft才合适。6.4 卸载提示target is busy以及fstab顺序的坑想卸载挂载点时经常收到target is busy的提示。第一次碰到以为是哪个进程在安装软件后来排查才发现十有八九是自己shell的当前目录还停在挂载点里。执行lsof D /mnt/rpmrepo或fuser -mv /mnt/rpmrepo能看出谁占用了。如果只是自己终端cd回根目录后再umount就干净了有进程占用就fuser -km /mnt/rpmrepo确认没问题后再卸载。fstab还有一个经典坑如果你没写_netdev系统启动时会在网络服务还没就绪的阶段就去挂载NFS然后整个开机流程卡在“a start job is running for ...”等超时。几十台机器如果fstab都是复制粘贴的你就要一台一台救。这也是我为什么反复强调把_netdev写进标准化配置里。6.5 同步仓库时客户端看到了半成品仓库不是一成不变的你会定期reposync更新。但如果同步直接写到/repo共享目录里RPM包和repodata正处在变化过程中恰好有客户端执行dnf操作它可能读到不完整的元数据报一些莫名其妙的校验错误。更稳的做法是先在一个隐藏目录里同步完成后rsync到正式目录。比如reposync --repoidbaseos --destdir/repo/.sync rsync -a --delete /repo/.sync/baseos/ /repo/baseos/这样NFS导出的还是/repo/baseos但内容在极短的时间内原地切换客户端基本感知不到更新过程。定时任务里我建议都按这个思路来仓库机维护时就能做到对客户端无感。7. 定时更新与仓库瘦身维护期该做的事7.1 一个可以直接改用的同步脚本仓库不是搭完就一劳永逸官方源持续在更新安全补丁、软件新版本都要跟进。我习惯写一个脚本配合crontab在半夜执行。下面是能直接改用的版本#!/bin/bash set -euo pipefail LOG/var/log/repo-sync.log REPO_BASE/repo REPOS(baseos appstream extras) exec ${LOG} 21 echo sync start $(date %F %T) for repo in ${REPOS[]}; do echo ---- sync ${repo} ---- reposync \ --repoid${repo} \ --download-metadata \ --downloadcomps \ --destdir${REPO_BASE} if [ -f ${REPO_BASE}/${repo}/comps.xml ]; then createrepo_c --update --groupfile ${REPO_BASE}/${repo}/comps.xml ${REPO_BASE}/${repo} else createrepo_c --update ${REPO_BASE}/${repo} fi done echo sync end $(date %F %T) 注意脚本里的set -euo pipefail任何一个环节失败都让脚本退出并写进日志你才能及时发现问题。crontab添加定时任务0 2 * * 0 /usr/local/bin/sync_repo.sh选择周日凌晨一是离线客户端负载低二是避免和业务高峰撞车。同步完成后检查日志重点看每个repo有没有报错repomd.xml的时间戳是不是最近一次执行生成。7.2 仓库瘦身怎么清理旧版本还不影响安装reposync会精确同步上游仓库但上游仓库里的旧版本RPM如果一直留着磁盘空间会越来越紧张。清理思路不是随手删而是利用repomanage识别旧包。安装一次dnf-utils后repomanage --old --keep2 /repo/baseos/Packages | head -50先看输出确认列出的都是旧版本再执行删除。删除之后一定要重新createrepo_c --update否则客户端看到的元数据和实际文件就对不上了。有一点务必注意内核包不要删得太狠。至少要保留当前运行内核和上一个可用内核免得哪天新内核出问题回滚都找不到包。repomanage的--keep参数就是一种保护不过你真要严谨最好单独写个排除列表把kernel开头的包排除在外。7.3 维护期的几个实用习惯维护这套系统时间长了我总结出几个可以让你少走弯路的习惯。第一repo文件的注释和命名要规范。在local.repo里把“这个源指向的NFS服务器IP、挂载路径、同步周期”都写清楚。当时折腾的人多了我自己也记不住哪台机器用的是哪个版本后来全靠注释撑过排查。第二做任何大版本升级前经验值是在一台客户端上先验证再考虑批量推送repo文件。NFS版本协商、exports参数兼容性这些都可能在新旧客户端上表现不一致。第三仓库机本身要纳入监控。NFS共享是单点仓库机挂了所有客户端安装都会中断。脚本跑完发日志、设置好磁盘空间告警比什么都重要。不要等到客户端反馈装不了软件才发现仓库机磁盘满了。最后聊点维护体会。这套方案我自己用了很久最大的感受是把“软件源”和“分发网络”两个问题拆开之后排障逻辑特别清晰源出问题就查reposync和createrepo分发出问题就查exports和防火墙。另外客户端/etc/yum.repos.d/local.repo里我强烈建议写上源说明注明仓库机IP、挂载路径和同步周期。三年后你或接手的同事再看到这个文件会感谢你留下的这行注释。