☰
内网私有Yum源搭建:repodata原理、GPG签名与同步运维
2026/10/26 21:59:16 网站建设 项目流程

不少人第一次在内网装软件时都踩过同一个坑:机器出不了外网,yum install卡在镜像地址上转半天,最后抛出一句Cannot find a valid baseurl for repo。折腾一圈之后,把一台能出网的机器的/var/cache/yum整个拷过来,或者干脆用 U 盘一趟趟搬 rpm 包,装一个带依赖的服务能搬上十几趟。私有 yum 源仓库本质上就是把这个过程一次性解决掉——找一台内网机器,把 rpm 包和 repodata 元数据按规范摆好,用 HTTP 暴露出去,所有内网机器改一行baseurl就能像用公网镜像一样装包。这篇东西写给需要在内网、隔离网或者离线环境里维护几十到几百台机器的人,从 yum 找包的底层链路讲起,把光盘源、自建源、上游镜像三条路线拆开对比,再给一套可以照着敲的完整搭建过程、签名方案和排错清单。哪怕之前没碰过 createrepo,跟着走也能把源跑起来。

1. 出网这件事在内网有多奢侈:私有源到底在解决什么问题

内网环境和公网环境的差别,不是"能不能下载"这么简单。公网机器上yum install nginx一条命令搞定,是因为身后有 mirrorlist 做负载均衡、有 CDN 做就近加速、有上游团队每天同步元数据。内网把这层全部剥掉了,只剩下"我手上有什么包,就只能装什么包"。私有 yum 源要做的,就是把这一层重新补回来,而且补得更可控——包从哪来、哪个版本、谁改过,全在自己手里。

1.1 手工拷贝 rpm 的依赖地狱

新手最容易走的路线是:报什么错就装什么包。yum install A报缺 B,装上 B 又报缺 C 和 D,装上 C 报冲突,装上 D 发现版本不对。这条路走不通的根本原因在于 rpm 的依赖是树状的,而且带版本约束区间。一个看起来简单的包,rpm -qpR看依赖列表可能只有五行,但每一行背后又是一棵树,手工递归下去大概率在第三层就断掉。

更麻烦的是版本约束。比如某个包要求libssl.so.10()(64bit),你手上只有.so.3的版本,硬装上去要么--nodeps骗过检查、运行时报段错误,要么直接卡死。--nodeps和--force这两个参数在应急时确实有用,但把它当成常规手段用,等于把包管理器的价值全部丢掉,最后得到的是一台"看着装上了但不知道哪里会炸"的机器。

私有源的价值就在这里:它让 yum 的依赖解析器重新工作。所有包放在同一个仓库里,元数据把Provides/Requires/Obsoletes全部登记好,yum 自己做回溯求解,该装几个装几个,版本冲突在解析阶段就暴露出来,而不是在运行阶段。

1.2 四种常见做法放在一起比一比

在动手之前,值得先把可选方案摆开看一遍。我见过团队在这上面反复返工,往往是因为一开始选了个不适合长期维护的做法。

做法适用场景主要问题
手工下载 rpm +rpm -ivh单台机器、缺一两个包依赖靠人工递归,无法应对复杂依赖
拷贝/var/cache/yum临时应急缓存是客户端视角的产物,缺元数据,不可复用
ISO 挂载做本地源隔离网、首次装机只有发布时的版本,拿不到后续更新
createrepo + HTTP 私有源长期维护、多机共用需要一次性投入搭建和后续同步维护

前三种属于"能跑就行"的思路,适合救火。第四种是真正意义上的私有仓库,它的成本集中在搭建那半天,之后每加一台机器都是零边际成本。我在实际项目里的判断标准很简单:如果内网机器超过 5 台,或者需要长期存在,就直接上私有源,别在手工搬包上浪费时间。

还有一个容易被忽略的点:私有源不只是给yum install用的。很多内部工具链会读取yum repolist的结果判断环境是否合规,一些自动化部署脚本会依赖dnf repoquery做包版本比对。这些场景下,一个只有 rpm 文件、没有 repodata 的目录是没法用的,必须走标准仓库结构。

2. repodata 里到底装了什么:yum 找包的完整链路

