1. 为什么需要自建DNF仓库并用NFS分发:内网统一软件源的业务场景
有个场景我估计不少运维都遇到过:内网几十台服务器,不能直接访问外网源,每次装软件都要先跑到能上网的机器上把rpm包一个个拉下来,再拷进去。更麻烦的是,不同机器要装同一个软件,过程得重复好几遍,拷贝过程中偶尔还会混入不兼容的包版本,装出来的环境五花八门,排查问题的时候人都要裂开。我这次要说的,就是针对这种场景的一套解决方案——在内网服务器上部署一个DNF仓库,然后通过NFS共享服务把仓库目录分发给所有客户端。
这里稍微解释一下两个名词。DNF仓库(dnf repository)本质上就是一个有特殊目录结构的软件包集合:里面放着rpm包文件,还有一个叫repodata的元数据目录,里面记录了rpm包的名称、版本、依赖关系、文件清单等信息。dnf或者yum通过读取这些元数据,就能知道从哪个包开始装、依赖哪些包、去哪里下载。而NFS(Network File System)是一种基于RPC(Remote Procedure Call)机制的网络文件系统,它可以让客户端像访问本地目录一样访问服务端共享出来的目录。
把两者结合起来的思路很直接:先在服务端用createrepo把一堆rpm包做成一个标准DNF仓库,再通过NFS把整个仓库目录共享出去。客户端挂载NFS后,把dnf仓库的baseurl指向挂载目录,就能直接dnf install任意软件包,依赖自动解析,版本统一管理,不用再一台台手动拷贝。
那有人会问,走HTTP协议共享仓库不是更常见吗?确实,很多公网软件源都是走HTTP,比如阿里云镜像、清华镜像。但内网场景下,NFS有几个很实际的优势:
- 不需要额外部署nginx/apache,系统自带的nfs-utils就够用,架构简单。
- 客户端挂载后,仓库就是一个本地目录,dnf的
file://协议读取非常稳定,不会出现网络源偶尔连接超时、下载中断这类问题。 - 如果后续想把某些rpm包手工放进仓库,直接在客户端挂载目录里操作即可(需要在服务端开放写权限),不用走HTTP上传工具。
- 内网环境网络可控,NFS的传输速度和稳定性都很好。
当然,NFS也不是万能的。如果客户端数量巨大(几十台以上)、分布在不同网段,或者仓库需要跨公网访问,那还是建议走HTTP。我这次环境是同一个二层网段内的十几台机器,NFS就是最省事的选择。
下面这张图可以概括整体架构:
| 角色 | 机器 | 作用 |
|---|---|---|
| NFS服务端 + DNF仓库宿主机 | 192.168.1.10 | 存放rpm包,生成repodata,共享目录 |
| NFS客户端 + DNF客户端 | 192.168.1.x(若干台) | 挂载NFS目录,配置local仓库,dnf install |
| 仓库目录 | /data/repo | 按base/updates/extras等子目录组织 |
这套方案最适合的场景是:内网离线环境、需要统一软件版本的中小型集群、没有外部软件源权限的机房。只要准备好一批rpm包,服务端一次搭好,客户端配置一次,后续所有装软件的操作都变得非常丝滑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 服务端准备:仓库目录结构与基础环境规划
2.1 环境规划表
动手之前,先把环境规划清楚。我这次用的是CentOS Stream 9作为服务端,客户端也是同一个大版本的系统。这里建议,如果可能的话,客户端和服务端的发行版大版本尽量保持一致,否则rpm包在跨版本安装时容易出现依赖不兼容的问题。
| 项目 | 规划值 | 说明 |
|---|---|---|
| 服务端系统 | CentOS Stream 9 x86_64 | 仓库宿主机 |
| 服务端IP | 192.168.1.10 | 静态IP,固定不变 |
| 客户端系统 | CentOS Stream 9 / Rocky Linux 9 | 同版本即可 |
| 客户端网段 | 192.168.1.0/24 | NFS共享范围 |
| 仓库根目录 | /data/repo | 建议单独分区或独立磁盘 |
| 仓库子目录 | base / updates / extras | 按用途分目录,避免元数据混杂 |
| 挂载点(客户端) | /mnt/dnf-repo | 客户端统一路径 |
这里特别提醒一下:仓库目录的可用空间一定要提前估算。一个base仓库全量同步下来,大约需要10GB到20GB,如果加上updates,可能到30GB以上。我这次因为只做基础环境,所以只放了base和updates两个子集,一共约15GB。建议用df -h确认空间后,再决定同步节奏。
2.2 安装createrepo_c、nfs-utils、rpcbind
服务端需要安装的工具主要是三个:createrepo_c(用于生成DNF仓库元数据)、nfs-utils(提供NFS服务端程序)、rpcbind(NFS依赖的RPC端口映射服务)。
bash复制dnf install -y createrepo_c nfs-utils rpcbind
关于createrepo的版本,这里多说一句。老版本里叫createrepo,是一个Python写的工具,createrepo_c是它的C语言重写版,性能差异非常明显。我对比过,在一个包含3000多个rpm包的目录上,createrepo_c生成元数据大概只要10秒左右,而createrepo可能要一两分钟。而且createrepo_c生成的元数据格式与dnf完全兼容,所以新环境直接装createrepo_c就好,不用纠结。
如果你用的系统是Ubuntu/Debian,对应的包名是createrepo-c和nfs-kernel-server,安装命令不同但思路一致。整个方案本质上是跨发行版通用的,只是dnf在Debian系里要换成apt,这部分就不展开了。
2.3 创建仓库目录结构
目录结构的设计我建议一步到位,因为后续改起来很麻烦。根目录/data/repo下面,我按base、updates、extras分了三个子目录,每个子目录里面再放一个Packages目录放rpm包,元数据则由createrepo生成在子目录根部。
bash复制mkdir -p /data/repo/base/Packages
mkdir -p /data/repo/updates/Packages
mkdir -p /data/repo/extras/Packages
为什么要这样分?因为base、updates、extras在发行版里面对应不同的软件源类别,它们的rpm包来源不同、更新频率不同、依赖关系也可能交错。如果全塞在一个目录里,createrepo会把所有包当作一个整体来生成元数据,依赖解析会变得混乱。分开之后,客户端可以针对每个子仓库独立启用或禁用,灵活性高很多。
除了这些子目录,我还在/data/repo下放了一个README文件,里面写清楚目录命名规则、如何增加rpm包、如何重新生成元数据。这个习惯帮了大忙——隔了几个月再维护时,不用靠记忆,看README就能快速上手。这也是我建议所有做运维的朋友养成的习惯,仓库类服务尤其需要文档。
3. 用createrepo构建可用的DNF仓库元数据
3.1 准备rpm包:三种常见来源
有了目录结构,下一步就是往Packages目录里塞rpm包。根据环境不同,rpm包一般有三个来源。
第一种,在线环境同步镜像仓库。如果服务端能访问外网,可以用reposync工具把某个远程仓库的rpm包整体同步到本地。比如同步CentOS Stream 9的base仓库:
bash复制dnf install -y dnf-plugins-core
dnf reposync --repoid=base --download-metadata --downloaddir=/data/repo
--download-metadata参数会把远程仓库的元数据也下载下来,这样后续就可以直接用这些元数据,省去本地生成的时间。不过我这里因为要演示完整的createrepo流程,所以选择只同步rpm包,然后在本地生成元数据。
第二种,离线环境从已有机器拷贝。这是最常见的内网场景。找一台能上网的机器,dnf download下载需要的rpm包,或直接从/var/cache/dnf缓存目录里把rpm拷贝出来,然后传到服务端指定目录。这里有个经验,拷贝时最好把整个Packages目录一起拷,别只拷部分包,否则依赖关系不完整,客户端安装时还是会报“找不到依赖”的错误。
第三种,手工加入特定rpm包。有些企业内部自研的软件包也需要放进仓库统一分发,此时直接拷贝到Packages目录即可。但要注意,自研包的命名最好符合rpm命名规范(name-version-release.arch.rpm),否则createrepo在解析时可能报warning。
我这次用的是第二种方式,从一台已安装好基础软件的机器上,把/var/cache/dnf里的缓存rpm包拷贝到了服务端的/data/repo/base/Packages,大约2800多个包,用rsync传输大概花了20分钟,大小约10GB。
3.2 生成repodata元数据
rpm包就位后,进入对应目录执行createrepo_c:
bash复制cd /data/repo/base
createrepo_c .
执行完毕后,目录下会多出一个repodata子目录,里面有这些关键文件:
repomd.xml:元数据的总入口,记录了其他元数据文件的路径、校验和、时间戳。primary.xml.gz:每个rpm包的名称、版本、依赖、提供能力等核心信息。filelists.xml.gz:每个rpm包包含的具体文件清单,用于按文件路径查询软件包。other.xml.gz:包的其他信息,如changelog。
dnf在运行时会先下载/读取repomd.xml,再根据它找到primary.xml.gz,从而构建软件包列表。如果这三个文件中的任何一个与实际rpm包不一致,就会导致软件包列表不完整或依赖解析出错。
createrepo_c生成元数据后,还有一个常用的操作是增量更新。比如你往Packages目录里新增了几个rpm包,不需要重新生成全部元数据,用--update参数即可:
bash复制createrepo_c --update .
--update会对比已有元数据和当前目录下的rpm包,只重新生成发生变化的部分,速度比全量生成快非常多。我实测在几千个包的基础上新增几十个包,update操作只需要几秒钟。
3.3 验证仓库可用性
在服务端本地验证一下仓库是否可用。先创建一个临时repo文件:
bash复制cat > /etc/yum.repos.d/test-local.repo << 'EOF'
[test-local]
name=Test Local Base Repository
baseurl=file:///data/repo/base
enabled=1
gpgcheck=0
EOF
然后执行:
bash复制dnf clean all
dnf repolist
如果输出中能看到test-local仓库,并且列出了包数量,说明元数据生成成功。下面是我这里执行dnf repolist的截取输出:
code复制repo id repo name status
test-local Test Local Base Repository 2,847
再尝试安装一个包验证依赖解析是否正常:
bash复制dnf install -y httpd
如果httpd及其依赖的十几个包都能正常安装,说明仓库可用,依赖关系没有缺失。
3.4 大型仓库的优化配置
如果你要管理的是几千甚至上万个rpm包的大型仓库,还有几个参数值得关注:
bash复制createrepo_c --workers=8 --compress-type=gz --retain-old-md=0 .
--workers=8:指定8个并行线程处理,能明显加快元数据生成速度。默认情况下createrepo_c会根据CPU核数自动调整,但手动指定也很有用。--compress-type=gz:元数据压缩格式,默认是gz,也可以换成xz或zstd。gz兼容性最好,zstd压缩率最高、解压最快,但老版本dnf可能不支持。我建议内网环境统一用gz,省得踩兼容性坑。--retain-old-md=0:不保留旧的元数据文件,避免repodata目录里堆积历史元数据,占用额外的磁盘空间。
另外,如果你的服务端内存比较小(比如2GB以下),生成元数据时可能会遇到内存不足的问题。这时候需要限制进程并发数,或者考虑分批生成元数据。不过正常x86服务器内存都在8GB以上,这个情况很少见。
4. NFS服务端配置:exports规则与启动细节
4.1 启动rpcbind和nfs-server
DNF仓库就绪后,接下来把整个仓库目录通过NFS共享出去。这里有一个容易忽略的点:NFS依赖rpcbind来注册和查询RPC服务,所以先确保rpcbind在运行,再启动nfs-server。
bash复制systemctl enable --now rpcbind
systemctl enable --now nfs-server
实际上systemd的依赖关系会自动处理启动顺序,但排查问题时还是要清楚它们之间的依赖。如果rpcbind没有启动,客户端执行showmount -e或者mount -t nfs会收到类似“RPC: Unable to receive”的错误。
启动后可以用rpcinfo -p查看RPC服务注册情况:
bash复制rpcinfo -p localhost
输出中应该能看到nfs、mountd、portmapper等条目。如果看不到mountd或nfs相关记录,说明服务没有正常启动,需要查看日志:
bash复制journalctl -u nfs-server -u rpcbind -n 50
4.2 exports文件配置详解
NFS的共享规则配置在/etc/exports文件中。以下是我在这次环境中的配置:
code复制/data/repo 192.168.1.0/24(ro,sync,no_root_squash,no_subtree_check)
一行配置,含义从前往后分别是:共享目录、允许访问的客户端网段、括号内的导出选项。
各选项的作用如下:
| 选项 | 作用 | 推荐值 |
|---|---|---|
| ro / rw | 共享目录是只读还是可读写 | DNF仓库用ro即可,安全 |
| sync | 写操作同步落盘,保证数据一致性 | 固定用sync |
| no_root_squash | 客户端root用户映射为服务端root,拥有完全权限 | 内网可用,公网不建议 |
| no_subtree_check | 关闭子目录检查,减少告警并提升性能 | 推荐加上 |
这里解释一下root_squash和no_root_squash的区别。默认情况(即root_squash)下,客户端的root用户会被映射成服务端的nobody用户,这样即使客户端是root,在服务端也只是一个普通用户权限,这是NFS的一种安全机制。但代价是,客户端在挂载目录里创建文件时,文件的属主可能变成nobody,权限不好控制。内网环境中,如果目录是只读共享给客户端,用ro+no_root_squash问题不大。如果目录需要客户端写入(比如让客户端直接更新仓库里的rpm包),那么就需要用rw+no_root_squash,并且要谨慎评估安全风险。
4.3 使用exportfs重载并验证
修改完/etc/exports后,不需要重启NFS服务,只需要执行:
bash复制exportfs -ra
-r表示重新导出所有目录,-a表示导出全部(或者全部撤销后再导出)。执行后可以通过以下命令查看当前共享列表:
bash复制showmount -e localhost
我这里的输出:
code复制Export list for localhost:
/data/repo 192.168.1.0/24
到这里,服务端的NFS共享已经配置完成。正常情况下,客户端就能挂载这个目录了。不过先别急,多一步检验总是好的。在服务端本机挂载一次,验证NFS整个链路是否通:
bash复制mkdir -p /mnt/nfs-test
mount -t nfs 192.168.1.10:/data/repo /mnt/nfs-test
ls /mnt/nfs-test
umount /mnt/nfs-test
这一步如果成功,说明NFS服务端导出、rpcbind注册、端口映射等环节都是正常的,问题大概率出在客户端或网络层面。
5. 客户端接入:NFS挂载与DNF仓库文件配置
5.1 客户端安装nfs-utils并挂载NFS目录
回到客户端,首先确认客户端安装了nfs客户端工具。CentOS/RHEL系列默认可能只有mount.nfs的基础支持,但为了保险起见,统一安装nfs-utils:
bash复制dnf install -y nfs-utils
然后创建挂载点并挂载:
bash复制mkdir -p /mnt/dnf-repo
mount -t nfs 192.168.1.10:/data/repo /mnt/dnf-repo
挂载后验证一下:
bash复制ls /mnt/dnf-repo
df -h /mnt/dnf-repo
df -h的输出应该能看到类似这样的内容:
code复制Filesystem Size Used Avail Use% Mounted on
192.168.1.10:/data/repo 50G 15G 35G 30% /mnt/dnf-repo
如果你希望客户端重启后自动挂载,需要写入/etc/fstab。注意NFS挂载不同于本地磁盘,需要加_netdev选项,确保网络就绪后再挂载,否则开机时网络未启动可能导致挂载失败。
bash复制echo "192.168.1.10:/data/repo /mnt/dnf-repo nfs defaults,_netdev 0 0" >> /etc/fstab
5.2 配置本地DNF仓库文件
挂载成功后,在客户端创建dnf仓库配置文件/etc/yum.repos.d/local.repo:
bash复制vi /etc/yum.repos.d/local.repo
内容如下:
code复制[local-base]
name=Local Base Repository
baseurl=file:///mnt/dnf-repo/base
enabled=1
gpgcheck=0
[local-updates]
name=Local Updates Repository
baseurl=file:///mnt/dnf-repo/updates
enabled=1
gpgcheck=0
这里要特别注意几点:
第一,baseurl使用的是file://协议,直接指向本地的NFS挂载目录。dnf对本地路径的读取非常稳定,不会像HTTP源那样出现超时或断连。
第二,gpgcheck=0。公网仓库一般都有GPG签名,用来验证软件包来源的完整性。自建仓库默认没有签名检查,所以这里必须设置gpgcheck=0,否则dnf安装时会报“Public key for xxx.rpm is not installed”或“Signature verification failed”之类的错误。
第三,如果客户端原本配置了其他外网源,建议把外网源的enabled设为0,或者在安装时通过--disablerepo参数禁用。否则dnf会同时读取多个源,解析依赖时可能从外网源拉取版本不一致的包,失去了自建仓库统一版本的意义。
配置完成后,刷新缓存并验证:
bash复制dnf clean all
dnf makecache
dnf repolist
正常输出中会看到local-base和local-updates两个仓库,状态显示已启用。然后测试安装一个包:
bash复制dnf install -y vim
依赖解析、下载、安装全程走的是本地NFS目录,整个过程非常快,十几秒就完成。
5.3 批量客户端的配置分发
只有一两台客户端,手工配置就够。如果我这种十几台机器的情况,建议做个简单的分发脚本,或者用Ansible统一推送。
使用Ansible的思路是:把local.repo文件分发到所有客户端,并在分发前通过一个playbook统一安装nfs-utils、创建挂载点、写入fstab、挂载NFS。我这里提供一个最小化的Ansible示例:
yaml复制- hosts: all
tasks:
- name: Install nfs-utils
dnf:
name: nfs-utils
state: present
- name: Create mount point
file:
path: /mnt/dnf-repo
state: directory
- name: Mount NFS share
mount:
path: /mnt/dnf-repo
src: 192.168.1.10:/data/repo
fstype: nfs
opts: defaults,_netdev
state: mounted
- name: Deploy local repo config
copy:
src: local.repo
dest: /etc/yum.repos.d/local.repo
notify:
- dnf clean all
如果没有配置Ansible环境,也可以用shell循环配合scp批量操作,效果类似。这里核心思路是:这些操作是完全幂等的,反复执行不会出问题,非常适合批量分发。
5.4 仓库更新后客户端如何同步
后续维护时,服务端往Packages目录新增rpm包并执行createrepo_c --update后,客户端不需要重新挂载NFS,因为NFS是实时共享的,目录里的文件和元数据会立即反映到客户端。但dnf的缓存需要刷新,否则它读到的还是旧的元数据。
bash复制dnf clean all
dnf makecache
这里有个容易被忽略的点:NFS共享虽然实时,但dnf在运行时会把元数据缓存到本地/var/cache/dnf目录,它不一定每次安装操作都重新读取NFS上的repodata。所以服务端更新仓库后,记得在客户端执行一次dnf clean all,这一点非常重要。否则你可能会遇到“明明在仓库里放了新包,客户端却装不上”的诡异问题。
6. 权限、防火墙与挂载异常:常见问题排查实录
6.1 客户端mount失败:Connection timed out
这是NFS共享部署中最常见的坑之一。客户端执行mount -t nfs后卡了很久,最终报错:
code复制mount.nfs: Connection timed out
排查思路分三步走:
第一步,确认服务端rpcbind和nfs-server进程状态:
bash复制systemctl status rpcbind
systemctl status nfs-server
第二步,确认服务端showmount -e能看到共享:
bash复制showmount -e 192.168.1.10
如果showmount能看到共享但客户端mount超时,大概率是防火墙问题。NFS除了2049端口外,还会使用rpc.mountd等动态分配端口。防火墙默认只放行了一部分端口,NFS的动态端口无法正常通过,就会导致mount超时。
解决办法有两个。第一个办法是开放NFS动态端口范围。在服务端的/etc/nfs.conf中,可以固定mountd和nlockmgr等服务的端口。以CentOS Stream 9为例,在[mountd]段设置port=4001,在[lockd]段设置port=4002等。然后在防火墙中放行这些端口:
bash复制firewall-cmd --permanent --add-service=nfs
firewall-cmd --permanent --add-service=rpc-bind
firewall-cmd --permanent --add-service=mountd
firewall-cmd --reload
如果你用的是iptables,则需要手动添加对应的端口规则。
第二个办法更省事:直接在防火墙里放行NFS相关的服务。对firewalld来说,直接开放nfs、rpc-bind、mountd这三个服务是最稳妥的,因为每个服务对应的端口范围会被自动放行。我这次环境用的是firewalld,执行上面的三条命令后问题立刻解决。
6.2 挂载成功后看不到共享目录或没有权限
另一个典型问题是:mount成功了,但ls显示目录为空,或者提示Permission denied。
如果ls为空,先确认服务端共享路径是否正确。比如showmount -e显示的是/data/repo,客户端挂载的也是/data/repo,但仓库根目录下并没有直接看到Packages,而是看到base、updates等子目录,这是正常的。真正放rpm包的路径是/mnt/dnf-repo/base/Packages,不要搞混。
如果是Permission denied,则要检查三个方面:
- exports中的权限选项是否为
ro,而客户端尝试写入(比如dnf install想在缓存目录里写文件,但那是本地缓存,不会影响。真正出现问题的是如果挂载的是rw,但客户端root被root_squash映射成nobody)。 - 服务端共享目录的文件系统权限是否允许其他用户读取。检查
ls -ld /data/repo,确保目录至少有r-x权限。 no_root_squash是否在exports中声明。如果没声明,客户端root在服务端被当作nobody,而/data/repo的属主又是root且权限不是755,就会导致读取失败。
我遇到过一种特殊的情况:服务端的共享目录在/root/repo,而不是/data/repo。/root目录的权限默认是700,客户端挂载后即使是no_root_squash也读不了。解决方案很简单,把仓库目录放到/data或/srv这类对普通用户可访问的路径下。这也是我在开头强烈建议把仓库目录规划到独立数据磁盘的原因之一。
6.3 NFS挂载后卡死或进程无响应
客户端正在通过NFS目录执行dnf install时,如果服务端重启、网络抖动或者NFS服务异常,客户端可能长时间无响应,最后报错:
code复制nfs: server 192.168.1.10 not responding, timed out
这个错误本质上是因为NFS客户端向服务端发送的RPC请求一直得不到响应,导致客户端挂载点上的一切IO操作都被阻塞。遇到这种情况,不要急着杀进程,先排查网络和服务端状态:
- 服务端是否还在正常运行?
systemctl status nfs-server。 - 网络是否正常?
ping 192.168.1.10。 - 服务端负载是否过高,导致NFS请求处理缓慢?
top、iostat看一下。
如果服务端恢复正常,客户端一般会在几十秒内自动恢复。如果长时间不恢复,可以手动强制执行一次NFS回收操作,或者干脆卸载重挂。但注意,卸载卡死的NFS挂载点可能也需要强制操作:
bash复制umount -f /mnt/dnf-repo
mount -t nfs 192.168.1.10:/data/repo /mnt/dnf-repo
为了避免这种问题影响生产环境,可以在mount选项中增加超时参数。比如修改/etc/fstab中的挂载选项:
code复制192.168.1.10:/data/repo /mnt/dnf-repo nfs defaults,_netdev,timeo=50,retrans=3 0 0
timeo=50表示超时时间为50个tenth seconds(即5秒),retrans=3表示重传3次。默认值比较保守,网络不稳定的环境下可以把timeo调小一些,让客户端更快感知到NFS服务端异常并响应。
6.4 仓库签名问题与gpgcheck配置
自建仓库如果不做软件包签名,客户端配置时必须设置gpgcheck=0。但实际环境中,很多团队会直接沿用公网源的repo文件模板,gpgcheck=1忘记改掉,导致安装时一直报签名错误:
code复制Public key for xxx.rpm is not installed
这种情况下有两种解决方案。第一种,把gpgcheck改为0。第二种,如果确实想保留签名校验,就需要在服务端引入GPG签名流程。简单说:在构建仓库时,用rpm --addsign对包签名,然后在客户端导入公钥,并在repo配置中通过gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-local指定公钥路径。
对内网自建仓库来说,我的建议是直接gpgcheck=0。因为内网源本身可信,再加上包来源都是规范同步的rpm包,签名校验的意义有限,反而增加了配置复杂度。但如果你的环境有安全合规要求,签名校验还是要做的。
6.5 仓库元数据与rpm包不一致问题
如果你通过rsync或者其他方式向仓库目录同步rpm包,但忘了执行createrepo_c --update,客户端在dnf install时会出现元数据错误,比如:
code复制Error: Failed to synchronize cache for repo 'local-base'
或者安装时提示某个包不存在(实际上rpm包文件已经在目录里)。这时候只要在服务端重新执行一次createrepo update,然后客户端dnf clean all即可。
这里有个小技巧:在服务端写一个简单的定时任务,定期检查Packages目录的文件变动并自动更新元数据。比如每天凌晨执行一次:
bash复制0 2 * * * /usr/bin/createrepo_c --update /data/repo/base >> /var/log/createrepo-base.log 2>&1
这样即使忘记手动更新,元数据最多和实际rpm包相差一天,问题不会累积。
6.6 showmount -e有输出但客户端mount被拒绝
还有一种情况:客户端执行showmount -e 192.168.1.10能看到共享列表,但mount时提示类似:
code复制mount.nfs: access denied for 192.168.1.11
这通常是因为exports文件中的客户端匹配规则不正确。比如我只写了192.168.1.0/24(ro,sync,...),但客户端IP是192.168.2.11,自然被拒绝。或者exports文件写错了语法,比如客户端网段和权限选项之间缺少空格。
修改exports后记得执行exportfs -ra,然后再次验证:
bash复制showmount -e localhost
在服务端看NFS的日志:
bash复制journalctl -u nfs-server -n 30
日志里通常会有类似“refused mount request from 192.168.1.11”这样的记录,一目了然。
6.7 小规模环境下的性能观察
最后说一下实际使用中的性能感受。我在十几台客户端同时执行dnf install的场景下测过,NFS+DNF仓库的表现非常稳定。一次并发的dnf makecache大约在10秒内完成,安装一个约50MB的软件包(含依赖)大约需要15到20秒,和本地安装速度差别不大。
如果客户端数量上升到几十上百台,NFS单点可能会成为瓶颈。这时候建议把NFS和HTTP仓库混搭:NFS负责小规模内部机器,HTTP(比如nginx)负责大规模分发。或者干脆用dnf自带的mirrorlist机制,把多个NFS服务端的仓库路径做成镜像列表,dnf会自动选择可用的源。不过这些都是后话,针对本文讨论的中小型内网环境,一套NFS共享的DNF仓库已经完全够用。
搭建这套环境的过程中,最大的心得是:仓库的目录结构和命名规则一定要在第一天定好,否则后续一旦有多个仓库源或者多人维护,很容易乱套。其次就是排查顺序,遇到挂载问题先查rpcbind、再查防火墙、再看exports,不要一上来就改客户端配置,那样只会把问题搞得更复杂。最后再说一个小技巧:如果你经常需要往仓库里塞新的rpm包,建议在服务端保存一份所有rpm包的清单文件(用find /data/repo -name "*.rpm" > /data/repo/rpm-list.txt定期生成),这样既能快速对比包数量,也能在误删时及时发现。这些经验看起来琐碎,但真正维护过这套系统的人,都会明白它们在关键时刻有多省心。
