ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DNF本地仓库+NFS共享:搭建内网统一软件源完整指南

DNF本地仓库+NFS共享:搭建内网统一软件源完整指南

前段时间我在整理一组内网机器,每台跑的都是一套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-utils
  • createrepo_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/iso

ISO内部一般自带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 --repoid=baseos --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] name=BaseOS Local Repository baseurl=file:///storage/dnf-repo/BaseOS enabled=1 gpgcheck=0

常用参数的含义我整理成了表格:

参数作用经验值
[id]仓库唯一标识,在DNF输出中显示不要包含空格,且全局唯一
name仓库描述,显示用写清楚源名称,方便排障
baseurl仓库实际地址,支持file、http、ftp等多种协议本地源必须是file://开头
enabled是否启用该仓库默认1,临时关闭可设为0
gpgcheck是否校验RPM包签名内网自制源建议0,减少配置麻烦
gpgkey公钥文件路径仅当gpgcheck=1时需要
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。
  • sync:NFS默认推荐使用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.1

exportfs -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] name=BaseOS via NFS baseurl=file:///mnt/dnf-repo/BaseOS enabled=1 gpgcheck=0 [appstream-local] name=AppStream via NFS baseurl=file:///mnt/dnf-repo/AppStream enabled=1 gpgcheck=0

有人会问,为什么不是http://而是file://?因为NFS共享对客户端来说就是一个本地目录,DNF读取的是文件系统路径,底层的NFS协议栈被内核接管,DNF根本不需要知道网络细节。用file://最自然,也省去了在DNF里配置HTTP访问的时间。

5.3 清理缓存、重建元数据、实装验证

客户端执行以下命令,让DNF重新认识仓库:

dnf clean all dnf makecache dnf repolist -v

dnf 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 o+rX -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,rsize=1048576,wsize=1048576,actimeo=60

noatime去掉访问时间戳写入,rsize/wsize加大NFS读写窗口,actimeo=60延长文件属性缓存时间。重新挂载后,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-service=nfs firewall-cmd --permanent --add-service=mountd firewall-cmd --permanent --add-service=rpc-bind firewall-cmd --reload

6.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无人值守装机结合,做成装机即用的完整环境,那就是另一个阶段的事了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进