搭建私有源的过程中,绝大多数报错都能追溯到对 repodata 的理解不到位。很多人把仓库当成"一个放 rpm 的文件夹",结果更新了包、客户端却装到旧版本,或者删了包、客户端还在报找不到依赖。真正的仓库是"rpm 包 + 描述这些包的元数据"两部分,缺一不可,而且元数据有它自己的版本和校验规则。

2.1 repomd.xml 与它指向的四个兄弟文件

进入任意一个仓库目录,会看到一个repodata/子目录,里面那堆文件名带一串哈希的 XML 就是元数据。核心是repomd.xml,它是入口,本身很小,作用相当于一本书的目录页:记录仓库里有哪些元数据文件、各自的校验和、时间戳。

repomd.xml指向的主要文件及其职责如下:

文件内容作用
*-primary.xml.gz包名、版本、依赖、文件列表摘要yum 解析依赖时读的主要数据
*-filelists.xml.gz包内完整文件路径支持yum provides /usr/bin/xxx
*-other.xml.gzchangelog 等杂项查询包变更记录
*-comps.xml包组(group)定义支持yum grouplist/yum groupinstall

客户端执行yum install时,第一步就是拉repodata/repomd.xml,比对本地缓存的校验和,不一致才去下载 primary 等文件。所以repomd.xml时间戳和内容的一致性,是整个仓库的命门。手工往Packages/目录里扔一个 rpm 而不重新生成元数据,客户端是看不见这个包的——这是最高频的误解,没有之一。

还有一个细节:repomd.xml里记录的文件名带哈希,每次createrepo重新生成时,哈希会变,旧文件默认会被清理掉。如果客户端正好在你更新元数据的瞬间去拉旧文件名,就会 404。createrepo_c提供了--retain-old-md N参数,保留 N 份历史元数据,让正在同步的客户端不至于踩空。生产环境上这个参数值得加上。

2.2 baseurl 拼接规则与 404 的由来

客户端 repo 文件里的baseurl必须指向"包含 repodata 的那一级目录",而不是仓库的父目录。这个规则听起来简单,但配错的概率极高。举个具体例子,仓库实际路径是/data/repo/centos/7/os/x86_64/repodata/,那么baseurl应该写http://10.0.0.10/centos/7/os/x86_64/,末尾带不带斜杠都行(yum 会做规整),但多一级、少一级都会 404。

排查这类问题时有个很快的办法:直接在客户端用 curl 打一下 repomd.xml。

curl -sI http://10.0.0.10/centos/7/os/x86_64/repodata/repomd.xml

返回200说明路径通、服务正常;返回404说明路径错或者文件不存在;返回403大概率是权限或 SELinux;连接超时则是网络或防火墙。这一步能在三十秒内把问题范围缩小一半,比反复yum clean all高效得多。

顺便说一句,baseurl支持多个地址,用空格分隔,yum 会依次尝试。这个特性在内网做多台源服务器冗余时很好用,但要注意:如果第一个地址返回 404 而不是连接失败,yum 不一定会自动切到第二个,所以冗余源的可用性依赖健康检查,不能光靠 yum 兜底。

2.3 客户端缓存与 metadata_expire 的作用

yum 不是每次装包都去服务器拉元数据,它把结果缓存在/var/cache/yum/(dnf 是/var/cache/dnf/)里,按metadata_expire配置的周期复用。默认值通常是 6 小时左右。这个机制带来的直接后果是:你在服务端更新了仓库,客户端可能还在用几小时前的旧缓存,看到的现象是"包明明放上去了,yum list里就是没有"。

解决办法是主动清缓存:

yum clean all yum makecache

clean all会把所有缓存清掉,makecache重新拉元数据并生成缓存。这两条是更新仓库后的标准动作,养成习惯能省掉大量"为什么没生效"的困惑。

反过来,在调试阶段也可以临时绕过缓存,用--cacheonly强制只用本地缓存,或者改metadata_expire=0让 yum 每次都去校验。后者只建议在排错时用,日常这么配会给源服务器带来不必要的压力。

3. 三条路线怎么选:光盘源、自建源、上游镜像

知道了结构,接下来要决定包从哪来。这是策略问题,不是技术问题,选错了后面一直在返工。我按使用频次和复杂度排列三条路线,多数内网场景是路线一打底、路线二补充、路线三按需。

3.1 路线一:ISO 挂载做离线源

