前段时间我在整理一组内网机器每台跑的都是一套Linux系统。系统装完最让我头疼的一件事就是软件包从哪来——外网源时快时慢有一次几百MB的补丁包下载到80%直接超时前功尽弃。就是那次之后我下决心把DNF本地仓库和NFS共享服务一次性搭好让整组机器从同一个软件源拉包后续加机器、打补丁、装软件都轻松不少。这篇不聊高大上的架构就是一个能把内网软件源稳定跑起来的完整流程从环境准备、软件源搭建、NFS共享配置到客户端验证和踩坑记录全都按照我自己实际试验过的路径来写。1. 先想明白一件事DNF仓库解决什么问题NFS又为什么在这里1.1 外网源的脆弱性值得你多花一小时做本地仓库日常我们用DNF装软件时都是指向官方镜像或公共镜像网络一波动下载就中断。机器少的时候还能忍机器一多重复下载同一批包、反复浪费带宽是所有人的时间一起被浪费。再加上有些环境是隔离网压根碰不到外网软件源就必须内网自己解决。DNF仓库能做的事很简单把上游的软件包集中放到一台服务器上生成索引元数据内网其他机器通过仓库配置指向这台服务器就能像用官方源一样查包、装包、升级包。解决的不仅是速度问题更是统一版本、统一管理的问题。同一网段里十几台机器每台装的软件版本完全一致靠的就是共用同一个源。1.2 为什么选NFS而不去折腾HTTP服务DNF仓库的本质是一个“目录结构repodata元数据”发布方式并不只有HTTP一种。HTTP方案需要额外维护Nginx或Apache端口、权限、SELinux都要照顾对一个小型内网来说反而是额外负担。NFS方案直接把仓库目录共享出去客户端挂载后仓库对DNF来说就是一个本地文件夹配置时直接用file://协议指向挂载点即可整个链路里不需要再跑一个Web服务。举一个不太恰当的类比HTTP仓库是“打电话叫外卖”要等菜品送到NFS共享是“直接把厨房搬到你家楼下”饿了就去楼下拿。DNF吃的是本地文件目录流量则在底层通过网络传输模块本身完全不关心介质是什么。这个思路特别适合5到20台规模的集群或内网机房管理成本低、行为直观、出问题排查简单。1.3 整条链路的最终形态最终拓扑很简单一张表就能说清角色操作系统职责仓库机/NFS服务端RHEL系8/9衍生版存放软件包生成repodata通过NFS共享仓库目录客户端RHEL系8/9衍生版NFS挂载仓库目录repo配置指向挂载点日常DNF操作全走本地源部署顺序大致是准备服务器 → 收集或同步软件包 → 生成仓库元数据 → 配置NFS共享 → 客户端挂载 → 编写repo配置 → 验证安装升级。这条链路每一步都有坑我在后面把坑都标出来了。2. 环境准备选型、拓扑与安装地基2.1 发行版选型服务端和客户端建议都使用RHEL 8/9衍生版比如Rocky Linux、AlmaLinux 9.x原因很简单DNF本来就是这些系统的默认包管理器配置目录是/etc/yum.repos.d工具链齐全直接开干就行。RHEL 8之后DNF替代了YUM但命令习惯和配置逻辑基本一脉相承网上YUM时代的资料大部分还能对应上。2.2 网络规划与硬件空间仓库机至少准备50GB以上独立磁盘空间如果打算同步多个软件源比如BaseOS、AppStream、EPEL最好预留100GB。我个人习惯把仓库路径单独放在一块数据盘下比如/storage/dnf-repo不要放在/root或/home下面这样方便单独做磁盘监控也不怕日志或家目录把根分区挤爆导致仓库写入失败。网络按隔离机房举例仓库机192.168.10.10客户端网段192.168.10.0/24。NFSv4主要依赖2049端口配合rpcbind的111端口内网环境最直接的做法是把防火墙对这些网段放行或者干脆关闭firewalld前提是你确认这个网络环境足够受控。2.3 基础软件包安装清单服务端需要装以下三样dnf install -y createrepo_c nfs-utils dnf-utilscreaterepo_c用来生成repodata元数据这是仓库能被DNF识别的关键。nfs-utils提供NFS服务端程序和管理工具exportfs、showmount都由它提供。dnf-utils提供dnf repolist等辅助命令排查仓库配置时很有用。客户端也要装一下nfs-utils否则执行mount.nfs时会提示找不到命令。虽然内核通常也能独立完成NFS挂载但命令行工具齐全一些处理故障更方便。2.4 SELinux策略先打预防针我不建议一上来就setenforce 0除非你确认你的内网环境不需要任何SELinux保护。处理NFS场景时通常只需要设置对应的布尔值setsebool -P nfs_export_all_rw 1这句话的意思是允许NFS导出可读写的目录。如果只是分享一个只读仓库开着SELinux一般问题不大后面如果遇到SELinux拦截日志再用ausearch -m AVC结合sealert处理比无脑关掉整个系统要稳妥得多。3. DNF本地仓库搭建三种软件来源按需选择3.1 方案A用系统ISO镜像做基础源如果你所在的网络完全碰不到外网手头只有官方ISO这个方案最省事。把ISO挂载到服务器上直接作为源目录共享即可。mount -o loop /data/iso/rocky-9-x86_64.iso /mnt/isoISO内部一般自带repodata目录字段结构完整可直接使用。但这里有个常见坑部分ISO自带的元数据缺少comps组信息执行dnf groupinstall时会报No such group之类的错误。解决办法是在共享前用createrepo基于ISO目录重新生成一次并指定组文件dnf install -y createrepo_c createrepo /mnt/iso --groupfile /mnt/iso/comps.xml如果你的场景只是日常安装基础软件ISO源完全够用。缺点是包版本固定在发行版发布时升级要等下一版ISO或额外引入增量源。3.2 方案B从上游镜像站同步完整仓库能访问外网时我更推荐用dnf reposync把上游源同步到本地这样软件包版本永远是最新的客户端拉到的补丁和官方源一致。同步命令示例dnf reposync --repoidbaseos --download-metadata -p /storage/dnf-repo重点说明一下--download-metadata这个参数它会把上游的repodata一并拉下来。有了它目录初始状态就是一个可直接被DNF识别的仓库不需要额外createrepo。之后想定期增量更新在同目录下再加一次--download-metadata即可。对于AppStream源把--repoid换成appstream再执行一次。EPEL源同理。建议把几个源的同步写成一个脚本扔进crontab每天凌晨执行一次仓库机就能自动保持和上游同步。3.3 方案C用散装RPM包自建定制仓库这个方案最常见的使用场景是某些软件包不在标准发行版源里或者你需要锁特定版本给内网机器统一部署。把收集到的RPM包全部丢进一个目录比如/storage/dnf-repo/custom然后执行createrepo /storage/dnf-repo/custom执行完成后目录下会生成repodata目录一个最小的自定义仓库就成型了。这个方案真正的坑在于依赖不完整——如果没有把依赖包一并收集客户端安装时会报依赖错误。我的经验是用一台能连外网的机器先执行dnf install --downloadonly把需要的包和依赖全部下载下来再整体拷入自定义目录生成仓库这样客户端安装基本一次通过。3.4 repo文件参数逐个拆解无论使用哪种方案仓库配置文件的格式都一致位置在/etc/yum.repos.d/下后缀.repo。以本地BaseOS源为例[baseos-local] nameBaseOS Local Repository baseurlfile:///storage/dnf-repo/BaseOS enabled1 gpgcheck0常用参数的含义我整理成了表格参数作用经验值[id]仓库唯一标识在DNF输出中显示不要包含空格且全局唯一name仓库描述显示用写清楚源名称方便排障baseurl仓库实际地址支持file、http、ftp等多种协议本地源必须是file://开头enabled是否启用该仓库默认1临时关闭可设为0gpgcheck是否校验RPM包签名内网自制源建议0减少配置麻烦gpgkey公钥文件路径仅当gpgcheck1时需要priority仓库优先级需额外插件不指定时DNF按配置顺序处理一个容易被忽略的点baseurl支持多个地址中间用空格分隔。当第一个地址故障时DNF会自动切换到下一个。如果你有热备仓库机这个参数价值很大。4. NFS共享服务部署从exports语法到开机细节4.1 安装、启动、确认服务状态服务端已经装好nfs-utils了接下来启动主服务并设置开机自启systemctl enable --now nfs-server systemctl status nfs-server在较新的RHEL衍生版上服务名是nfs-server老系统上可能是nfs。用rpcinfo -p确认RPC端口列表正常出现nfs和mountd信息就算在线。4.2 exports文件配置与参数取舍NFS允许共享哪些目录写在/etc/exports里。我的典型配置/storage/dnf-repo 192.168.10.0/24(rw,sync,no_root_squash,no_subtree_check)逐项解释为什么这样填rw允许读写。仓库机本身只需客户端读取时可以收窄成ro如果希望客户端能往共享目录回写同步结果比如某台机器兼职负责拉包才需要rw。syncNFS默认推荐使用sync保证服务端写入落盘后再返回完成。仓库共享哪怕只是读取也保持sync避免极端情况下数据丢失。no_root_squash默认行为是客户端root被映射成nobody这在只读仓库里反而没必要但如果你允许客户端root写仓库就必须关闭root_squash否则客户端root照样没有权限。注意这是双刃剑只在可信内网开启。no_subtree_check关闭子树检查。这个参数解决的是NFS在目录被重命名或移动后出现Stale file handle的问题减少无谓的校验开销。4.3 导出目录并做基础验证修改完exports后让配置立即生效exportfs -rav showmount -e 127.0.0.1exportfs -rav会重新读取配置并导出所有目录showmount输出里能看到刚刚共享的路径和允许网段。先在服务端本地试挂一次mount -t nfs4 127.0.0.1:/storage/dnf-repo /mnt/testrepo ls /mnt/testrepo df -h /mnt/testrepo umount /mnt/testrepo本地能挂载能读取说明NFS服务端姿势正确可以转战客户端。4.4 客户端挂载与fstab持久化客户端手动挂载dnf install -y nfs-utils mkdir -p /mnt/dnf-repo mount -t nfs4 192.168.10.10:/storage/dnf-repo /mnt/dnf-repo挂载成功后写入/etc/fstab实现重启自动挂载192.168.10.10:/storage/dnf-repo /mnt/dnf-repo nfs4 defaults,_netdev,noatime,soft,intr 0 0_netdev必须写它告诉系统这个文件系统依赖网络开机在网络就绪后再挂载否则会出现启动时报“挂载失败”的尴尬。soft,intr配合使用能避免NFS服务端故障时客户端进程长时间卡住noatime可以减少文件访问时间戳的写入对仓库这种只读共享场景有性能收益。5. 客户端配置与全链路验证让DNF真正吃上共享仓库5.1 挂载路径与权限确认客户端挂载完成后第一时间检查两件事第一ls /mnt/dnf-repo能否看到各子目录第二stat -c %a %U %G /mnt/dnf-repo/repodata能否正常返回权限信息。这里的repodata目录权限决定了DNF能否读取元数据如果返回的权限异常说明服务端权限设置有问题先去服务端调整再继续。5.2 编写客户端repo文件客户端上新建/etc/yum.repos.d/local.repo指向NFS挂载点[baseos-local] nameBaseOS via NFS baseurlfile:///mnt/dnf-repo/BaseOS enabled1 gpgcheck0 [appstream-local] nameAppStream via NFS baseurlfile:///mnt/dnf-repo/AppStream enabled1 gpgcheck0有人会问为什么不是http://而是file://因为NFS共享对客户端来说就是一个本地目录DNF读取的是文件系统路径底层的NFS协议栈被内核接管DNF根本不需要知道网络细节。用file://最自然也省去了在DNF里配置HTTP访问的时间。5.3 清理缓存、重建元数据、实装验证客户端执行以下命令让DNF重新认识仓库dnf clean all dnf makecache dnf repolist -vdnf repolist -v能看到每个仓库的元数据URL确认路径类似file:///mnt/dnf-repo/BaseOS/repodata/repomd.xml后说明DNF确实在读这个共享目录。接下来可以做一次真实安装测试dnf install -y tree htop安装期间可以另开终端执行nfsstat -m如果看到NFS挂载有读取流量就证明软件包数据是从NFS仓库拉取的不是本地缓存的结果。5.4 仓库更新的正确操作顺序服务端软件包变化后客户端并不是随时都能看到新版本这中间有两个关键步骤。服务端同步或添加包后必须重新生成元数据createrepo --update /storage/dnf-repo/BaseOS--update参数会基于现有元数据增量重建比全量重建快很多。然后在客户端刷新缓存dnf makecache --refresh整个过程就是“服务端更新包→重建元数据→客户端刷新缓存→DNF正常工作”顺序不能乱少了任一步都会造成客户端拿到旧索引或找不到新包的错觉。6. 实战踩坑记录权限、挂载参数和安全策略三座大山6.1 权限问题客户端能ls顶层目录但子目录无穷尽这个坑我踩到过两次现象非常一致客户端能列出/mnt/dnf-repo下的顶层目录但进入BaseOS/repodata时提示Permission denied。问题根源多半不在NFS配置而在Linux本地文件权限上——repodata目录如果权限是700服务端root能看客户端非root用户自然看不了。排查链路是先看文件权限ls -l /storage/dnf-repo/BaseOS/如果发现目录权限收敛直接放开读取权限chmod orX -R /storage/dnf-repo再检查exportfs -v里是否意外带了root_squash如果客户端root被映射成nobody读取权限会进一步受限。可信内网场景下把no_root_squash加回来能彻底绕开这一层映射问题。6.2 dnf makecache慢得像蜗牛NFS挂载参数背锅有一次我在客户端执行dnf makecache足足跑了十分钟才结束当时第一反应是网络有问题可网络明明一切正常。后来发现是NFS挂载参数太保守——默认开启的ac对目录属性缓存时间短每次DNF扫描大量文件时都要频繁向服务端发起属性请求。我当时的修复是在fstab挂载参数里加上noatime,rsize1048576,wsize1048576,actimeo60noatime去掉访问时间戳写入rsize/wsize加大NFS读写窗口actimeo60延长文件属性缓存时间。重新挂载后dnf makecache耗时从十分钟降到了两分钟左右效果立竿见影。6.3 SELinux和防火墙两种典型的拦截姿势SELinux出现拦截时服务端日志里通常能看到AVC记录。仓库存放在非标准路径时尤其容易遇到导出目录被SELinux拒绝的问题。先用真布尔值放开setsebool -P nfs_export_all_rw 1如果问题依旧检查文件上下文semanage fcontext -a -t public_content_rw_t /storage/dnf-repo(/.*)? restorecon -Rv /storage/dnf-repo防火墙方面NFS的坑通常是服务端端口没放行。手动验证方法是在客户端执行showmount -e 192.168.10.10如果超时或拒绝基本就是防火墙拦了rpcbind或mountd。放行方法firewall-cmd --permanent --add-servicenfs firewall-cmd --permanent --add-servicemountd firewall-cmd --permanent --add-servicerpc-bind firewall-cmd --reload6.4 元数据更新后客户端缓存不到新包服务端执行了createrepo --update客户端dnf makecache还是读到旧索引这个问题我在刚开始用仓库时困扰了很久。根本原因是NFS客户端对文件元数据有属性缓存服务端文件的mtime或大小变化没有立刻被客户端感知到。最简单有效的处理方法是客户端强制清一次缓存dnf clean all rm -rf /var/cache/dnf/* dnf makecache --refresh如果大批客户端都出现这个问题可以在挂载参数里加actimeo调整属性缓存时长或者更新服务端元数据后touch /storage/dnf-repo/BaseOS/repodata/repomd.xml让文件时间戳变化更明显客户端更容易识别到变化。6.5 多客户端并发安装时的体验问题几十台机器同时执行dnf install时NFS服务端的压力会瞬间增大。现象是安装过程整体变慢、个别任务超时。这不是NFS本身出错而是并发读请求撞到了服务端磁盘和网络吞吐上限上。我的优化思路有两条第一服务端磁盘用SSD或至少独立硬盘仓库路径不要和其他高IO服务混用第二批量任务尽量错峰用脚本在客户端之间加随机延时或者串行分发安装任务。NFSv4虽然有状态锁和会话管理但对纯并发读没有神奇加速效果物理资源才是硬道理。我在实际维护中越来越觉得内网软件源这件事投入的时间永远赚得回来。现在这批机器装的包版本完全一致更新时只要在仓库机上跑一遍同步脚本内网所有客户端一次性提升到同一版本之前那种“A机器装好了B机器还要再下一遍包”的日子再也没出现过。如果你也正在被内网机器软件源问题折磨按这个流程从一台仓库机加一块独立分区起步坑我都标得差不多基本能一路畅通。后面想再升级可以把这套仓库目录和PXE无人值守装机结合做成装机即用的完整环境那就是另一个阶段的事了。