很多运维新手或者刚接手内网环境的朋友,第一次面对“几十台机器都要装同一个软件包,但外网源慢得像蜗牛”这种场景时,第一反应往往是手动下载rpm包然后一台台scp过去。这种做法在小规模环境里勉强能用,一旦机器数量上来、依赖关系复杂起来,几乎就是一场灾难。我自己早年就干过这种傻事,拿着一块移动硬盘拷了几十个rpm包,跑到机房一台台装,结果装到第五台发现缺依赖,又得跑回去重新下载,来回折腾了一整天。
后来我学会了两件事:第一,用DNF(YUM的下一代版本)自建仓库,把所有的rpm包集中管理起来,客户端只要一条命令就能完成软件的安装和升级;第二,用NFS把仓库目录共享到整个内网,这样连HTTP服务都不用配,客户端直接挂载本地目录就能当软件源用。今天这篇博文,我就把这个组合方案完整地拆开讲清楚,从原理到实操,从纯内网离线环境到增量更新,每一步都给出可以直接照抄的命令和配置,同时把我踩过的那些坑也一并交代清楚。
1. 为什么是DNF仓库+NFS共享,而不是其他方案
先说结论:DNF仓库解决的是“软件包从哪儿来、怎么校验、怎么处理依赖”的问题,NFS共享解决的是“仓库目录怎么让所有机器都访问到”的问题。这两者一组合,就是一个典型的纯内网软件分发方案。
1.1 先搞清楚DNF仓库到底是个什么东西
DNF是Fedora、RHEL 8+、CentOS 8+、Rocky Linux、AlmaLinux、openEuler等发行版默认的软件包管理器,它替代了老一代的YUM。很多人以为DNF仓库是一个很神秘的东西,其实它的本质就是一个目录,这个目录里放着两类文件:
- 一类是真正的软件包文件,也就是rpm包,数量可能从几个到几千个不等;
- 另一类是元数据文件,它们集中在
repodata子目录下,记录着仓库里所有rpm包的名称、版本、依赖关系、校验值等信息。
当你执行dnf install的时候,DNF会先去读取repodata里的元数据,在内存中构建出完整的依赖关系树,然后根据这棵树决定需要安装哪些rpm包以及以什么顺序安装。
打个比方:rpm包就像超市货架上的一件件商品,而repodata就是商品旁边的电子价签和商品目录。没有商品目录,你根本不知道该买什么、价格是多少、生产日期是什么时候。同理,如果一个目录里只有rpm包而没有repodata,DNF是无法把它当作仓库来使用的。我们必须借助createrepo_c这个工具来生成元数据。
1.2 NFS在其中的角色
NFS(Network File System)是Linux/Unix系统之间最经典的文件共享协议。它的核心思路就是把服务器上的某个目录“挂载”到客户端本地的某个挂载点上,挂载之后,客户端对这个目录的读写操作几乎等同于对本地目录的操作。
DNF仓库本来可以通过HTTP、HTTPS、FTP等多种协议对外提供服务,其中最常用的是HTTP。但HTTP方案需要额外安装和配置Nginx或Apache,还要处理SELinux上下文、防火墙端口、虚拟目录路径等一堆细节。而NFS方案有一个天然优势:客户端可以把远程仓库目录直接挂载成file://协议的本地路径,DNF仓库的baseurl可以指向file:///mnt/dnfrepo这种本地地址,解析速度极快,而且不需要在仓库服务器上跑任何Web服务。
换句话说,NFS方案特别适合那种“内网环境、机器数量不大、不想引入太多中间组件”的场景。它把“网络传输”这件事下放到了内核级别的文件系统层,稳定性和传输效率都相当不错。
1.3 这个方案适合谁,不适合谁
写到这里得说句公道话。这个方案虽然好用,但不是万能的:
- 适合:纯内网离线环境,机器数量在几十台到几百台之间;对软件包分发时效性要求不高的场景;没有专职运维团队、不想维护Nginx配置的小团队;嵌入式开发环境,尤其是有ARM架构机器的场景(RK3568这类开发板通过NFS挂载rootfs然后从DNF仓库装包,是很多嵌入式工程师的标准操作)。
- 不太适合:跨公网分发的场景(NFS在公网上几乎是裸奔的,而且MTU、延迟问题会让人崩溃);机器量达到上千台且分布在不同网段的大规模集群(这种场景建议用HTTP+负载均衡,或者干脆用专门的企业级仓库管理工具如Spacewalk、Katello)。
搞清楚边界之后,后续的配置思路就不会跑偏了。
2. 搭建DNF仓库:从零到能用的完整过程
这一节是纯实操。我在下面的操作里统一以Rocky Linux 9为例(RHEL 9系、CentOS Stream 9、AlmaLinux 9、openEuler 22.03及以上的操作基本一致),仓库服务器IP假设为172.16.140.200。
2.1 准备基础环境
首先更新系统并安装生成仓库元数据所需的工具:
bash复制sudo dnf update -y
sudo dnf install -y createrepo_c nfs-utils
这里说明一下,createrepo_c是C语言实现的新一代createrepo工具,相比老的Python版本,它在处理数千个rpm包时速度更快、内存占用更小。建议直接用createrepo_c,而不是还在装老版的createrepo。
然后创建一个专门存放仓库文件的目录。我习惯用/data/dnfrepo,因为这个路径短、好记,也方便后面NFS共享配置:
bash复制sudo mkdir -p /data/dnfrepo
2.2 收集rpm包:两种方式
有了目录之后,最关键的问题来了:rpm包从哪里来?这里分两种典型场景。
场景一:在线环境,直接把外网仓库同步到本地
如果你所在的服务器能访问外网,最简单的办法是使用dnf reposync命令把某个外网仓库的rpm包全部同步到本地。比如,把Rocky Linux 9官方BaseOS仓库同步到本地:
bash复制sudo dnf install -y dnf-plugins-core
sudo dnf reposync --repoid=baseos --download-metadata -p /data/dnfrepo
这里的--download-metadata参数会自动把仓库的元数据也一并下载。同步完成后/data/dnfrepo/baseos目录下就会同时出现rpm包和repodata子目录,这种场景甚至不需要手动运行createrepo_c。
场景二:纯离线环境,手动收集rpm包
这是很多内网环境真实的样子。外网下个包都得审批,更别说同步整个仓库了。这种时候可以用一个笨但有效的办法:在一台能上外网的机器上,用dnf download命令把需要的软件包连同依赖一起下载下来。比如,要下载nginx及其所有依赖:
bash复制mkdir -p /data/dnfrepo/nginx
cd /data/dnfrepo/nginx
dnf download --resolve --alldeps nginx
--resolve表示解析依赖,--alldeps表示包含所有依赖包(包括弱依赖)。下载完成后,这些rpm包就全都在当前目录里了。
提示:
dnf download默认只下载包不安装,这一点和yumdownloader是一样的。如果你要下载的包很多,建议写个脚本批量下载,别一个个手敲命令,否则会疯。
2.3 生成仓库元数据,并理解仓库签名校验机制
无论rpm包是什么方式收集来的,只要你是手动攒的目录(也就是目录里没有repodata),都必须在放置rpm包的顶层目录执行一次元数据生成:
bash复制cd /data/dnfrepo
sudo createrepo_c .
执行完毕后,/data/dnfrepo下会出现一个repodata目录,里面至少有一个以*-primary.xml.gz结尾的文件,这就是仓库的核心索引。
这里要强调一个非常容易踩坑的点:如果你把多个来源的rpm包放在同一个目录里,然后执行一次createrepo_c,DNF会默认校验所有包的GPG签名。 如果这些包没有用同一个GPG key签名,或者根本没有签名,客户端在安装时会报Public key for xxx.rpm is not installed之类的错误。解决方案通常是在客户端配置里加上gpgcheck=0,或者导入对应的GPG key。内网环境我一般直接关掉校验,因为包都是从可信渠道下载的,没必要在这个环节上给自己添堵。
2.4 仓库目录结构规划
这一步我个人强烈建议在动手之前就想清楚。很多方案里,仓库目录只有一层,所有rpm包全堆在一起。几百个包的时候还行,几千个包的时候ls都会卡顿,而且后续增量更新时很难维护。
我常用的目录结构是按软件来源或用途分目录:
text复制/data/dnfrepo/
├── baseos/ # 操作系统基础包
├── appstream/ # 应用流包(nginx、mysql等)
├── thirdparty/ # 第三方软件
├── custom/ # 自己打的rpm包
└── repodata/ # 这是顶层仓库的元数据,如果顶层也要作为仓库
每个子目录都执行一次createrepo_c,每个子目录就是一个独立的软件源。客户端配置时,一个仓库指向多个baseurl即可。这样做的好处是:不同来源的包互不干扰,更新时只需要重新生成对应子目录的元数据,效率高很多。
3. NFS共享服务:让仓库目录“变成”本地目录
仓库目录就绪后,下一步就是让内网的其他机器能访问到这个目录。如果你网络里只有一台机器需要装软件(比如你自己那台),那NFS这一步可以完全跳过,直接用file://协议指向本机目录就行。但咱们讲的既然是“共享”,重点还是在NFS。
3.1 配置NFS服务端
nfs-utils已经装好了,现在直接修改NFS导出配置文件/etc/exports:
bash复制sudo vim /etc/exports
写入如下内容:
text复制/data/dnfrepo 172.16.140.0/24(rw,sync,no_root_squash,no_subtree_check)
这一行的含义要逐段拆解:
/data/dnfrepo:要共享出去的目录绝对路径。172.16.140.0/24:允许访问的网段。如果你希望所有机器都能访问,可以写成*,但我不建议在内网也这样搞,能限定网段就限定。rw:允许读写。理论上只需ro(只读)就能满足DNF仓库的使用,但为了将来能直接在客户端调试、往里丢包,可以先用rw。sync:数据写入服务器磁盘后才返回成功,避免掉电丢数据。no_root_squash:默认情况下,NFS会把客户端的root用户映射为匿名用户(nobody),这会导致客户端用root写文件时权限报错。no_root_squash的作用是保留root权限,这样客户端可以正常写文件。内网环境问题不大,但也意味着风险更高,自己权衡。no_subtree_check:禁用子树检查,提升稳定性,避免出现“Permission denied”的莫名其妙的问题。
保存退出后,执行以下命令使导出配置生效:
bash复制sudo exportfs -r
sudo systemctl restart nfs-server
sudo systemctl enable nfs-server
提示:如果要确认导出配置是否正确,可以用
showmount -e localhost查看。
3.2 防火墙和SELinux这两个拦路虎
这一步是NFS配置中最容易出问题的环节,我单独拎出来讲。
防火墙问题:NFS服务依赖的端口不仅仅是2049这一个,它还需要rpcbind(111端口)以及一堆动态端口(默认在32768-60999区间)。所以防火墙如果开了,需要一次性放行这些服务:
bash复制sudo firewall-cmd --permanent --add-service=nfs
sudo firewall-cmd --permanent --add-service=rpc-bind
sudo firewall-cmd --permanent --add-service=mountd
sudo firewall-cmd --reload
如果你的系统用的是ufw,类似地放行nfs和rpcbind即可。
SELinux问题:这是最大的坑。SELinux不关的话,客户端即使能挂载上NFS导出目录,也无法正常读写。最省事的做法是直接把SELinux设为permissive(宽容模式),但不建议直接禁用,因为禁用后重启SELinux策略状态会乱:
bash复制sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
如果你实在不想关SELinux,也可以为共享目录设置正确的SELinux文件类型。比如手动把/data/dnfrepo的默认类型设成public_content_rw_t,同时开启NFS相关布尔值:
bash复制sudo semanage fcontext -a -t public_content_rw_t "/data/dnfrepo(/.*)?"
sudo restorecon -Rv /data/dnfrepo
sudo setsebool -P nfs_export_all_rw 1
sudo setsebool -P nfs_export_all_ro 1
但我个人的经验是,内网环境直接permissive最省心,别把时间浪费在SELinux的布尔值排列组合上。你服务端permissive并不影响系统整体安全性,因为内网NFS本身就不是一个高安全性的方案。
3.3 客户端挂载与开机自动挂载
在客户端机器上,用mount命令即可把远程目录挂载到本地:
bash复制sudo mkdir -p /mnt/dnfrepo
sudo mount -t nfs 172.16.140.200:/data/dnfrepo /mnt/dnfrepo
挂载成功后,执行df -h | grep dnfrepo,如果能看到远程目录的信息就说明挂载正常。为了开机自动挂载,把配置写入/etc/fstab:
text复制172.16.140.200:/data/dnfrepo /mnt/dnfrepo nfs defaults,_netdev,nofail 0 0
解释一下为什么用这两个参数:
_netdev:告诉系统这是网络设备,必须等网络就绪后再挂载,避免开机时网络没起来导致挂载失败、卡住启动流程。nofail:如果NFS服务器不可达,不要报错,跳过挂载继续启动,避免系统启动卡死。
这两个参数是生产环境的标配,没有它们,服务器一重启就可能卡在挂载等待上,那一幕非常煎熬。
4. 客户端配置DNF源:把仓库目录变成软件源
NFS挂载成功之后,实质上DNF仓库目录已经“变成”了客户端本地的/mnt/dnfrepo目录。现在要做的事,就是告诉DNF这个目录是一个软件仓库。
4.1 配置仓库文件
在客户端上新建一个DNF仓库配置文件:
bash复制sudo vim /etc/yum.repos.d/local-dnf.repo
写入如下内容:
ini复制[local-dnf]
name=Local DNF Repository via NFS
baseurl=file:///mnt/dnfrepo
enabled=1
gpgcheck=0
这里最核心的就是baseurl=file:///mnt/dnfrepo。因为NFS已经把远程目录挂载到本地,DNF访问这个源就等同于访问本地目录,不需要走HTTP请求,解析效率极高。gpgcheck=0是关闭GPG签名校验,理由前面已经说过。
4.2 验证仓库是否生效
配置完别急着装包,先验证一下仓库能不能被正确识别:
bash复制sudo dnf clean all
sudo dnf makecache
sudo dnf repolist
如果一切正常,repolist的输出里应该能看到local-dnf这一项,且状态不是0(即仓库中有可用的包)。如果repolist显示的是0,说明DNF没有检索到任何rpm包,大概率是createrepo_c生成的元数据和baseurl指向的目录不匹配——最常见的错误是createrepo_c在子目录执行的,而baseurl指向了父目录。
4.3 测试安装一个软件包
现在可以试试用新仓库安装软件了:
bash复制sudo dnf install -y htop
如果仓库包里有htop,它会直接安装成功。如果提示找不到这个包,也不要慌,可能是包不在仓库中,先去仓库目录确认有没有这个rpm包,没有的话就补进去然后重新生成元数据。
4.4 关于仓库更新:为什么客户端有时候看不到新包
这是内网仓库使用中最容易让人抓狂的一个问题:服务端明明加了一个新包并重新执行了createrepo_c,客户端dnf install却还是找不到。
原因其实很简单:客户端的DNF有元数据缓存机制。dnf makecache生成的缓存不会自动过期,除非你设置了metadata_expire参数,或者缓存超过默认的48小时。解决方法是每次服务端更新完仓库后,在客户端执行:
bash复制sudo dnf clean all
sudo dnf makecache
或者,在仓库配置中直接加一行:
ini复制metadata_expire=0
把元数据过期时间设为0,表示每次执行dnf命令都重新拉取元数据。这样做的代价是每次命令都会多花一点时间在元数据下载上,对于NFS这种本地挂载来说时间开销几乎可以忽略不计,但对于HTTP远程仓库来说可能就不太合适了。内网环境我肯定推荐metadata_expire=0,省心。
5. 增量更新与多架构支持:让仓库“活”起来
仓库不是建好就不动了,它是一个持续演进的东西。这一节讲两个很实际的问题:如何往仓库里加新包、如何支持x86_64和ARM64等多架构。
5.1 增量更新操作流程
比如你要往/data/dnfrepo里加一个新软件包myapp-1.2.0-1.el9.x86_64.rpm:
- 把rpm包拷贝到
/data/dnfrepo目录:
bash复制sudo cp myapp-1.2.0-1.el9.x86_64.rpm /data/dnfrepo/
- 在仓库目录重新生成元数据:
bash复制cd /data/dnfrepo
sudo createrepo_c --update .
注意这里我加了--update参数。这个参数的作用是增量更新,它不会重新扫描所有rpm包,而是复用已有的元数据,只处理新增或变更的包。在包数量很大时,加与不加这个参数,执行时间可能是几秒和几分钟的差别。
- 客户端侧操作(如果需要立即生效):
bash复制sudo dnf clean all
sudo dnf makecache
三步走完,新包就能在客户端搜到并安装了。
5.2 多架构仓库怎么组织
很多内网环境既有x86_64的服务器,又有ARM64(比如RK3568、树莓派)的机器。如果把两个架构的rpm包放在同一个仓库目录里,DNF会报错或者无法正常安装(因为DNF要求仓库的架构必须一致)。
推荐的方案是拆分目录:
text复制/data/dnfrepo/
├── x86_64/
│ ├── baseos/
│ └── appstream/
└── aarch64/
├── baseos/
└── appstream/
每个架构目录各自执行createrepo_c。客户端配置时,根据自身的架构指向对应的baseurl。NFS共享出去的时候,直接把/data/dnfrepo整个共享出去,客户端mount之后,x86_64机器配置file:///mnt/dnfrepo/x86_64/baseos,ARM64机器配置file:///mnt/dnfrepo/aarch64/baseos,互不干扰。
提示:NFS挂载本身不区分架构,它是文件系统层面的共享,客户端能拿到所有架构的包,怎么用取决于DNF配置指向哪里。所以同一个NFS共享可以同时服务两种架构的机器,这也是NFS方案比HTTP方案在这个场景下的一个隐形优势——不需要为不同架构单独配置不同的HTTP站点路径。
5.3 如何清点仓库内容,避免“库里有什么”全凭记忆
当你维护的仓库越来越大,“我仓库里到底有哪些包”这个问题会变得非常难以回答。我常用的做法是定期用下面的命令生成一个包清单:
bash复制cd /data/dnfrepo
dnf --repofrompath=tmp,file:///data/dnfrepo --repo=tmp list available > /data/dnfrepo-packages-list.txt
生成清单后,可以当作日常审计材料,也能用来快速确认某个包是否存在于仓库中。
6. 避坑锦集:这些年的NFS和DNF仓库实操教训
最后一部分,我把这些年实打实踩过的坑集中列出来,每个都附带排查思路,希望帮你少走弯路。
6.1 坑一:NFS挂载后客户端写入失败
现象:客户端mount成功,也能ls,但只要往共享目录里写文件就提示Permission denied。
排查链路:
- 先看服务端是否设置了
readonly导出(显然这种情况下只能读不能写)。 - 再看服务端目录的属主和权限:
ls -ld /data/dnfrepo。NFS的权限检查本质上是服务端文件系统的权限检查,即使客户端是root,如果服务端目录不是root拥有的,照样写不进去。 - 再看
/etc/exports里是否写了rw,exportfs -v可以查看实际生效的导出选项。 - 最后查SELinux,这是最常见的元凶。如果服务端SELinux处于enforcing状态,即使权限位全对,也可能因为SELinux类型不对而拒绝NFS客户端写入。
结论:这个问题80%以上是由SELinux引起的,其次是root_squash导致root被映射成了nobody。建议按上面顺序排查。
6.2 坑二:客户端dnf提示Failed to download metadata for repo
现象:baseurl=file:///mnt/dnfrepo,执行dnf makecache时报错Failed to download metadata for repo 'local-dnf'。
排查链路:
- 看
/mnt/dnfrepo/repodata目录是否存在。没有就说明createrepo_c没执行成功,重新执行。 - 看
repodata目录里的repomd.xml文件是否能正常读取。cat /mnt/dnfrepo/repodata/repomd.xml,如果提示权限问题,查NFS服务端的目录权限。 - 确认
baseurl是否拼写正确,file:///mnt/dnfrepo是三个斜杠,少一个就变成了file:/mnt/dnfrepo,DNF也能解析但顺序会乱。 - 确认目录下确实有rpm包,而不是只有空目录。
结论:绝大多数情况是createrepo_c执行路径和baseurl指向路径不一致。
6.3 坑三:客户端挂载NFS后开机卡住
现象:重启客户端系统,启动过程卡在A start job is running for /mnt/dnfrepo,一等就是几分钟甚至更久。
排查链路:这是/etc/fstab里没有加_netdev和nofail参数导致的。系统在启动时去挂载网络文件系统,但此时网络服务还没就绪,就一直等待。
结论:/etc/fstab里的NFS挂载条目必须包含_netdev,nofail,这是铁律。
6.4 坑四:服务端往仓库目录拷包,客户端立即就能看到吗
答案:取决于你客户端有没有执行dnf clean all。前面讲过了,DNF有元数据缓存,即使服务端更新了仓库,客户端依然使用旧的元数据,所以搜不到新包。这不是BUG,是设计如此,理解了缓存机制就很好处理。
6.5 坑五:createrepo_c执行报错Errno 13 Permission denied
现象:在仓库目录执行sudo createrepo_c .时,报权限错误。
排查链路:多半是当前用户对该目录没有写权限,sudo执行的话,检查目录是否是NFS挂载目录。如果仓库目录本身就是用NFS挂载的(比如服务端和客户端角色混在一起),还要考虑no_root_squash是否配置正确。
写在最后的个人体会
这套DNF+NFS的组合方案,我现在已经用了两年多,管理着公司内网一百多台机器和嵌入式开发板的软件分发。最初搭的时候也走了不少弯路,尤其是SELinux和防火墙那一关,折腾了整整一个晚上,后来把整个排查流程写成了内部文档,大家照着操作基本一次就能通。
如果让我给刚接触这套方案的你提一个建议,那就是先在一台服务器上把仓库建好,客户端能正常装包之后,再去做NFS共享。把两步拆开,每一步都验证通过再往下走,排查问题的范围会小很多。别想着一步到位,那只会让你在多个环节的错误叠加中迷失方向。
最后补充一个小技巧:如果你的仓库目录很大(比如几个GB),在createrepo_c生成元数据的时候,可以考虑加-d参数开启数据库后端(默认就是sqlite),它对大量包的元数据查询效率会有明显提升。这条参数平时根本不会有人提,但你真正用到几千个包的仓库时,就会发现它的价值。
