1. 先把需求想透:自建yum源仓库到底解决什么问题
聊“建立 yum 源仓库”这个话题,我通常会先问一句:你是想给自己几台测试机省点流量,还是想给一整个机房的几百台服务器做统一的软件供给。这两个诉求看起来都叫“建个源”,但落地形态差别很大。yum 源仓库(现在多数发行版底层已经换成 dnf,但大家嘴上还是习惯叫 yum 源)本质上只有两样东西:一堆 rpm 包,加一份描述这些包之间依赖关系、版本、校验值的元数据。客户端执行安装时,第一步是抓repodata/repomd.xml,再顺着索引找到需要的包,最后下载安装。所以“建仓库”这件事的技术核心,从来不是把包堆到目录里,而是让元数据正确、可访问、可被信任。
我在实际项目里见过太多“仓库建好了但装不上”的情况,追到底往往不是网络问题,而是元数据没重生成、目录权限差一点、或者客户端缓存了旧索引。所以这篇文章我打算按真实交付的顺序讲:先判断该建哪种源,再算磁盘、备工具,然后手把手做出一个能用的 HTTP 源,接着解决“仓库怎么持续更新”这个真正磨人的部分,最后把签名策略和踩过的坑摊开说。适合刚接手内网运维的新手照着做,也适合已经有一版粗糙实现、想把它做规范的人对照检查。
需要提前说清一个前提:下面所有命令我都以主流企业级发行版的第 8、第 9 代为准,包管理器是dnf,工具是createrepo_c和dnf reposync。如果你手上是更老的系统,命令名会变成createrepo和reposync(属于yum-utils),参数逻辑基本一致,少几个选项而已。版本差异我会在关键位置标注,其余地方不重复啰嗦。
1.1 三种典型场景,决定了你该建哪种源
第一种是离线机房,机器根本出不了网。这种场景下你需要的其实是“本地目录源”或者“光盘源”,把安装镜像挂载到某个目录,客户端baseurl直接写file:///mnt/dvd。这种做法简单到几乎没有技术含量,但它有个致命短板:光盘里的包是死的,三个月后你要装个新版本的中间件,大概率找不到对应包,只能重新刻盘或者手工拷 rpm 进去再重新生成元数据。
第二种是有可控出网口的内网,允许一台机器访问上游镜像站,其他机器只访问这台机器。这是最主流的形态,也是我推荐的做法:选一台磁盘够大、带宽稳定的机器当“仓库节点”,用dnf reposync把上游的包和元数据同步到本地,再用 nginx 把它暴露成 HTTP 服务。好处是内网所有机器的软件版本完全一致,升级节奏由你控制,某天上游撤包了你本地还有备份。
第三种是多环境隔离,开发、测试、生产各要一套,版本还不能互相污染。这时候就得在目录结构上做文章,按发行版大版本/仓库名/架构分三层,比如el9/BaseOS/x86_64/os、el9/AppStream/x86_64/os。客户端通过不同的.repo文件切换,再配合priority插件控制优先级。这种结构一开始多花十分钟设计,后面能省掉几十次“为什么这台机器装出来的版本跟那台不一样”的扯皮。
选型上我的判断标准很直接:机器数量少于十台、且能出网,别折腾,直接用上游源加本地缓存就够了;机器超过三十台,或者有合规要求不允许外联,那就老老实实建仓库,投入产出比立刻显现。
1.2 方案对比:别一上来就上重型工具
很多人第一反应是去找一套现成的仓库管理平台,其实大部分场景用系统自带工具就能覆盖。我把几种常见做法列一下,你可以对着自己的情况挑:
| 方案 | 适用规模 | 优点 | 明显短板 |
|---|---|---|---|
| 光盘/ISO 挂载本地源 | 单机、临时 | 五分钟搞定,零依赖 | 包固定,无法增量,多机要重复挂载 |
目录 +createrepo_c+ nginx | 十到几百台 | 全手工可控,结构清晰 | 需要自己写同步脚本 |
dnf reposync全量镜像 | 中大型内网 | 与上游完全一致,含元数据 | 占磁盘,首次同步耗时长 |
| 上游镜像站 rsync 同步 | 大型内网 | 增量效率最高,支持断点续传 | 需要上游开放 rsync 服务 |
这里有个容易被忽略的点:reposync默认只下载 rpm 包,不下载元数据,下载完你还得自己跑一遍createrepo_c。新版 dnf 提供了--download-metadata参数,可以把上游的repodata原样拉下来,省掉重新生成这一步,速度快很多,而且元数据与上游完全一致,出问题的概率更低。代价是你没法对仓库做“裁剪”,比如剔除某些包,因为元数据是上游签名的,你改了就对不上。要裁剪就只能走“下载包 + 本地重新生成元数据”这条路。
提示:如果你的内网有合规审计要求,建议选“重新生成本地元数据”的方案。这样你能明确掌握仓库里到底有哪些包、什么时候放进来的,出问题时可追溯。
2. 动手之前的准备:磁盘、目录、工具三件事
准备工作做得好,后面八成的故障都不会出现。我吃过的最痛的一次亏,是仓库节点磁盘只给了 100G,同步到一半空间满了,reposync报了个很含糊的错误,我花了两个小时才定位到是空间问题。从那以后我养成了一个习惯:建仓库之前先把容量算清楚,宁可多留一倍余量。
2.1 容量估算与分区规划
一个完整的企业级发行版仓库,体积比大多数人想象的大。以第 9 代为例,BaseOS大概 2 到 3G,AppStream因为包含各种运行时和开发工具,能到 8 到 12G,如果再算上CRB(兼容库)和第三方扩展源,单机房的完整镜像很容易突破 40G。而且这只是“一个版本、一个架构”,如果你还要保留上一代做兼容,直接翻倍。
| 仓库组成 | 大致体积 | 说明 |
|---|---|---|
| BaseOS | 2 至 3 GB | 内核、基础命令、系统库 |
| AppStream | 8 至 12 GB | 语言运行时、数据库、Web 服务 |
| CRB / 兼容库 | 3 至 5 GB | 开发头文件、兼容性依赖 |
| 第三方扩展源 | 15 至 25 GB | 额外工具,包量大但单个小 |
| 元数据与索引 | 数百 MB | 别小看,包多时 repodata 很占地方 |
具体数字随版本和架构波动,量级参考即可。我的做法是按估算值乘以 2.5 倍给分区,因为同步过程会有临时文件,createrepo_c生成新元数据时旧元数据还在,两边同时存在;而且你几乎一定会遇到“顺手再同步一个源”的需求。分区建议单独挂载,别和系统盘混在一起,否则仓库写满会连带把日志、数据库一起拖垮。
还有一个坑是 inode。rpm 包数量多、体积小,如果文件系统格式化时 inode 比例设得保守,可能出现“df -h显示还有空间,但就是写不进去”的情况。用df -i看一眼 inode 使用率,超过 80% 就要警惕了。
2.2 必备工具与版本确认
工具其实就三个:createrepo_c(生成元数据)、dnf-plugins-core(提供reposync)、nginx或httpd(对外提供 HTTP 服务)。装之前先确认版本,因为不同大版本的参数差异不小:
# 确认系统版本与包管理器版本 cat /etc/os-release dnf --version # 安装核心工具 dnf install -y createrepo_c dnf-plugins-core nginx装完验证一下:
createrepo_c --version dnf reposync --help | head -20如果dnf reposync提示找不到命令,说明dnf-plugins-core没装上,或者你用的是极简安装的系统,插件包被裁掉了。这时候别急着去编译,先把插件包装回来。另外顺手把rsync也装上,后面做增量同步时会用到。
createrepo_c是 C 语言重写的版本,比老的 Python 版createrepo快一个数量级,处理几万个包时差距非常明显。如果你在很老的系统上只能用createrepo,那就得做好心理准备,第一次全量生成元数据可能要跑十几分钟,而且占内存。
2.3 目录结构怎么设计才不后悔
我见过最难受的一种仓库结构,是所有包全平铺在一个目录里,几百个仓库混在一起,元数据互相覆盖,最后谁也不敢动。正确的做法是按层级分,我一般用这套:
/data/yumrepo/ ├── el8/ │ ├── BaseOS/x86_64/os/ │ ├── AppStream/x86_64/os/ │ └── extras/x86_64/os/ ├── el9/ │ ├── BaseOS/x86_64/os/ │ ├── AppStream/x86_64/os/ │ └── CRB/x86_64/os/ └── thirdparty/ └── tools/x86_64/os/这个结构的精髓在于每一层都有明确含义:第一层区分发行版大版本,第二层是仓库名(对应上游的 repoid),第三层是架构,最后os表示这是正式发布包而不是测试包。客户端配置时,baseurl直接指向os目录,$releasever变量还能自动替换版本号,同一个.repo文件可以复用到多个版本上。
目录权限统一设成755,属主给运行 Web 服务的用户或者干脆root,因为 nginx 只需要读权限。这一点后面讲排查的时候还会提到,权限不对是最常见的 403 来源。
3. 从零做一个能被内网访问的 yum 源仓库
假设现在磁盘挂好了、目录也建了,我们从“塞几个包进去”开始,一步步做成一个客户端能真正识别的仓库。这一步我建议先用少量包验证,跑通了再做全量同步,别一上来就同步几十 G,出错了排查成本太高。
3.1 手动放包并生成第一份元数据
先建一个最小的测试仓库:
mkdir -p /data/yumrepo/el9/BaseOS/x86_64/os # 从安装介质或已有缓存里拷几个 rpm 进去 cp /root/rpms/*.rpm /data/yumrepo/el9/BaseOS/x86_64/os/ # 生成元数据 createrepo_c --database --workers 4 \ /data/yumrepo/el9/BaseOS/x86_64/os执行完去看目录,会多出一个repodata子目录,里面最关键的三个文件是repomd.xml(总索引)、*-primary.xml.gz(包信息)、*-filelists.xml.gz(文件列表)。客户端第一步就是拿repomd.xml,如果这个文件不存在或者损坏,后面全是错误。
几个参数值得解释一下。--database是生成 sqlite 格式的额外索引,老版本 yum 需要它才能正常工作,新版 dnf 其实已经不依赖了,但生成它成本很低,兼容性更好,我一般都会加上。--workers控制并行度,包多的时候设成 CPU 核数能明显提速,包少的时候没必要。还有一个--update参数是增量模式,只处理新增或变化的包,全量重建时不加,日常更新时必加,否则每次都从头扫描,几万个包能把人急死。
注意:
--update的一个已知行为是,如果你删除了某个 rpm 包,元数据里不一定立刻反映出来。稳妥做法是在大量删包之后做一次--update之后再补一次全量。我在一个项目里就因为删了旧包但元数据残留,导致客户端一直报“找不到包”,查了很久。
3.2 用 Web 服务把仓库暴露出去
本地路径只适合单机,多机场景必须走 HTTP。nginx 配置很简单,关键是几个细节:
server { listen 80; server_name _; root /data/yumrepo; autoindex on; autoindex_exact_size off; location / { index index.html; } access_log /var/log/nginx/yumrepo_access.log; }autoindex on的作用是让浏览器能列目录,排查问题时非常有用:客户端报 404,你直接在浏览器打开对应 URL,一眼就知道是文件不存在还是路径写错了。生产环境如果觉得不安全,可以只在内部网段开放,或者加个基础认证,但排查阶段先开着。
如果想快速验证而不装 nginx,用 Python 自带的模块也能顶一下:
cd /data/yumrepo && python3 -m http.server 8000这个命令前台运行、性能一般,只适合临时验证,别拿去生产。验证通过后记得关掉。
SELinux 开启的情况下,nginx 读/data下的目录会被拦,表现为 403。处理方式有两种,改上下文或者加策略,我一般用前者:
chcon -R -t httpd_sys_content_t /data/yumrepo这条命令要在每次新增目录后重新执行,因为新目录继承的是父目录上下文,如果父目录本身没设对,新目录一样会被拦。如果系统默认策略不允许访问/data,还得确认httpd_can_network_connect之类的布尔值,不过读本地文件通常不需要。
3.3 客户端.repo文件怎么写才不容易出错
服务端搞好后,客户端配置是最后一公里。在/etc/yum.repos.d/下建一个文件:
[internal-baseos] name=Internal BaseOS baseurl=http://10.0.0.10/yumrepo/el9/BaseOS/x86_64/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-internal metadata_expire=3600几个字段值得细说。gpgcheck=1表示校验包的签名,这个开关别为了图省事关掉,内网也不是绝对安全,签名是最后一道防线。metadata_expire控制元数据缓存时间,默认是 48 小时,对频繁更新的内部仓库来说太长,设成一小时比较合适。如果不设,客户端可能在你更新仓库半天之后还看不到新包,然后你会怀疑人生。
还有baseurl结尾的斜杠,这个细节坑过不少人。有些版本的 dnf 对结尾斜杠比较敏感,加上更保险。另外如果内网用自签证书上了 HTTPS,记得配sslverify=0或者把 CA 导入系统信任链,前者简单但降低安全性,后者麻烦但规范,看你自己的合规要求。
配好之后立刻验证:
dnf clean all dnf makecache dnf repolist -vdnf repolist -v会打印每个仓库的 baseurl、元数据更新时间、包数量。如果包数量是 0,说明元数据没读到;如果直接报错,看错误信息里的 URL,拿浏览器打开那个 URL 对一下,通常十秒就能定位。
4. 让仓库自己长起来:同步上游与增量更新
手工拷包只适合验证,真正跑起来必须靠同步。这一节讲怎么把上游的仓库完整拉到本地,以及怎么让它在无人值守的情况下每天自动更新。
4.1reposync的两种用法,选错会多花几小时
dnf reposync有两种工作模式。第一种是不带--download-metadata,只下载 rpm 包,下载完你自己跑createrepo_c:
dnf reposync --repoid=appstream \ --destdir=/data/yumrepo/el9/AppStream/x86_64 \ --newest-only --delete第二种是带--download-metadata,把上游元数据一起拉下来:
dnf reposync --repoid=appstream \ --destdir=/data/yumrepo/el9/AppStream/x86_64 \ --download-metadata --download-comps两者的差别在于:前者你能完全控制仓库内容(可以剔除包、可以加自己的包),但每次同步后要重新生成元数据,包多时这一步很慢;后者速度最快、元数据与上游完全一致,但你没法裁剪,而且如果上游元数据签名了,你本地改了包会校验失败。
--newest-only表示只同步每个包的最新版本,能省大量空间,适合“只要求能装最新版”的场景。如果内网有历史版本回溯需求(比如某个业务锁定在老版本),这个参数就必须去掉,全量同步。
--delete表示本地有但上游没有的包会被删掉。这个参数要慎用,它是“镜像”语义,不是“增量”语义。我曾经在一个项目里手滑执行,把同事手工上传的几个内部定制包一起删了,好在有备份。建议在执行带--delete的命令前先做一次目录快照,或者干脆不用这个参数,改为定期人工清理。
4.2 同步脚本怎么写才稳
真实环境里,同步脚本要考虑四件事:并发冲突、失败重试、日志留痕、执行时间。我常用的一份脚本大致是这样:
#!/bin/bash set -uo pipefail REPO_ROOT=/data/yumrepo LOG=/var/log/repo-sync.log LOCK=/var/lock/repo-sync.lock exec 9>"${LOCK}" if ! flock -n 9; then echo "$(date '+%F %T') 已有同步任务在跑,本次退出" >> "${LOG}" exit 0 fi sync_one() { local repoid="$1" dest="$2" echo "$(date '+%F %T') 开始同步 ${repoid}" >> "${LOG}" if dnf reposync --repoid="${repoid}" \ --destdir="${dest}" --download-metadata --download-comps \ >> "${LOG}" 2>&1; then echo "$(date '+%F %T') ${repoid} 同步完成" >> "${LOG}" else echo "$(date '+%F %T') ${repoid} 同步失败,退出码 $?" >> "${LOG}" return 1 fi } sync_one baseos "${REPO_ROOT}/el9/BaseOS/x86_64" || exit 1 sync_one appstream "${REPO_ROOT}/el9/AppStream/x86_64" || exit 1flock那一段是关键。定时任务最怕的就是上一轮还没跑完,下一轮又被拉起来,两个进程同时写同一个目录,元数据直接损坏。用文件锁挡住,成本极低,收益极高。
定时任务这么配:
0 3 * * * /usr/local/bin/sync-repo.sh凌晨三点是相对安全的窗口。如果你的上游更新频繁,可以一天跑两次,但要注意两次之间要留够时间,别撞上。
4.3 增量同步的另一条路:rsync
如果上游镜像站开放了 rsync 服务,用 rsync 同步比reposync效率高得多,因为它能做真正的块级增量,还能断点续传:
rsync -avz --delete --partial \ rsync://mirror.example.internal/el9/ \ /data/yumrepo/el9/这种方式同步完,元数据也是上游原样,不需要重新生成。缺点是依赖上游提供 rsync 端口,很多内部镜像站并不开放。另外--delete同样是危险参数,删之前想清楚。
有一类特殊情况值得提一句:如果你需要在同步后追加自己的内部包,那就不能直接用上游元数据,必须走“下载包 + 本地createrepo_c --update”的流程,否则你自己的包在元数据里根本不存在,客户端永远看不到。
5. 签名、校验与客户端策略
内网仓库不做签名,短期看不出问题,但一旦有人往仓库里塞了个来路不明的包,你连追溯的手段都没有。签名这件事前期投入半小时,后面省心几年。
5.1 生成本地 GPG 密钥并对元数据签名
先生成一对密钥,注意用批量模式,否则会卡在交互界面:
cat > /root/repo-gpg.batch <<'EOF' %echo Generating internal repo key Key-Type: RSA Key-Length: 4096 Name-Real: Internal Repo Signing Name-Email: repo@internal.local Expire-Date: 0 %no-protection %commit %echo done EOF gpg --batch --gen-key /root/repo-gpg.batch导出公钥,分发到所有客户端:
gpg --armor --export repo@internal.local \ > /etc/pki/rpm-gpg/RPM-GPG-KEY-internal然后对每个仓库的repomd.xml签名:
gpg --batch --yes --detach-sign --armor \ --default-key repo@internal.local \ /data/yumrepo/el9/BaseOS/x86_64/os/repodata/repomd.xml执行完会多一个repomd.xml.asc。客户端.repo里加一行repo_gpgcheck=1,就会去校验这个签名文件。要注意的是,repo_gpgcheck和gpgcheck是两个不同的开关,前者校验元数据,后者校验 rpm 包本身,两个都开才算完整。
签名这件事有个顺序陷阱:必须先createrepo_c生成元数据,再对repomd.xml签名。如果你先签名再重新生成元数据,签名文件就对不上了,客户端会报 “repomd.xml signature could not be verified”。所以同步脚本里,签名这一步必须放在元数据处理之后,我一般写成同步完成后统一遍历所有repomd.xml逐个签名。
5.2 客户端侧的优先级与排除策略
多仓库并存时,同一个包可能在多个源里都有,版本还不一样。这时候priority插件能救你一命:
dnf install -y dnf-plugin-priority然后在.repo文件里给每个仓库加优先级数字,数字越小优先级越高:
[internal-baseos] priority=10[thirdparty-tools] priority=50 exclude=kernel* php8.0-*这个机制的逻辑是:当客户端要装某个包时,先看高优先级仓库里有没有,有就从那里拿,没有才降级去找。这能有效防止第三方源里的同名包把系统基础包覆盖掉——那种事故恢复起来非常麻烦,轻则依赖地狱,重则系统起不来。
exclude也是好东西。第三方源里经常有一些包与系统自带版本冲突,与其每次装的时候手动加--exclude,不如在源配置里一次性排除掉。排除支持通配符,kernel*这种写法能用,但要注意它会把kernel-devel、kernel-headers一起排掉,写之前想清楚。
6. 常见故障排查实录
这一节是我这些年攒下来的错误对照表,基本都是现场真实遇到过的。遇到问题先查表,能省掉大量瞎试的时间。
| 报错关键词 | 大概率原因 | 处理动作 |
|---|---|---|
Cannot find a valid baseurl | URL 写错、DNS 不通、端口没开 | 浏览器直接访问 baseurl 验证 |
repomd.xml: HTTP Error 404 | 目录层级不对,少了os或架构层 | 对照实际目录结构改 baseurl |
403 Forbidden | 目录权限或 SELinux 上下文 | chcon -R -t httpd_sys_content_t |
Failed to download metadata | 元数据缺失或损坏 | 重新跑createrepo_c |
GPG key retrieval failed | 公钥路径错或未导入 | 检查gpgkey=指向的文件是否存在 |
signature could not be verified | 签名文件与元数据不匹配 | 重新签名repomd.xml |
| 装到了旧版本 | 客户端元数据缓存未过期 | dnf clean all && dnf makecache |
requires X, but none of providers | 多仓库版本混装、依赖缺失 | 检查priority与exclude |
| 空间够但写不进去 | inode 耗尽 | df -i查看 |
| 同步报错但错误信息含糊 | 磁盘满或文件锁冲突 | 看日志末尾,查df -h和进程 |
6.1 元数据类问题的快速定位法
元数据问题占了故障的一半以上,而它们的共同特征是“客户端说找不到包,但服务端明明有”。我总结了一个三步定位法,几乎百试百灵。
第一步,在服务端确认元数据新鲜度:ls -l repodata/repomd.xml,看时间戳是不是你最近一次更新的时间。如果不是,说明createrepo_c根本没跑成功,或者跑到了别的目录。
第二步,在客户端绕开缓存直接看:dnf --refresh repolist -v,--refresh会强制重新拉元数据。如果这一步能看到包,那就是缓存问题,dnf clean all解决。
第三步,如果前两步都正常但装包还是失败,用dnf repoquery --repoid=internal-baseos <包名>精确查询。这个命令会直接告诉你该仓库里有没有这个包、版本是多少,比dnf list更干净。
实操心得:有时候问题出在“同名仓库重复定义”。
/etc/yum.repos.d/下如果存在两个.repo文件定义了同一个[repoid],dnf 会以其中一个为准,另一个被静默忽略。我就遇到过有人手工复制了一份 repo 文件改了 baseurl 却忘了改 repoid,结果怎么改都不生效。排查时用grep -r "^\[" /etc/yum.repos.d/扫一眼,重复的 repoid 立刻现形。
6.2 依赖冲突与版本混装的排查思路
requires X, but none of the providers can be installed这条报错,字面意思是找不到满足依赖的包,但真实原因往往是“找到了多个版本,挑不出一个能同时满足所有约束的”。典型的触发场景是把第三方源和系统源混用,同一个库有两个不兼容版本。
处理思路是先把候选摊开看:
dnf repoquery --whatprovides "libfoo.so.1" dnf repoquery --requires <包名> --resolve第一条看谁提供了这个依赖,第二条顺着依赖链往下走。如果发现第三方源和基础源都提供,用priority把基础源提上去,让系统优先从官方源取。如果依赖是版本区间冲突(A 要求libfoo >= 2,B 要求libfoo < 2),那就只能靠--nobest或者干脆把其中一个包换掉,没有银弹。
还有一种隐蔽的情况:仓库里同时存在同一个包的两个版本(比如你全量同步过又加了--newest-only同步),元数据里两个版本都在。客户端一般会选最新的,但如果依赖要求旧版本,就会陷入选择困难。清理方式是定期做一次仓库整理,把过老的版本移走,然后重新生成元数据。
6.3 同步过程中的典型卡点
reposync跑到一半停下,最常见的三个原因是磁盘满、网络中断、上游临时限流。前两个好判断,第三个比较隐蔽,表现是速度突然掉到几百 K 然后超时。我的应对方式是给dnf配超时和重试:
# /etc/dnf/dnf.conf [main] timeout=60 retries=3 minrate=1kminrate这个参数挺有意思,它表示“如果下载速度低于这个值一段时间就判定失败并重试”,配合retries用,对不稳定链路很有效。默认是不开启的,很多人不知道有这东西。
另外同步大仓库时建议用nohup或者tmux跑,别直接在 SSH 会话里执行。我见过太多次“传了一半 SSH 断了,进程也跟着没了”的事故,虽然reposync有一定续传能力,但重新拉起还是要重新扫描一遍,白白浪费时间。
6.4 关于缓存代理的一个提醒
如果内网在仓库前面架了 HTTP 缓存代理,要特别注意repomd.xml的缓存策略。这个文件内容会变,但 URL 不变,代理一旦缓存了几小时,客户端拿到的就是旧索引。表现是“服务端明明更新了,客户端死活看不到新包”。解决方式是在代理上对repodata/路径设置不缓存,或者给 nginx 加上明确的响应头:
location ~* /repodata/ { add_header Cache-Control "no-cache, must-revalidate"; expires 0; }这个细节非常容易被忽略,但一旦踩上,排查方向会完全跑偏,因为你会一直怀疑是服务端的问题。
7. 长期运维里,我踩出来的几条经验
仓库建起来只是开始,真正花时间的是维护。说几条我认为最有价值的做法,都是吃过亏换来的。
第一,给元数据更新和包上传建立明确的时间纪律。别在业务高峰期跑全量同步,createrepo_c会大量占用磁盘 IO,同一台机器上如果还跑了别的东西,很容易被拖慢。我一般把同步窗口和业务、备份窗口错开至少两小时。
第二,保留至少一份“上一个可用版本”的元数据快照。做法很简单,每次更新前把repodata目录复制一份到repodata.bak。一旦新元数据生成出错导致整个仓库不可用,把备份拷回去重启服务,一分钟内恢复。这个习惯让我在一次createrepo_c中途被 OOM 杀掉的事故里几乎没有影响业务。
第三,监控要盯三个指标:磁盘使用率、inode 使用率、repomd.xml的修改时间。前两个防写满,第三个防“同步任务静默失败”。我见过最坑的一种情况是定时任务因为密钥过期、路径变更之类的原因连续失败了一周,但没人发现,直到某天要装包才炸出来。加一个简单的告警:如果repomd.xml超过 72 小时没更新,就发通知。
第四,别把仓库节点当成唯一的包来源。重要版本的 rpm 包最好再另存一份到对象存储或者离线磁盘,仓库节点只是个分发服务,不是备份系统。磁盘阵列也会坏,真坏了重建一个几十 G 的仓库要花好几个小时,有备份能省掉这段停机时间。
最后说个结构上的建议:如果你的内网机器超过五十台,考虑在仓库前面加一层内网 CDN 或者多台镜像节点,用 DNS 轮询或负载均衡分摊流量。单台 nginx 扛几十台机器的makecache请求是没问题的,但要是几百台机器在同一时间点集中拉取(比如统一变更窗口),带宽瞬间打满,体验会非常糟糕。提前规划这一步,比事后救火舒服得多。