这是成本最低的起点。把系统安装 ISO 上传到服务器,挂载到某个目录,就是一个完整的 BaseOS/AppStream 仓库——因为 ISO 里本来就带了Packages和repodata。不需要createrepo,挂载即用。

mkdir -p /data/repo/iso/centos7 mount -o loop /data/iso/CentOS-7-x86_64-DVD.iso /data/repo/iso/centos7

挂载点直接作为baseurl的根。为了让挂载在重启后依然有效,写进/etc/fstab,加上loop和ro选项。这个方案的短板很明显:它冻结在 ISO 发布那一刻的包版本,后续的安全补丁、软件更新都拿不到。所以它适合"首次装机"和"隔离网最小可用",作为长期唯一来源是不够的。

早期用 CentOS 7 的时候,官方是三张盘(DVD1/DVD2/Everything),把三张盘的Packages合并到同一个目录之后再createrepo,才能得到完整的包集合。合并的时候注意同名不同版本的包会共存,这是正常的,元数据会都登记上,yum 装的时候选最新版。

3.2 路线二:createrepo 自建自研包仓库

内部自研的 rpm、二次打包的中间件、从别处收集的零散包,都走这条路。流程就是:建目录、放 rpm、跑createrepo。这里的关键是"每次包有变化都要重新生成元数据",把这一步固化成脚本,不要依赖人的记忆。

自建源还有一个隐藏优势:可控的包版本。公网源更新不受自己控制,某天上游把某个版本撤了,你的自动化脚本就断了。自建源里放什么版本你说了算,稳定性更好。

3.3 路线三:reposync 同步上游仓库

需要完整镜像某个上游仓库时用这条路。reposync会根据仓库配置把包拉到本地,但这里有个版本差异必须说清楚,否则容易踩坑。

环境命令来源是否下载元数据同步后是否要 createrepo
CentOS 7 / 老版本 yumyum-utils中的reposync默认不下载,需要-m拉 comps必须
CentOS 8+ / Rocky / Almadnf-plugins-core中的dnf reposync加--download-metadata可直取可以不用

老版reposync只拉 rpm,元数据全靠本地createrepo重建,重建过程要计算每个包的校验和,包多了会很慢。新版dnf reposync --download-metadata直接把上游的 repodata 拿下来,速度快很多,但要注意上游元数据的签名也得一起处理,不然客户端repo_gpgcheck=1时会校验失败。

3.4 三条路线的取舍表

维度ISO 挂载createrepo 自建reposync 镜像
前期投入10 分钟半天半天到一天
磁盘占用单发行版约 4-10G按实际包量单发行版全量 20G 起
更新能力无完全可控跟随上游
维护成本极低中较高,需定时同步
适合场景隔离网首次部署自研包、精选包需要完整上游仓库

实际组合方式我一般这么搭:ISO 源做 BaseOS 的底座,自建源放内部包和精选的第三方包,两者用同一个 HTTP 服务暴露,客户端配两个 repo 段。这样既有完整的基础包,又能放自己的东西,还不用扛全量镜像的磁盘和带宽成本。

4. 从空目录到可用的源:一台内网机器的完整搭建过程

下面这部分是可以直接照着敲的。我用 Rocky Linux 8/9 这一代做示例,CentOS 7 的差异会在对应位置标注。整体思路是:规划目录、装工具、生成元数据、用 Web 服务器暴露、客户端验收。

4.1 磁盘规划与容量估算

先算容量,别搭到一半发现盘满了。经验值如下:单个发行版的主仓库(BaseOS + AppStream)全量大约 20-40G;加上 EPEL 再添 15-25G;如果同时维护两个主版本,直接按 100G 准备。自建的小仓库通常几百 MB 到几 GB。另外createrepo过程中会临时占用额外空间生成元数据,留 20% 余量比较稳妥。

目录结构建议按"发行版/版本/仓库名/架构"分层,这个结构和上游镜像站保持一致,以后要加新版本就是复制一层目录的事:

/data/repo/ ├── el8/ │ ├── baseos/x86_64/ │ └── appstream/x86_64/ ├── el9/ │ └── ... ├── epel/8/x86_64/ └── internal/ └── tools/x86_64/

用df -h先确认挂载点,把/data单独挂一个大盘。别把仓库放在根分区上,日志或者缓存涨起来容易把系统盘撑爆。

