
搞运维的人迟早会遇到这样一个场景内网里的机器不能访问外网或者只有少数几台机器有外网权限项目上线前需要在一批服务器上安装同一批软件包而且版本必须完全一致。手动拷rpm一个个装装一个报一个依赖缺失眼泪都能给你装出来。把DNF仓库和NFS共享服务组合起来就是为了根治这类问题。这篇文章我用自己的实操记录把从设计到落地的完整过程拆给你看适合正在做内网源、离线交付或者多节点环境标准化的运维和交付人员参考。1. 动手之前的方案设计DNF仓库和NFS为什么会绑定在一起1.1 先看看现实中到底卡在哪里在内网环境里最常见的软件安装麻烦有三个。第一是没外网。机器装好了dnf install一执行就超时因为默认源指向公网但网络根本不通。于是大家回到石器时代用U盘拷rpm包手动rpm -ivh完全没考虑依赖关系。结果就是装一个vim缺lib装lib又缺另一个lib半小时装不上一个编辑器。第二是版本不一致。就算你把rpm包拷贝到每台机器上时间一长有人手动升了级有人没升环境差异性越来越大。应用到这些机器上之后行为不一致“在我机器上能跑在你这跑不了”的现象到处冒头。第三是重复同步。如果统一通过一台机器下载软件包再用rsync往各个节点推包是能到但每台机器都得维护一份完整的本地缓存。10台机器就是10份冗余占空间不说更新时还要逐个处理非常痛苦。1.2 DNF仓库负责“索引”NFS负责“分发”要理解这个组合先得看清楚各自扮演什么角色。DNF仓库的本质是软件包加上一份元数据索引repodata。dnf这类的包管理工具之所以强大关键在于依赖解析而依赖解析依赖的就是这份索引索引记录了每个rpm包的名字、版本、依赖关系和文件列表。但仓库本身不解决“多台机器如何同时访问”的问题。NFS共享服务的本质是把服务器上的一个目录通过网络挂载到客户端本地。挂载完成以后客户端看到的就是一个本地目录可以直接ls、cd、读文件不需要额外下载整个目录。所以一个很自然的方案就出来了**在一台服务器上维护好DNF仓库的数据通过NFS把这个目录共享出去。客户端挂载NFS之后再用file:///挂载点作为DNF仓库的baseurl。**这样客户端不用自己下载rpm包也能获得完整的依赖解析能力。服务器上数据一更新所有客户端立刻就能看到。1.3 为什么优先考虑NFS而不是HTTP很多人会问仓库共享用HTTP不也一样吗确实HTTP也是常规套路而且跨网段访问更友好。但在内网场景NFS有它不可替代的优势。NFS挂载后客户端能直接浏览目录这对运维排障很有帮助。仓库里到底有哪些包、某个rpm在某目录下是否存在直接ls就能确认而HTTP方式只能通过网页或客户端去猜测路径。另外在部署初期如果发现某个rpm缺了运维人员可以直接在服务器上往目录里丢包然后重新生成元数据客户端刷新缓存即刻生效不需要等待Web服务的路径映射和缓存刷新。还有一点NFS的权限体系比较直观。你通过exports规则控制某个网段是否能访问、是否可写客户端root默认会被压制为匿名用户安全模型比裸HTTP更可控。而HTTP一旦配上目录所有人只要有URL就能读。也有反过来的场景。如果客户端和服务器跨机房、跨运营商链路不稳定NFS对延迟和丢包比HTTP敏感这时候就该用HTTP仓库。我在下文排查部分会专门展开怎么切换。1.4 目录结构和容量要做多少预算我建议按照仓库的层次组织目录。以RPM系发行版为例你的仓库大概率包含基础包、额外包和更新包那就按类别建子目录/data/repo/dnf/ ├── base ├── extras └── updates为什么一定分目录因为createrepo只能对单个目录生成一套repodata不同类别的包如果混在一起更新时整个元数据要重新生成体积大、耗时长。分开之后基础仓库除非大版本升级基本不变extras和updates则按需同步每次只需要重新生成变化的那部分。容量上给个参考值一个基础仓库的二进制包通常几十GBupdates和extras加起来按基础仓库的1~2倍考虑都正常再加上repodata的增量空间准备200GB的磁盘比较稳妥。如果机器是多副本冗余或者需要保留历史版本这个值还得再乘倍数。2. 核心机制与部署前准备搞清楚原理再敲命令2.1 DNF仓库的元数据流转过程理解DNF仓库的元数据是排查一切诡异问题的前提。客户端执行dnf install时会先扫描/etc/yum.repos.d/下的所有repo文件找到第一个enabled1的仓库地址然后去下载repodata/repomd.xml。这个repomd.xml是整个元数据的“入口索引”里面有primary、filelists、other等元数据文件的文件名、校验和和时间戳。客户端校验这些文件后把它当作依赖解析的依据解析出需要下载的rpm包列表最后按需从baseurl拉包安装。这里最关键的细节是**客户端不是每次安装都重新拉取全部元数据而是优先用本地缓存的元数据通过repomd.xml里的时间戳判断是否过期。**如果你在服务器端更新了rpm包却没有重新生成repodata客户端无论怎么执行dnf install都看不到新包。反过来如果你重新生成了repodata但客户端本地缓存没有刷新情况也一样。所以仓库维护的动作链路是服务器更新rpm包 - 重新生成repodata - 客户端dnf clean all dnf makecache。三步缺一不可。2.2 NFS导出参数与权限模型root_squash是个大坑NFS本身不负责用户认证它信任的是IP网段。真正让新手头疼的是权限参数。/etc/exports里每行代表一个导出规则格式是“导出的本地目录 允许访问的网段 参数”。常碰到的参数有这些。rw/ro代表可读写或者只读。仓库共享场景下我建议导出为只读避免客户端误操作。维护时如果需要客户端写可以临时改成rw维护完再改回来。sync/async代表写入模式。sync保证数据写入磁盘后才返回安全但慢async允许先写入内存即返回性能好但容易丢数据。仓库这种低频写、高频读的场景sync就够了没必要冒险。root_squash和no_root_squash是权限坑。默认情况下NFS会开启root_squash意思是即使客户端以root身份访问NFS服务端也把身份降级成一个匿名用户通常叫nobody这样客户端root没法以root权限操作共享目录。这在普通共享场景是安全特性但仓库维护时你经常需要在服务器本机以root操作目录服务端没问题如果是客户端想临时执行createrepo --update之类的操作就麻烦了Permission denied没商量。我建议的场景是日常共享用ro,root_squash。如果因为流程原因必须在客户端执行写操作就单独开一个维护入口用rw,no_root_squash并且限定具体IP。no_subtree_check是性能选项取消子树检查减少内核开销大目录场景建议加上。2.3 环境准备、网络规划与防火墙端口本文的实操基于RPM体系的企业级Linux发行版做示例内核版本3.10以上基本没问题。涉及的核心工具主要有dnfRPM系发行版的标准包管理工具自带。createrepo用于生成repodata元数据的工具也可以用性能更好的createrepo_c。nfs-utils包含NFS服务端和客户端所需组件。rpcbindNFS依赖的RPC端口映射服务没有它NFS根本起不来。我用这个网络配置举例服务器IP是192.168.10.10客户端分别为192.168.10.20和192.168.10.21。仓库根目录统一放在/data/repo/dnf/。NFS服务本身依赖多个RPC端口。默认情况下这些端口不是固定的会在服务启动时随机分配这对防火墙很不友好。所以正式部署前建议先把NFS相关端口固定下来方便放行。各服务端口规划参考下面这张表服务组件用途默认端口rpcbindRPC端口映射必开111nfs-serverNFS主服务2049mountd处理挂载请求可固定为4001statd网络锁管理可固定为4002lockd文件锁管理可固定为4003固定端口的方法是在NFS服务的配置文件中指定具体数值不同的发行版位置略有差异但配置内容思路一致把MOUNTD_PORT、STATD_PORT、LOCKD_TCPPORT、LOCKD_UDPPORT等变量修改为你规划的端口号。修改之后必须重启NFS服务并重新导出。防火墙放行时除了111和2049还要把上面固定好的端口放行。放行范围不要用0.0.0.0/0只放客户端所在的内网网段。3. 完整实操记录从一台空服务器到客户端能正常装包3.1 在服务器端创建仓库目录并填充rpm包登录服务器先检查工具是否就绪。如果没有createrepo和nfs-utils直接安装dnf install -y createrepo nfs-utils创建目录结构mkdir -p /data/repo/dnf/{base,extras,updates}如果你有一台能访问外网的机器可以使用dnf reposync一次性把某个远端仓库的数据镜像到本地dnf reposync --repoidbase --download-metadata --destdir/data/repo/dnf/base这里的--repoid指定仓库ID--download-metadata会顺带把远端元数据拉下来--destdir是本地存放目录。执行前先用dnf repolist确认远端仓库ID对应的名字。如果没有外网访问条件那就从安装介质或者另一台机器拷贝rpm包过来。注意要拷贝完整的rpm二进制包不要拷半截文件否则后续生成元数据时会出现校验失败或解析出空包列表的问题。3.2 用createrepo生成与更新元数据包放好之后生成元数据是这个方案能不能落地的最关键一步createrepo -v /data/repo/dnf/base-v参数会输出详细过程方便你观察是否每个rpm包都成功解析。执行完成后/data/repo/dnf/base/repodata/目录会出现里面包含repomd.xml和几个以.xml.gz结尾的元数据文件。之后每次加入新rpm包不需要重新全量生成用--update参数增量更新即可createrepo --update /data/repo/dnf/base--update只检查目录中新增和变化的rpm包生成速度快很多。但要注意如果你的仓库是从dnf reposync命令直接带元数据镜像回来的那本身就已经包含了repodata目录可以直接使用不必重新生成。只有当你手动添加或修改了rpm包之后才需要跑一次createrepo --update。3.3 配置并启动NFS服务编辑/etc/exports文件把仓库目录导出给整个内网网段/data/repo/dnf 192.168.10.0/24(ro,sync,no_subtree_check,root_squash)日常只读加root_squash保护。如果你确定某台维护机需要写权限可以临时追加一行具体IP加rw的规则不要直接对全网段开写。启动服务并让导出生效systemctl enable --now rpcbind systemctl enable --now nfs-server exportfs -arv showmount -e 192.168.10.10exportfs -a让所有导出条目生效-r重新导出-v显示详细状态。showmount -e用来验证服务器端导出的目录是否已经可见。执行后如果列出了/data/repo/dnf说明NFS服务端已经准备好了。3.4 客户端挂载NFS并配置DNF源到客户端机器上同样先装nfs-utilsdnf install -y nfs-utils创建挂载点并测试挂载mkdir -p /mnt/repo mount -t nfs 192.168.10.10:/data/repo/dnf /mnt/repo挂载成功后可以实际操作感受一下ls /mnt/repo/base/能直接列出服务器上的rpm包说明NFS链路已经通了。接下来写入/etc/fstab实现开机自动挂载192.168.10.10:/data/repo/dnf /mnt/repo nfs defaults,ro,_netdev 0 0_netdev这个参数很关键它告诉系统在网络上这一层就绪之后再挂载这个NFS目录否则开机时网络配置还没起来挂载就会失败。如果你用了网络管理服务来管理网卡建议配合remote-fs.target确保网络真正可用后再挂载。然后创建DNF源配置文件/etc/yum.repos.d/local.repo[local-base] nameInternal Base Repository baseurlfile:///mnt/repo/base enabled1 gpgcheck0这里要说明一下NFS挂载后/mnt/repo就是一个普通本地目录路径所以baseurl直接用file://接本机路径即可不需要专门的“NFS协议地址”。多套仓库之间不要混用一个[仓库ID]对应一个独立目录。3.5 验证安装与缓存刷新机制在客户端执行dnf clean all dnf makecache dnf install -y tree rpm -q treednf clean all清掉本地旧缓存dnf makecache重新拉取服务器端元数据建立缓存。如果这两步都能成功执行并且rpm -q tree能看到已安装版本说明客户端已经从NFS共享的仓库中完成了安装整套链路跑通了。换一台新客户端重复同样的操作如果也能正常安装并看到完全一致的软件包版本说明多节点统一交付的目标已经达成。建议此时用另一台客户端对比一下rpm -q tree的输出结果版本号一字不差才算成功。3.6 首轮makecache容易忽略的一个小细节第一次在客户端执行dnf makecache时会发现一个现象明明只配置了local-base一个源但输出里可能还出现系统自带的仓库或者提示某些仓库元数据失效。这是因为官方默认配置文件里的repo文件仍然存在只是状态是enabled1的引擎会去公网拉取元数据然后卡住或超时。遇到这种情况不用慌也不要在local.repo里纠结。直接把其他仓库文件在配置里加一行enabled0或者把那些后缀为.repo的文件移出目录。简单粗暴但管用。这个动作要趁早做否则内网环境下每次执行dnf命令都卡在那十来秒超时重试上体验非常差。4. 生产环境排查手册这些坑我基本都踩过4.1 元数据过期导致的“明明有包却装不上”现象是最迷惑人的服务器/data/repo/dnf/base/目录里明明有某个rpm包但客户端执行dnf install时却提示没有这个包。按这个顺序排查服务器上检查rpm包是否完整放入目录。服务器上执行ls /data/repo/dnf/base/repodata/repomd.xml确认repodata存在。检查repomd.xml的时间戳。如果你放弃一个包很久才想到要同步很可能一直没跑createrepo --update。客户端执行dnf clean all dnf makecache强制刷新缓存。绝大多数情况是第3步没做。记住一句话目录里放了包只是第一步repodata里不索引等于白放。4.2 NFS权限和文件属主显示异常客户端挂载后ls -l看到的文件属主全是nobody或者以root执行操作时报Permission denied这就是root_squash在生效。默认root_squash模式下任何客户端root操作都会被降级为匿名用户普通读取没影响写操作直接就拒了。如果你的维护流程确实需要在客户端对仓库目录做写操作只能临时在exports里对这个客户端IP开rw,no_root_squash然后执行exportfs -arv客户端上再重新挂载一次注意要umount后再mount让新的导出参数生效。还有一类情况客户端和服务器的账号体系不同uid不一致。NFS权限判断完全看数字uid不看用户名。服务器上uid1000的用户导出的文件客户端同一uid可能是另一个用户ls -l显示的用户名自然就不对。这通常只影响显示和访问控制不影响读取。如果要在NFS上做写入控制统一账号uid才是正解。4.3 挂载超时和端口不通症状是客户端执行mount -t nfs时没有任何反应最后超时或showmount -e是好的但挂载一直失败。这大概率是防火墙拦了mountd服务。NFS的常规端口111和2049被大多数人记住了但mountd、statd这些辅助服务的端口默认是随机分配的防火墙根本不知道要放行哪一个。解决方式仍然是固定端口再放行具体做法在前面环境准备部分已经说了。固定端口并重启NFS服务后用ss -lntp检查端口是否已经监听确认端口都在规划范围内然后再去客户端重试挂载。4.4 NFS vs HTTP什么情况下必须切换仓库形态NFS好用但并非万能。如果你遇到以下任一情况建议果断切HTTP仓库客户端与服务器跨网段跳数多NFS挂载后操作卡顿明显。有大量客户端同时刷新缓存NFS性能撑不住。客户端环境不可控有人挂着共享目录长时间不释放文件句柄NFS服务端可能需要强制重启才能恢复。切换成本其实很低。服务器端只需要额外装一个Web服务把/data/repo/dnf目录作为Web根目录或虚拟目录发布出去。客户端repo文件把baseurlfile:///mnt/repo/base改成baseurlhttp://192.168.10.10/dnf/base然后dnf clean all dnf makecache链路就切换过去了。我实际维护的几个项目里NFS仓库和HTTP仓库同时存在小规模内网机器直接用NFS省心跨地域的交付环境用HTTP稳定。4.5 多客户端并发场景下的性能策略几十台客户端同时执行dnf makecache的瞬间服务器磁盘I/O很容易扛不住。元数据文件不大但同一时间几十个请求同时读机械盘和低配云盘都会掉链子。几个缓解手段按优先级排序客户端把metadata_expire设大一点比如86400秒减少反复拉元数据的频率。客户端开启keepcache1rpm包缓存保留在本地重复安装不用反复读NFS。仓库服务器使用SSD或NVMe盘这是最直接的性能提升。如果客户端实在太多不要全部挤一个NFS出口在部分机器上做本地同步NFS只做更新源的“母版”。5. 稳定运行后的维护与扩展5.1 增量同步与自动化避免仓库越跑越乱仓库建好只是开始日常维护才是重头。从外网同步过来的仓库需要定期刷新我维护的某项目用的是reposync加createrepo --update组合。先同步dnf reposync --repoidbase --download-metadata --destdir/data/repo/dnf/base再更新元数据createrepo --update /data/repo/dnf/base这两条命令很适合放进定时任务。比如每天凌晨2点执行一次避免业务高峰期间同步影响使用。脚本里还要注意日志记录方便回溯。5.2 多副本冗余防止单点故障NFS仓库最怕的就是服务器磁盘坏了或者系统崩了一旦这块出问题所有依赖该源的客户端全部瘫痪。最省事的做法是用rsync把仓库根目录定时同步到另一台备用服务器备用机上同样启动NFS服务。客户端如果配了多个baseurl可以把备用服务器地址加在后面。dnf在第一个源不可用时会自动尝试下一个源这个特性用来做高可用非常顺手。同步时要注意repodata和rpm包必须一起同步不能只同步rpm包。否则备用服务器上目录有包但元数据不匹配照样没法用。5.3 仓库安全加固与签名验证内网环境虽然相对封闭但该有的安全意识不能丢。gpgcheck1不是摆设它可以防止rpm包被篡改后混入安装流程。服务器端生成GPG密钥对把公钥分发到所有客户端的RPM数据库里。客户端执行rpm --import /path/to/RPM-GPG-KEY然后repo文件设置gpgcheck1。这样每次安装包时都会校验签名任何未签名的包都会拒绝安装。配置签名后新增rpm包时不要忘了用签名工具对包签名否则客户端安装时会报校验失败。仓库目录本身也建议只对运维账户开放写权限不要为了图省事把所有用户都丢进同一组里。这套方案我前后维护过好几套环境踩过的坑基本都是权限、缓存和端口这三类问题。尤其是那些看起来不起眼的小选择比如本地缓存没刷新、root_squash权限没放开、mountd随机端口没固定往往就是故障主因。仓库更新后的习惯动作我个人强烈建议固定为服务器跑createrepo --update、客户端跑dnf clean all dnf makecache两步缺一不可。把这个动作固化成脚本能省掉后续大把排查时间。