1. 为什么离线RPM仓库不是“备选方案”,而是生产环境的刚性需求
在某高校实验室部署一套边缘计算节点集群时,我负责为23台物理服务器构建统一的软件交付体系。所有节点位于封闭网络中,既无外网出口,也禁止通过跳板机临时拉取包——这是典型的“空气隔离”环境。第一天,我信心满满地执行yum install nginx,结果卡在Downloading packages...超过40分钟,最后报错:Could not resolve host: mirrors.aliyun.com。那一刻我才意识到:在真实工业场景里,“能联网”从来不是默认前提,而“必须离线可用”才是基础设施设计的第一条铁律。
CentOS 7.x 的 EPEL(Extra Packages for Enterprise Linux)源,本质是 Red Hat 生态中一个极其关键的“补丁集”。它不提供内核或核心系统组件,但囊括了 Python 生态(如python36,pip,virtualenv)、监控工具(collectd,netdata)、开发辅助(htop,iftop,jq,yq)、数据库客户端(mysql-community-client,postgresql95)等数百个高频刚需包。这些包在官方 base 和 updates 源中根本不存在。一旦你依赖其中任意一个,又身处离线环境,整套运维链路就会瞬间断裂。
很多人误以为“下载几个 rpm 包解压就能用”,这是对 RPM 包管理机制的根本性误解。RPM 不是 ZIP,它有严格的依赖图谱(Dependency Graph)。比如nginx-1.16.1-1.el7.x86_64.rpm依赖openssl-libs >= 1:1.0.2k、pcre >= 8.32、zlib >= 1.2.3.4;而pcre又依赖glibc >= 2.17;glibc是系统基础库,但它的具体版本号在不同 CentOS 7.x 小版本中并不一致(7.4 vs 7.9 的 glibc 版本差了 3 个小版本)。直接拷贝一个 rpm 安装,大概率触发error: Failed dependencies,且错误信息只告诉你缺什么,不告诉你该去哪找哪个精确版本。
更隐蔽的陷阱在于元数据(Metadata)。YUM 客户端从不直接读取 rpm 文件,而是先解析repodata/目录下的primary.xml.gz(记录所有包的名称、版本、依赖、文件列表)、filelists.xml.gz(记录每个包安装后生成哪些文件)、other.xml.gz(变更日志与校验和)。没有这套结构化的元数据,yum install命令连“有哪些包可选”都无从得知,更别说自动解决依赖了。所谓“搭建离线仓库”,90% 的工作量其实是在构造并维护这一套元数据,而非搬运 rpm 文件本身。
所以,这个标题里的“下载 rpm 包”只是表象,“搭建离线仓库”才是实质目标。它要求我们完成三重能力构建:第一,精准捕获 EPEL 源中所有目标 rpm 及其完整依赖树;第二,将这些二进制包按标准目录结构组织;第三,生成符合 YUM 协议规范的元数据索引。这三步缺一不可,任何一步出错,离线仓库就形同虚设。接下来的内容,就是我踩过至少七次坑、重试四轮才沉淀下来的完整闭环方案。
2. 为什么不能用 wget 或 curl 手动下载?——EPEL 源的镜像结构与路径陷阱
第一次尝试,我打开 EPEL 官方镜像站(https://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/),想用wget -r -np -nH --cut-dirs=4把整个Packages/目录镜像下来。结果跑了 12 小时,磁盘爆满,只下载了不到 1/3 的包,还全是重复的符号链接和损坏的.rpm文件。问题出在 EPEL 镜像的底层设计逻辑上:它根本不是一个扁平的文件夹,而是一个高度分层、动态生成的 CDN 分发体系。
EPEL 7 的主路径是https://dl.fedoraproject.org/pub/epel/7/x86_64/,但这个 URL 下并没有一个叫Packages/的真实目录。你看到的Packages/是一个由 Web 服务器(通常是 Nginx)根据请求路径动态拼接的虚拟路径。真实文件存储在https://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/a/(以包名首字母分组)、/b/、/c/…… 直到/z/。每个子目录下,文件名格式为<name>-<version>-<release>.<arch>.rpm,例如ansible-2.9.27-1.el7.noarch.rpm存在Packages/a/下,而zsh-5.0.2-7.el7.x86_64.rpm存在Packages/z/下。这种设计极大提升了 CDN 缓存效率,却给爬虫制造了巨大障碍:你无法通过一次ls列出所有子目录,也无法预知某个包究竟在哪个字母目录下。
更致命的是,EPEL 镜像站启用了强防盗链(Referer Check)和 User-Agent 过滤。我用curl -I https://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/a/ansible-2.9.27-1.el7.noarch.rpm测试,返回403 Forbidden;换成wget --user-agent="Mozilla/5.0",依然 403;直到我模拟一个真实的浏览器请求头,才拿到 200 响应。这意味着,任何未经认证的批量下载脚本,在发起请求的瞬间就会被边缘节点拦截,返回空内容或 403 错误,而你可能要下载完才发现所有 rpm 都是 0 字节。
另一个常被忽略的细节是 EPEL 的“滚动发布”(Rolling Release)机制。EPEL 7 并非一个静态快照,而是持续更新的。今天下载的nginx-1.16.1,明天可能就被nginx-1.16.2替代,旧包会被移入archive/目录。如果你在 T+0 时间点下载了全部包,T+1 时运行createrepo生成元数据,T+2 时用户执行yum update,YUM 客户端会去比对远程repodata/中的最新primary.xml.gz,发现本地没有这个新版本,于是报错No package nginx available。离线仓库必须具备“时间锚定”能力,即锁定在某一特定日期的 EPEL 快照状态,否则它永远无法真正“离线”。
因此,手动下载的失败根源在于:它试图用文件系统思维去操作一个 HTTP 协议驱动的、带访问控制的、动态分片的 CDN 服务。正确的做法,是放弃“下载文件”的执念,转而使用专为 RPM 生态设计的工具链,它们内置了对 EPEL 镜像协议的深度理解。接下来要介绍的reposync,就是这个生态里最成熟、最可靠的“离线同步器”。
3. reposync:不是下载工具,而是离线仓库的“基因测序仪”
reposync是yum-utils包中的一个命令行工具,它的设计哲学与wget截然不同:它不关心 HTTP 协议细节,只与 YUM 的 repo 配置文件(.repo)对话。你可以把它理解成 YUM 客户端的“后台静默模式”——当yum install在前台为你解决依赖并安装软件时,reposync在后台把整个仓库的“基因序列”(即所有 rpm 包及其元数据)完整克隆一份。
它的核心优势在于“协议感知”。当你配置好一个指向 EPEL 的.repo文件后,reposync会:
- 先解析
repodata/repomd.xml:这是仓库的“DNA 索引文件”,里面明确列出了primary.xml.gz、filelists.xml.gz、other.xml.gz三个核心元数据文件的 URL、校验和(checksum)及最后修改时间(timestamp)。 - 再下载这三个元数据文件:
reposync会校验 checksum,确保下载内容未被篡改;同时比对 timestamp,判断本地缓存是否过期。 - 最后根据
primary.xml.gz中的<location>标签,逐个下载 rpm 包:此时它已经知道每个包的确切 URL(例如https://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/a/ansible-2.9.27-1.el7.noarch.rpm),并且会自动处理重定向、设置正确 User-Agent、携带 Referer 头,完美绕过 CDN 的反爬机制。
整个过程完全自动化,无需人工干预路径或猜测文件名。更重要的是,reposync支持--download-metadata参数,它会把远程仓库的repodata/目录原样下载下来,作为本地仓库的“初始元数据种子”。这解决了手动下载无法获取元数据的根本难题。
下面是我实际使用的完整流程。首先,确保系统已安装yum-utils:
sudo yum install -y yum-utils然后,创建一个专门用于同步的 repo 配置文件/etc/yum.repos.d/epel-offline.repo:
[epel-offline] name=Epel Offline Mirror baseurl=https://dl.fedoraproject.org/pub/epel/7/x86_64/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 # 关键:禁用 fastestmirror 插件,避免因网络延迟导致超时 skip_if_unavailable=True # 关键:指定一个稳定的、已知的 EPEL 快照日期(2023年10月1日) metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-7&arch=x86_64×tamp=1696118400这里timestamp=1696118400是 Unix 时间戳,对应 2023-10-01 00:00:00 UTC。EPEL 的 metalink 服务支持通过timestamp参数精确锁定历史快照,这是实现仓库“时间锚定”的唯一可靠方式。你可以在 Fedora Mirror Status 页面查询任意日期的有效 timestamp。
接着,创建一个空目录作为同步目标,并执行同步:
mkdir -p /opt/epel-offline # 同步所有包,并下载元数据 sudo reposync -r epel-offline -p /opt/epel-offline --download-metadata --downloadcomps # --downloadcomps 参数用于下载 groupinfo.xml,支持 yum groupinstall 功能-r epel-offline指定使用上面定义的 repo;-p /opt/epel-offline指定本地存储路径;--download-metadata是灵魂参数,它确保repodata/目录被完整拉取。
同步完成后,检查目录结构:
tree -L 2 /opt/epel-offline/ # 输出应为: # /opt/epel-offline/ # ├── epel-offline # │ ├── Packages # │ └── repodata # └── repodata # 这是 --download-metadata 创建的原始元数据副本注意,/opt/epel-offline/epel-offline/repodata/是reposync自动生成的、基于当前下载包重建的元数据;而/opt/epel-offline/repodata/是从远程直接下载的原始元数据。前者是未来供客户端使用的,后者是备份和验证用的。
提示:
reposync默认会下载所有包,包括debuginfo和source包,这会极大增加体积。如需精简,可在 repo 配置中添加includepkgs=nginx,python36,ansible,jq(用英文逗号分隔所需包名),或使用exclude=*.debuginfo,*-debuginfo*,*-src排除不需要的类型。
4. createrepo:从一堆 rpm 文件到可被 yum 识别的“活”仓库
reposync解决了“把东西搬回来”的问题,但搬回来的东西还是一堆散落的 rpm 文件,YUM 客户端依然不认识它。这时就需要createrepo出场,它的工作是给这堆文件“注入生命”,生成 YUM 能读懂的元数据。你可以把createrepo想象成一个“RPM 包的编译器”:输入是 rpm 文件集合,输出是repodata/目录下那几个关键的 XML 和 SQLite 文件。
createrepo的核心原理是深度解析每个 rpm 包的内部信息。它会调用rpm -qpi <package.rpm>命令,提取出包的Name、Version、Release、Architecture、Summary、Description、License、URL,以及最重要的Requires(依赖列表)和Provides(提供能力列表)。然后,它将所有这些信息汇总、索引、压缩,写入primary.xml.gz。filelists.xml.gz则通过rpm -qlp <package.rpm>获取每个包安装后会写入哪些文件路径。other.xml.gz记录changelog和file digest(SHA256 校验和)。
最关键的一步,是createrepo会为整个仓库生成一个唯一的repomd.xml文件。这个文件就像仓库的“身份证”,里面包含了所有元数据文件的路径、大小、校验和和最后修改时间。YUM 客户端在连接仓库时,第一步就是下载并校验这个repomd.xml,只有校验通过,才会继续下载primary.xml.gz等后续文件。如果repomd.xml里的校验和与实际文件不符,YUM 会直接报错failure: repodata/repomd.xml from epel-offline: [Errno 256] No more mirrors to try.,这是离线仓库最常见的“启动失败”错误。
执行createrepo的命令非常简单,但参数选择至关重要:
# 进入同步好的包目录 cd /opt/epel-offline/epel-offline # 生成元数据,-v 显示详细过程,-s sha256 使用 SHA256 校验(更安全),-d 启用 sqlite 数据库(加速 yum 操作) sudo createrepo -v -s sha256 -d . # 如果需要兼容老版本 yum(如 CentOS 7.2 之前的),可加 --database 参数 # sudo createrepo --database -v -s sha256 .-d参数会生成repodata/other.sqlite.bz2和repodata/filelists.sqlite.bz2,这些 SQLite 数据库文件能让yum search、yum list等命令响应速度提升 5-10 倍,强烈建议开启。
执行完成后,检查repodata/目录:
ls -lh repodata/ # 应看到类似: # -rw-r--r--. 1 root root 12K Oct 5 10:23 filelists.xml.gz # -rw-r--r--. 1 root root 15K Oct 5 10:23 other.xml.gz # -rw-r--r--. 1 root root 2.1M Oct 5 10:23 primary.xml.gz # -rw-r--r--. 1 root root 12K Oct 5 10:23 repomd.xml # -rw-r--r--. 1 root root 12K Oct 5 10:23 filelists.sqlite.bz2 # -rw-r--r--. 1 root root 15K Oct 5 10:23 other.sqlite.bz2 # -rw-r--r--. 1 root root 12K Oct 5 10:23 primary.sqlite.bz2此时,/opt/epel-offline/epel-offline/目录已经是一个功能完备的、可被 YUM 直接挂载的离线仓库了。但别急着分发,还有一个隐藏的“时间炸弹”需要排除。
5. 时间炸弹排查:repomd.xml 中的 timestamp 与客户端缓存的博弈
在实验室首次部署离线仓库后,A同学在一台 CentOS 7.6 的测试机上配置了本地 repo:
[epel-offline] name=Local EPEL Mirror baseurl=file:///opt/epel-offline/epel-offline/ enabled=1 gpgcheck=0执行yum clean all && yum makecache,一切顺利。但当他执行yum install ansible时,却报错:
Error: Package: ansible-2.9.27-1.el7.noarch (epel-offline) Requires: python36-jinja2 >= 2.10.1 You could try using --skip-broken to work around the problem You could try running: rpm -Va --nofiles --nodigestpython36-jinja2这个包明明就在/opt/epel-offline/epel-offline/Packages/p/目录下!为什么 YUM 找不到?问题就出在repomd.xml的timestamp字段上。
我用cat repodata/repomd.xml | grep timestamp查看,发现里面的时间戳是2023-10-01T00:00:00Z,而createrepo生成的primary.xml.gz里,<package>标签的timestamp属性却是2023-10-05T10:23:45Z(即createrepo执行的本地时间)。YUM 客户端在解析primary.xml.gz时,会将这个时间戳与本地缓存的repomd.xml中的时间戳进行比对。如果两者不一致,YUM 会认为元数据已过期,从而拒绝使用本地缓存,转而尝试从baseurl拉取远程元数据——但在离线环境下,这必然失败。
这个 bug 的根源在于createrepo的默认行为:它会用当前系统时间覆盖repomd.xml中原有的timestamp。而我们通过reposync下载的原始repomd.xml,其timestamp是 EPEL 官方设定的、代表该快照有效性的权威时间。createrepo的覆盖操作,无意中破坏了这个权威性。
解决方案有两个,我推荐第一个,因为它更彻底:
在
createrepo命令中强制指定--update和--revision参数:# 先删除旧的 repodata rm -rf repodata # 重新生成,--revision 指定一个固定字符串,替代时间戳 sudo createrepo -v -s sha256 -d --revision "epel-7-20231001" .--revision参数会将repomd.xml中的timestamp替换为一个固定的、可读的字符串(如epel-7-20231001),这样所有客户端看到的都是同一个值,彻底规避时间比对逻辑。手动编辑
repodata/repomd.xml:找到<revision>标签,将其内容改为一个固定值,例如<revision>epel-7-20231001</revision>,然后用sha256sum repodata/repomd.xml计算新的校验和,并替换repomd.xml中对应<checksum type="sha256">标签内的值。这个方法有效,但繁琐且易出错。
修复后,再次在客户端执行yum clean all && yum makecache,yum install ansible就能成功了。这个案例深刻说明:离线仓库的稳定性,不仅取决于包的完整性,更取决于元数据中每一个看似微小的字段的精确性。一个时间戳的错位,就能让整个仓库瘫痪。
6. 客户端配置与实战验证:从“能用”到“好用”的最后一公里
仓库建好了,客户端的配置就变得异常简单,但有几个极易被忽视的细节,决定了它是“能用”还是“好用”。
6.1 最小化安全配置:禁用 GPG 校验的合理场景
在epel-offline.repo中,我设置了gpgcheck=0。这不是为了偷懒,而是基于风险评估的理性选择。GPG 校验的目的是防止 rpm 包在传输过程中被篡改。但在离线环境中,包是从可信的、物理隔离的存储介质(如 USB 硬盘)拷贝而来,传输链路本身是安全的。而启用gpgcheck=1会带来两个现实问题:第一,你需要将 EPEL 的公钥(RPM-GPG-KEY-EPEL-7)手动分发到每一台客户端,增加了部署复杂度;第二,createrepo生成的元数据中,primary.xml.gz里的<signature>字段是空的,因为createrepo不会为本地仓库签名。这会导致yum makecache时反复报错Importing GPG key 0x352C64E5,最终失败。
因此,在严格管控的离线内网中,gpgcheck=0是一个被广泛接受的最佳实践。当然,如果你的合规审计要求必须启用 GPG,那么你需要额外执行createrepo --sign --gpg-key <your-key-id>,但这会显著增加构建流程的复杂度,本文暂不展开。
6.2 高效的客户端缓存策略
yum makecache命令会将primary.xml.gz等元数据解压并缓存在/var/cache/yum/下。对于一个包含 15000+ 个包的 EPEL 仓库,primary.xml.gz解压后超过 200MB,makecache过程可能耗时 3-5 分钟。为了避免每次yum install都触发全量缓存重建,可以配置metadata_expire:
[epel-offline] name=Local EPEL Mirror baseurl=file:///opt/epel-offline/epel-offline/ enabled=1 gpgcheck=0 # 设置元数据缓存有效期为 7 天,单位是秒 metadata_expire=604800这样,只要缓存未过期,yum install就会直接使用本地缓存,响应时间从分钟级降到秒级。
6.3 实战验证清单
在将仓库交付给其他同事前,我总会执行以下五项验证,确保万无一失:
| 验证项 | 命令 | 预期结果 | 说明 |
|---|---|---|---|
| 1. 元数据完整性 | rpm -K /opt/epel-offline/epel-offline/repodata/primary.xml.gz | primary.xml.gz: OK | 校验元数据文件自身未损坏 |
| 2. 包文件完整性 | rpm -K /opt/epel-offline/epel-offline/Packages/a/ansible-2.9.27-1.el7.noarch.rpm | ansible-2.9.27-1.el7.noarch.rpm: OK | 随机抽查一个包的 SHA256 校验 |
| 3. 依赖图谱可解析 | yum deplist ansible --disablerepo=\* --enablerepo=epel-offline | head -20 | 显示dependency: python36-jinja2等依赖项 | 确认primary.xml.gz中的Requires字段被正确读取 |
| 4. 安装流程通路 | yum install --assumeno ansible | 显示将安装的包列表,无报错 | --assumeno模拟安装,不实际写入磁盘 |
| 5. 组安装功能 | yum groupinstall "Development Tools" --disablerepo=\* --enablerepo=epel-offline | 成功下载并列出所有组内包 | 验证--downloadcomps是否生效 |
这五步验证,覆盖了从文件层、元数据层、依赖解析层到最终用户操作层的全栈,是保障离线仓库交付质量的黄金标准。
7. 进阶技巧:增量同步、空间优化与跨架构支持
一个成熟的离线仓库,绝不能是一次性工程。随着业务发展,你可能需要定期更新仓库,或为不同硬件平台(如 aarch64)提供支持。以下是我在多个项目中沉淀下来的进阶技巧。
7.1 增量同步:只下载变化的包,节省 90% 带宽
reposync默认是全量同步,但通过--newest-only和--delete参数,可以实现智能增量:
# 第一次全量同步后,后续更新只需: sudo reposync -r epel-offline -p /opt/epel-offline --download-metadata --newest-only --delete--newest-only告诉reposync只下载比本地已有版本更新的包;--delete则会删除本地已存在、但远程已移除的包(例如被归档到archive/的旧版本)。实测表明,在 EPEL 7 每月约 5% 的包更新率下,增量同步耗时仅为全量的 1/10,流量消耗降低 90% 以上。
7.2 空间优化:硬链接去重,节省 40% 存储
EPEL 仓库中,大量包存在相同的glibc、openssl-libs等基础依赖。reposync下载的 rpm 文件是独立副本,造成冗余。一个简单高效的去重方案是使用硬链接(Hard Link):
# 安装 hardlink 工具 sudo yum install -y hardlink # 进入 Packages 目录,对所有同名同大小的文件创建硬链接 sudo hardlink -cv /opt/epel-offline/epel-offline/Packages/hardlink会扫描目录下所有文件,对inode相同的文件(即内容完全一致)用硬链接替代,物理存储只保留一份。在我的一个 120GB 的 EPEL 仓库中,此操作释放了 48GB 空间,效果立竿见影。
7.3 跨架构支持:为 aarch64 或 ppc64le 构建仓库
EPEL 不仅支持x86_64,还为aarch64(ARM64)、ppc64le(PowerPC)等架构提供包。构建多架构仓库,只需修改 repo 配置中的baseurl:
# 对于 ARM64 服务器 baseurl=https://dl.fedoraproject.org/pub/epel/7/aarch64/ # 对于 PowerPC 服务器 baseurl=https://dl.fedoraproject.org/pub/epel/7/ppc64le/然后,为每个架构创建独立的同步目录(如/opt/epel-offline-aarch64/),并分别执行reposync和createrepo。客户端只需配置对应的baseurl,即可无缝切换。这为异构计算环境提供了坚实的基础支撑。
最后分享一个小技巧:在createrepo命令后,加上--workers $(nproc)参数,可以利用所有 CPU 核心并行处理 rpm 包,对于大型仓库,生成元数据的速度能提升 3-5 倍。我在一台 16 核服务器上,处理 15000 个包的元数据,耗时从 42 分钟缩短到 9 分钟。
这套方案,已在某能源集团的 300+ 台工控服务器上稳定运行两年,零故障。它证明了一件事:离线不是落后的代名词,而是对系统可靠性最严苛的考验。当你能把一个看似简单的“下载 rpm 包”任务,拆解成对协议、元数据、依赖图谱和缓存机制的深度掌控时,你就已经站在了运维工程师的高地上。