4.2 装 createrepo_c 与初始化仓库

dnf install -y createrepo_c httpd # CentOS 7 用:yum install -y createrepo httpd

这里优先选createrepo_c而不是老的createrepo。它是 C 实现,同样的包量下生成速度快好几倍,而且支持并行和增量。两者命令行参数基本兼容,切换成本几乎为零。

初始化一个仓库:

mkdir -p /data/repo/internal/tools/x86_64 cp /path/to/*.rpm /data/repo/internal/tools/x86_64/ createrepo_c /data/repo/internal/tools/x86_64/

执行完会看到目录下多了repodata/。用ls repodata/确认里面有repomd.xml和几个.xml.gz,基本就成功了。如果包里有依赖其他仓库的,createrepo不会报错,它只负责登记,解析留给客户端。

一个实用技巧:加--workers 4参数开多线程,包多的仓库能明显提速;加--pretty让生成的 XML 带缩进,人工排查时好读一些,代价是文件稍大。

4.3 包有增删时元数据怎么更新

这是最容易出问题的一步。有两种写法,效果和适用场景不同:

# 方式一:全量重建,最稳但慢 createrepo_c --pretty /data/repo/internal/tools/x86_64/ # 方式二:增量更新,只算新增或变化的包 createrepo_c --update --retain-old-md 3 /data/repo/internal/tools/x86_64/

--update的判断依据是文件大小和 mtime,没变的包直接复用旧元数据,速度提升很明显。但它有个前提:旧元数据必须存在且可读。如果repodata/被误删了,加--update会退化成全量重建,不会报错,这点要知道。

--retain-old-md 3保留三份历史元数据,作用前面提过,是为了让正在同步的客户端不踩空。代价是repodata/目录会变大一点,可以接受。

删包的处理要特别注意:把 rpm 文件从目录里删掉之后,必须重新跑一次createrepo(带--update也行),元数据才会同步移除。如果只删文件不更新元数据,客户端会看到这个包还在列表里,下载时 404,报错信息是Package X is not available,很容易误导人去查网络。

4.4 Web 服务器托管与 SELinux 标签

用 nginx 或 httpd 都行,我一般用 nginx,配置简单、并发好。核心配置就几行:

server { listen 80; server_name repo.internal; root /data/repo; autoindex on; autoindex_exact_size off; charset utf-8; location / { try_files $uri $uri/ =404; } }

autoindex on只是为了浏览器里能浏览目录,方便人工核对,不是必须的;但如果仓库里有敏感内容,建议关掉,改用访问控制。

如果用的是 httpd,注意别把Options Indexes和FollowSymLinks的开关搞混,同时确认httpd.conf里没有对.gz文件做奇怪的 MIME 限制。yum 拉的是二进制流,MIME 类型不影响,但如果中间挂了反向代理做了内容改写,就可能出问题。

重头戏是 SELinux。把仓库放在/data而不是/var/www下,默认标签是default_t,nginx 读不到,表现是 403 而不是 404,日志里会有Permission denied。正确的处理方式不是setenforce 0,而是打标签:

semanage fcontext -a -t httpd_sys_content_t "/data/repo(/.*)?" restorecon -Rv /data/repo

semanage需要policycoreutils-python-utils包。打完标签再用ls -Z确认一下,看到httpd_sys_content_t就对了。这一步我在不同项目里至少踩过三次,每次都是先怀疑权限、再怀疑路径,最后才发现是 SELinux。

别忘了防火墙:

firewall-cmd --permanent --add-service=http firewall-cmd --reload

4.5 客户端 repo 文件写法与验收

服务端就绪后,在客户端写 repo 文件:

[internal-tools] name=Internal Tools Repository baseurl=http://10.0.0.10/internal/tools/x86_64/ enabled=1 gpgcheck=0 repo_gpgcheck=0 metadata_expire=3600

先关掉签名校验把链路跑通,签名的事放到下一节。写完执行:

yum clean all yum repolist yum list available | head

yum repolist能看到仓库名和包数量,说明链路整个通了。如果包数量显示 0 但没报错,多半是baseurl指到了父目录,或者元数据没生成。如果报Cannot retrieve metalink,说明这个仓库配置里还留着mirrorlist没注释掉,把它注释掉或者删掉即可。

最后做一个真实的安装验证,随便挑仓库里一个包yum install -y <包名>,能装上就彻底放心了。

5. 把源管严一点:签名校验与访问控制

内部源一旦被接受,就成了所有机器的信任基础。有人往里面放一个改过的包,所有机器在下次安装时都会中招。签名不是形式主义,它是这条链路上唯一能防止"包被替换"的机制。

5.1 为什么 gpgcheck=0 是个坏习惯

很多人图省事,仓库里所有配置都写gpgcheck=0,理由是"反正内网,没人攻击"。这个推理有问题。内网的风险主要不来自外部攻击,而来自内部误操作和中间环节:某台机器的/data目录权限配松了,某个 rsync 脚本同步错了版本,某个运维从不明来源拖了 rpm 塞进去。签名能在这些场景下给出明确信号——包被改过,装之前就拦住了。

更实际的一点是,很多合规检查项里gpgcheck=0是硬伤,后面做安全审计还得全部重来。不如一开始就配好。

这里有个非常关键、但常被混淆的区别:

参数校验对象缺失时的报错
gpgcheckrpm 包本身Package X is not signed
repo_gpgcheckrepodata 元数据(repomd.xml)repomd.xml signature could not be verified

两个参数独立生效。很多教程只讲gpgcheck,结果自建包里自己打的 rpm 没签名,一装就报错,然后就把它关了——其实是该用repo_gpgcheck保护元数据,包签名另说。

5.2 从生成密钥到给 repomd.xml 签名的完整流程

在源服务器上生成密钥对。注意用 root 或专用账号,~/.gnupg权限设成 700。

gpg --full-generate-key

交互里选 RSA、密钥长度 4096、有效期按需(长期使用的仓库建议不设过期或设长一点)。生成完记下密钥 ID:

gpg --list-keys --keyid-format LONG

然后给元数据签名。createrepo_c支持直接带签名参数,一步到位:

createrepo_c --local-user <KEYID> /data/repo/internal/tools/x86_64/

这会在repodata/下生成repomd.xml.asc。如果仓库是增量更新的,--update配合--local-user同样有效。另一种做法是手动签,好处是可以在签名前先检查元数据内容:

gpg --detach-sign --armor --local-user <KEYID> \ /data/repo/internal/tools/x86_64/repodata/repomd.xml

手动签之后要确保repomd.xml.asc和repomd.xml是同一批生成的,否则客户端校验会失败。这也是为什么增量更新后一定要重新签——元数据变了,旧签名立刻失效。

导出公钥给客户端用:

gpg --armor --export <KEYID> > /etc/pki/rpm-gpg/RPM-GPG-KEY-internal

文件名不要带空格,这是个小坑。带空格的文件名在 repo 配置的gpgkey=file:///...里会解析失败,报错还很含糊。

5.3 客户端导入公钥与验证签名生效

rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-internal rpm -qa gpg-pubkey*

能看到导入的密钥就对了。然后把 repo 配置改成:

[internal-tools] name=Internal Tools Repository baseurl=http://10.0.0.10/internal/tools/x86_64/ enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-internal

如果仓库里的 rpm 本身没有签名(自打包常见),gpgcheck=1会拦住安装。解决办法有两个:用rpm --addsign给包签名,或者把gpgcheck设为 0 但保留repo_gpgcheck=1。后者保护了元数据不被篡改,代价是包本身没校验,属于可接受的折中,但要清楚这是折中。

验证是否真的生效:随便改一下服务端repomd.xml的一个字节(测试完记得恢复),客户端执行yum makecache,应该报签名校验失败。能报出来,说明链路正确;不报错就说明哪里没生效,别急着上生产。

5.4 简单访问控制

内部源不需要暴露到公网,但内部也不是所有人都该有访问权。最轻量的做法是 nginx 的 IP 白名单:

location / { allow 10.0.0.0/8; deny all; try_files $uri $uri/ =404; }

如果需要认证,加一层 basic auth:

htpasswd -c /etc/nginx/.htpasswd repo_user
location / { auth_basic "repo"; auth_basic_user_file /etc/nginx/.htpasswd; }

客户端对应的baseurl要带用户名密码:http://repo_user:密码@10.0.0.10/...。但这样密码是明文存在 repo 文件里的,任何能读该文件的人都能拿到。更好的方式是只在受控的自动化环境里用,或者干脆用 IP 白名单,把认证交给网络层。

6. 几次真实报错的排查链路

排错这件事,知道结论没用,得知道怎么一步步缩小范围。下面这几类问题我在不同环境里反复遇到,把当时的排查顺序记下来,比记住报错本身更有价值。

6.1 repomd.xml 404 与 Cannot find a valid baseurl

看到Cannot find a valid baseurl for repo,第一反应不应该是改配置,而是先确认"服务端到底有没有这个文件"。排查顺序:

  1. 服务端ls /data/repo/.../repodata/repomd.xml,确认文件存在。
  2. 服务端本机curl -I http://127.0.0.1/...,确认 Web 服务能读到。
  3. 客户端curl -I http://10.0.0.10/...,确认网络和防火墙通。
  4. 客户端cat /etc/yum.repos.d/*.repo,确认路径拼接没多一级少一级。

绝大多数情况停在第 3 步或第 4 步。第 3 步失败最常见的原因是防火墙只开了 80 没开 443 而配置里写的是 https,或者反向代理把路径重写了。第 4 步失败最常见的是复制粘贴时多带了repodata/这一级。

还有一个隐蔽的情况:仓库目录本身没问题,但.repo文件里有多个仓库段,其中某一个的baseurl是坏的,yum repolist会对整个命令报错退非零,看起来像全挂了。这时候用yum repolist --disablerepo='*' --enablerepo=<目标仓库>单独验证,比一个个注释掉快得多。

6.2 Metadata file does not match checksum

这个报错的字面意思是元数据校验和对不上。常见成因有三个:

第一个是缓存半新半旧。服务端刚更新完元数据,客户端本地缓存里还留着旧的repomd.xml和新的 primary 文件,或者反过来。处理方式是yum clean all后重试。九成的情况这一步就解决了。

第二个是同步中断。用 reposync 拉到一半断了,Packages/和repodata/不一致。这种情况必须重新跑同步和createrepo,不能靠清缓存解决。

第三个是中间有代理或缓存服务器做了内容改写或截断。表现是同样的 URL 在客户端 curl 下来大小和服务端不一致。用md5sum两边对比一下立刻见分晓。

要注意的是这个报错有时会连带出现repomd.xml signature could not be verified,因为元数据变了签名自然也变了。如果开启了repo_gpgcheck,先解决校验和问题,再重新签名。

6.3 依赖冲突与模块化带来的怪现象

EL8 之后引入了模块化(modularity),AppStream 里的部分包被拆成多个流(stream),比如php:7.4和php:8.0。私有源如果把 AppStream 全量同步下来,客户端可能会遇到"包存在但装不上"或者"版本被锁定"的现象,报错涉及module关键字。

处理思路:

  • 用dnf module list看当前启用的流。
  • 需要某个版本时用dnf module enable php:7.4显式启用。
  • 私有源自建的仓库如果没有 modulemd 元数据,模块相关的功能会失效,这时候可以在 repo 配置里加module_hotfixes=1绕过模块过滤,让 yum 直接按包解析。

另一个高频现象是"同一个包在两个仓库里版本不同",比如内部源和 EPEL 都有某个库,yum 选了低版本的。用yum --showduplicates list <包名>能看到所有可用版本和来源仓库,配合--disablerepo/--enablerepo定位。真要长期解决,靠priority或exclude在 repo 配置里做约束,而不是每次安装时手动指定。

6.4 权限、SELinux 与缓存三类"看起来像网络问题"的故障

最后这一类最有迷惑性,因为报错文本里带 HTTP 状态码或者"cannot retrieve",很容易被当成网络问题。

现象真实原因快速确认方式
403 ForbiddenSELinux 标签不对,或文件权限不足ls -Z看标签,namei -l看目录链权限
空响应 / 连接被重置反向代理配置错误或超时直连后端端口测试
包列表为空但不报错元数据未生成或baseurl层级错服务端ls repodata/
装了旧版本客户端缓存yum clean all后重试

关于文件权限,有个容易忽略的点:nginx 的工作用户(通常是nginx或www-data)需要对整条路径有x权限,不只是最后一层。仓库放在/data/repo/...时,/data目录的权限也要检查。用namei -l /data/repo/internal/tools/x86_64/能一次性打印整条路径的权限,比一层层ls高效。

7. 让源活下来:定时同步、增量更新与容量治理

搭起来只是开始,真正考验人的是三个月后。仓库会越来越胖,同步任务会互相打架,某天早上一看磁盘满了。这部分讲怎么让它可持续。

7.1 按需同步而不是全量搬运

全量镜像看起来很爽,代价是磁盘和同步时间。我的经验是先按需来:只同步真正用得到的仓库和架构。reposync支持按仓库 ID 指定,也可以配合--arch过滤架构,把i686、aarch64这些不需要的排除掉,体积能砍掉不少。

另一个维度是--newest-only(老版 yum)或--latest-only(dnf),只保留每个包的最新版本。这会把历史版本全部丢弃,好处是体积小,坏处是没法回滚到旧版本。如果内网有"必须锁定某版本"的需求,就别开这个选项,改用下面的保留策略。

EPEL 这种第三方仓库建议单独同步到独立目录,别和主仓库混在一起。出了问题可以单独回撤,客户端也可以按需启用。

7.2 定时任务与并发锁

同步动作不能并发跑。两个reposync同时对同一个目录写入,结果就是元数据和包对不上,然后全员报校验和错误。用flock加个锁是最简单的方案:

#!/bin/bash set -euo pipefail LOCK=/var/lock/reposync.lock exec 9>"$LOCK" flock -n 9 || { echo "another sync is running"; exit 1; } REPO_DIR=/data/repo/el8 LOG=/var/log/reposync.log echo "=== start $(date) ===" >> "$LOG" dnf reposync --repoid=baseos --download-metadata -p "$REPO_DIR" >> "$LOG" 2>&1 dnf reposync --repoid=appstream --download-metadata -p "$REPO_DIR" >> "$LOG" 2>&1 # 老版 yum 环境需要手动重建元数据 # createrepo_c --update --retain-old-md 3 "$REPO_DIR/baseos" echo "=== done $(date) ===" >> "$LOG"

flock -n的-n是关键,拿不到锁就直接退出,不要阻塞等待。否则定时任务会堆积,越积越多。

放到cron里每天凌晨跑一次:

0 3 * * * /usr/local/bin/sync-repo.sh

同步窗口要避开业务高峰。如果仓库大,一次同步要跑一两个小时,建议放在凌晨三四点,并且同步完成后再触发一次元数据重建,确保原子性。更严谨的做法是同步到临时目录,完成后用rsync原子切换,避免客户端在同步过程中读到半成品。

7.3 老旧包的清理与版本保留策略

容量治理的核心是"保留几个版本"。我的经验值是每个包保留最近 3 个版本:足够覆盖"上游刚发新版出问题要回滚"的场景,又不会无限膨胀。实现方式有两种:一是在同步时用--latest-only直接不拉旧的;二是定期跑清理脚本删掉超期的 rpm,再重建元数据。

清理必须成对做:删 rpm 和重建元数据是一个原子动作。只删文件不重建,客户端会看到幽灵包;只重建不删,磁盘继续涨。写脚本时把两步放在同一个set -e块里,中间断了就重跑。

还有一点:清理前先看看有没有客户端正在用旧版本。grep一下访问日志里最近被下载的包名,或者直接看yum的history(各客户端上),确认没有依赖旧版本的关键机器。这一步很容易被跳过,然后某天某台老机器的自动化脚本突然挂了。

监控方面,至少盯两个指标:磁盘使用率和同步任务的返回码。前者防止撑爆,后者防止"同步一直失败但没人发现"。这两个加起来能覆盖 80% 的运维事故。我一般会在同步脚本最后加一行判断,失败就发个通知,几行代码的事,能省掉很多事后排查。

个人经验上,私有源这类基础设施最怕的不是搭不起来,而是搭起来之后没人管。包放上去就不动了,半年后要装个新版本发现源里没有,又回到手工搬包的老路。所以搭建的时候就把同步脚本、清理脚本、监控通知一次性配好,后面基本就是无感运行。另外一个小技巧:在仓库根目录放一个README或者last-sync.txt,同步脚本每次追加一行时间戳,出问题时直接curl一下就知道最后一次同步是什么时候,比翻日志快得多。这个习惯我保持了几年,每次排查都能省下几分钟。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询