这两年做国产化迁移的团队越来越多,而我接到的第一个硬任务,就是在一台 CentOS 9 机器上把整套源环境部署起来。这里的“源”不是源码,而是 yum/dnf 软件源——让整个内网都从自建的仓库拉包,而不是一台台服务器去公网碰运气。这个需求听起来简单,真正落地时牵扯到仓库同步、元数据生成、客户端切换、国产化系统兼容等一堆细节,踩一次坑就是几小时起步,所以我把完整的部署过程整理成这篇文章,给准备做同样事情的同学一个可以照抄的作业。
先说一下适合读这篇文章的人:正在做系统迁移的运维工程师、信创适配项目的实施人员、需要管理大批 CentOS 机器的 DevOps 同学。文章不会绕弯子,直接讲清楚怎么做、为什么这么做,以及我在实际环境里遇到的坑。
1. 项目背景:CentOS 生命周期大限与迁移窗口
1.1 CentOS 9 到底是个什么版本
很多同学第一次接触这个需求时,会被版本号搞懵。CentOS Linux 8 在 2021 年 12 月 31 日停止维护,CentOS Linux 7 在 2024 年 6 月 30 日彻底 End of Life,而传统的 CentOS Linux 9 这个版本从来就没有独立发布过。现在大家口中说的 CentOS 9,实际指的是 CentOS Stream 9——它是 RHEL 9 的滚动预览版,跟当年那种“完全免费克隆 RHEL”的 CentOS Linux 定位完全不同。
这一点决定了源环境部署的第一个关键决策:如果你搜索“centos 9 下载”拿到的是 CentOS Stream 9 的 ISO,那么你的源环境必须基于 Stream 仓库结构来建。Stream 的仓库分成 BaseOS、AppStream、CRB、Extras 等几个独立模块,而不是老 CentOS 7 那种单个大仓库。理解这个结构,后面配置 repoid 才不会出错。
1.2 源环境部署在国产化迁移中的位置
国产化迁移不是把系统一换就完事,它是一条完整的链路:硬件选型、操作系统替换、中间件适配、应用改造、测试验收。而源环境部署属于最底层的基础设施环节,它决定了后续所有服务器能不能稳定、统一、可控地获取软件包。
实际项目里,源环境要解决三个具体问题。第一是内网隔离问题,很多机房的机器根本没有公网出口,装个 nginx 都要从光盘里翻,更别说装编译工具链。第二是版本一致性问题,如果放任每台机器各自从外网或第三方源拉包,三个月后你面对的就是几十种不同版本的 glibc 和 openssl,应用一迁移全是兼容性灾难。第三是供应链可控性问题,自建源环境意味着所有进入内网的 RPM 包都经过你的审查,这对国产生态里的安全合规要求尤其重要。
2. 方案选型与整体架构设计
2.1 四种源环境方案对比
我在动手之前,先梳理了市面上常用的四种做法,这里直接列成表给你参考:
| 方案 | 实施难度 | 可控性 | 适用场景 | 维护成本 |
|---|---|---|---|---|
| 直接使用公网镜像源 | 低 | 差 | 有外网的开发测试环境 | 无 |
| 自建本地镜像仓库(reposync + createrepo) | 中 | 高 | 内网生产环境、国产化迁移项目 | 中 |
| 离线 RPM 包打包(dnf download 批量拉取) | 低 | 中 | 应用依赖数量少、单机部署 | 低 |
| 源码编译安装 | 高 | 高 | 没有现成 RPM 的特殊软件 | 高 |
对于国产化迁移的内网生产环境,绝大多数情况下都应该选第二种:自建本地镜像仓库。原因很简单,离线 RPM 包打包只适合一次性交付,后续补丁更新又得重新打包,长期维护成本反而高;源码编译则把依赖问题无限放大,除非万不得已不要碰。reposync 是 dnf 插件自带的同步工具,它跟 dnf 共享仓库配置,能按 repoid 精确同步指定仓库,再配合 createrepo_c 生成元数据,整套链路非常成熟。
2.2 目录规划与存储容量计算
源环境的目录结构直接影响后续扩展和排查效率。我建议按“操作系统类型 / 版本 / 仓库名 / 架构 / os”的层级来组织,例如:
/data/repo/ ├── centos/ │ └── 9/ │ ├── BaseOS/x86_64/os/ │ ├── AppStream/x86_64/os/ │ ├── CRB/x86_64/os/ │ └── Extras/x86_64/os/ ├── openeuler/ │ └── 22.03/ │ ├── BaseOS/x86_64/ │ └── AppStream/x86_64/ └── anolis/ └── 8/ └── ...容量估算上,CentOS Stream 9 的 BaseOS 仓库大约 3-4GB,AppStream 仓库随着版本更新会长到 30GB 以上,CRB 和 Extras 加起来也有几个 GB。如果只同步最新版本(--newest-only),x86_64 架构下四个仓库总共约 40-50GB;如果保留全部历史版本,轻松超过 80GB。再加上国产化系统(openEuler、Anolis 等都是同样的 RPM 体系),两三个系统的源加起来就需要 120-200GB 的存储空间。所以仓库服务器我强烈建议至少配两块 2TB 的盘做 RAID1,或者四块盘做 RAID5,不要省这点钱——我见过不止一次因为容量不够导致 reposync 中断,元数据写一半,整条源直接废掉的情况。
2.3 仓库服务器的角色划分
源环境部署不是单点任务,合理的架构里至少要分三类角色:仓库源服务器、客户端、验收机。仓库源服务器负责同步和发布,客户端是被管理的生产机器,验收机则是用来测试源环境是否可用的“试验田”。
验收机这个角色容易被忽略,但实际价值很大。每次做完仓库同步、客户端切换后,先在验收机上跑一遍dnf upgrade和典型应用的安装,确认没问题再放量到生产,可以避免把错误配置扩散到几十台机器上。这个习惯我后面会反复提到。
3. 源环境部署全流程实操
3.1 仓库服务器基础环境准备
仓库服务器的操作系统,我选择直接用 CentOS Stream 9 本尊,理由是在做国产化迁移的过程中,源服务器本身保持最接近上游的环境,最容易排查包层面的差异。安装时记住几个关键配置:磁盘用 LVM,/data 分区单独划出来放仓库数据;时区、主机名这种基础项按公司规范来;最小化安装即可,不需要带图形界面。
装完系统后,第一步是安装同步工具链:
dnf install -y dnf-plugins-core createrepo_c nginx这里有个细节:工具刻意选了 createrepo_c 而不是老的 createrepo。createrepo_c 是 C 语言实现,生成上百 GB 仓库的元数据时速度优势非常明显,而且对新版 dnf 的 zchunk 格式支持更完整。实测在普通服务器上,createrepo_c 对 AppStream 仓库做增量更新只要一两分钟,而老版本可能要跑十几分钟。
然后是防火墙和安全上下文。仓库服务要对外提供 HTTP 访问,所以放行 80 端口:
firewall-cmd --permanent --add-service=http firewall-cmd --reloadSELinux 这块是很多人翻车的地方。仓库目录如果放在 /data 下面,默认的 SELinux 上下文是 default_t,httpd/nginx 进程读不了。必须手动指定:
semanage fcontext -a -t httpd_sys_content_t '/data/repo(/.*)?' restorecon -Rv /data/repo这两条命令做完之后,nginx 才有权限读取仓库文件。如果你图省事直接setenforce 0,那我不建议,因为等迁移验收阶段安全检查时,SELinux 关闭本身就是不合格项。
3.2 使用 dnf reposync 同步 CentOS Stream 9 仓库
reposync 的用法核心是搞清楚 repoid。在 CentOS Stream 9 里,用dnf repolist能看到默认启用的仓库,但我做同步时会显式指定要同步的仓库,避免把不需要的东西拉下来:
dnf reposync \ --repoid=baseos \ --repoid=appstream \ --repoid=crb \ --repoid=extras \ --download-path=/data/repo/centos/9 \ --download-metadata \ --newest-only解释几个关键参数。--download-metadata会同时下载远程仓库的元数据,让同步下来的目录立刻具备仓库结构;--newest-only只保留每个包的最新版本,对大多数迁移场景都够用,还能省一半以上的磁盘空间。如果不加这个参数,仓库目录会积累一个包的所有历史版本,体积爆炸不说,客户端解析元数据也会变慢。
同步结束后,检查一下目录结构是否符合预期:
tree -L 3 /data/repo/centos/9/BaseOS/x86_64/os/正常情况下应该看到 Packages 目录和 repodata 目录。repodata 里就是远程源带过来的元数据,这部分数据可以省掉本地生成的那一步,不过为了保险,我习惯还是会用 createrepo_c 基于本地内容重新生成一遍。
3.3 用 createrepo_c 生成仓库元数据并发布
即使 reposync 已经带了远程的 repodata,我还是会在同步完成后对每个仓库独立执行一遍元数据生成,原因有两点:一是本地仓库目录可能被手动调整过(比如加了私有包),二是让元数据与本地文件指纹严格一致,避免客户端在 gpgcheck 之外遇到 checksum 不一致的报错。
createrepo_c --update /data/repo/centos/9/BaseOS/x86_64/os/ createrepo_c --update /data/repo/centos/9/AppStream/x86_64/os/ createrepo_c --update /data/repo/centos/9/CRB/x86_64/os/ createrepo_c --update /data/repo/centos/9/Extras/x86_64/os/--update参数非常关键,它表示增量更新,只重新扫描变化过的包,而不是全量重新生成。这个参数在后期定时同步脚本里几乎是必需品,配合--check还能先检查是否有变化再决定要不要更新元数据。
发布环节用 nginx 最省心。配置一个简单的 server 块:
server { listen 80; server_name repo.example.com; root /data/repo; autoindex on; autoindex_exact_size off; autoindex_localtime on; location / { index index.html; } }autoindex 打开后,浏览器可以直接看到目录结构,排查问题非常方便。配置完nginx -t检查语法,然后systemctl enable --now nginx启动。
3.4 客户端源配置与切换
客户端配置是源环境落地的最后一公里。进入 /etc/yum.repos.d/,把原来的公网源文件全部禁用,新增一个指向内网仓库的 repo 文件:
[baseos] name=CentOS Stream 9 - BaseOS (internal) baseurl=http://repo.example.com/centos/9/BaseOS/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial [appstream] name=CentOS Stream 9 - AppStream (internal) baseurl=http://repo.example.com/centos/9/AppStream/$basearch/os/ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficialGPG 密钥这块很多人会忘记。客户端必须能拿到官方公钥去验证 RPM 包的签名,否则 gpgcheck=1 会直接报错。两种方式:一种是在仓库服务器上把密钥放到 HTTP 目录里,客户端通过rpm --import http://repo.example.com/centos/9/RPM-GPG-KEY-centosofficial导入;另一种是把密钥手动拷贝到客户端的 /etc/pki/rpm-gpg/ 目录下。我推荐第二种,少一次网络依赖,离线机器也能处理。
配置完成后,按顺序执行:
dnf clean all dnf makecache dnf repolist看到仓库列表正常、缓存生成成功,再试装一个包验证,比如dnf install -y nginx,能装上就算基本跑通了。这里有个小技巧:先在一台验收机上做完整流程,确认无误后,再用 Ansible 或 SaltStack 批量推送 repo 文件和密钥。
4. 国产化系统的源适配实践
4.1 从 CentOS 源平滑迁移到 openEuler / Anolis
国产化迁移项目里,纯 CentOS 场景其实只占一半。很多单位的目标操作系统是 openEuler、Anolis OS(龙蜥)、麒麟 V10 或统信 UOS。好消息是,这些系统绝大多数保留了 RPM + dnf/yum 的包管理体系,意味着你在 CentOS 上学到的源环境技能可以直接迁移。
以 openEuler 22.03 LTS 为例,它的仓库结构和 CentOS Stream 很像,分为 BaseOS、AppStream、EPOL 等。同步方式也是 reposync,只是 repoid 名字不同。Anolis OS 走的是“兼容 CentOS”路线,它的 8 系列仓库设计上就考虑过和 CentOS 的迁移关系,部分包的命名和依赖与 CentOS 保持一致,迁移时应用层几乎不用改。
我在实际操作中总结了一条经验:不要试图用一份 repo 文件打天下。虽然$releasever变量在单系统里好用,但跨系统(CentOS 切 openEuler)时这个变量会自动被 dnf 替换成对应系统的版本号,路径就对不上了。正确做法是给每类系统单独建 repo 文件,通过 Ansible 的 os_family 判断分发不同模板。
4.2 兼容模式下的依赖一致性保障
国产化系统跟 CentOS 的“兼容”不是百分之百的二进制兼容。有些包在 openEuler 上编译用的 glibc 版本、openssl 版本和 CentOS Stream 9 不一样,强行混用源会导致依赖解析混乱。所以源环境里必须讲到隔离和锁版本。
我的做法是,在源服务器上对不同系统建独立的仓库目录,互不混放,同时在客户端启用versionlock插件锁定关键系统包版本。比如 glibc、openssl、systemd 这类底层组件,一旦确认当前版本可用,就锁住不参与日常dnf upgrade,只有经过验收的补丁窗口才手动解锁升级。
依赖一致性还有一层含义:应用打包时声明的依赖必须在源环境里能找到。迁移验收阶段最容易出的问题就是应用构建机用的源跟生产机的源不一致,导致构建出来的 RPM 生产机装不上。所以源环境部署完成后,一定要把构建机和生产机配置成同一套 repo 地址,这是从源头杜绝依赖漂移的办法。
5. 常见问题与排查技巧实录
5.1 问题速查表
把我在多个项目里遇到的典型问题整理成一个速查表,遇到类似现象可以直接对照。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| dnf 报 GPG key retrieval failed | 客户端没导入密钥,或 repo 里 gpgkey 路径错误 | 重新导入密钥,检查 gpgkey= 指向的文件是否存在 |
| 404 错误,报 repodata/repomd.xml 找不到 | baseurl 路径错误,或 createrepo 没在该目录执行 | 浏览器打开 baseurl 确认目录层级,必要时重新生成元数据 |
| 同步时磁盘写满,进程中断 | 仓库体积超过磁盘容量 | 加磁盘或用 --newest-only 重新同步 |
| nginx 访问目录 403 | SELinux 上下文未设置,或目录权限不够 | 执行 semanage fcontext + restorecon,检查 nginx 进程的读权限 |
| dnf makecache 报元数据校验失败 | 仓库正在更新时客户端拉取了不完整的元数据 | 清理缓存后等同步脚本跑完再操作 |
| 安装软件包报依赖冲突,提示找不到某依赖 | 缺少对应仓库,如依赖在 crb 里但没启用 | 检查 repolist,补齐 crb/extras 仓库 |
| 同步完客户端看不到新包 | 元数据是旧的,createrepo 没执行 | 重新执行 createrepo_c --update |
5.2 三个让我印象深刻的坑
第一个坑是 GPG 密钥的“一次性”思维。我最早部署完源环境,验收机测试一切正常,但一个月后新加一批机器时,密钥文件因为当时没保留原件,客户端死活导入不了。后来我养成了习惯:所有 GPG 密钥在仓库服务器上单独建一个 /data/repo/keys 目录长期保存,原文件和校验值一起收录进内部知识库,换人交接也不会断。
第二个坑是 SELinux 把 nginx 挡了整整半天。现象特别迷惑:nginx 配置没有任何问题,仓库目录权限也是 755,但客户端 curl 就是 403。后来用ausearch -m avc -ts recent查看审计日志,才发现 nginx 进程被 SELinux 拦截了,因为仓库目录在 /data 下,默认上下文不是 httpd_sys_content_t。这之后我把 SELinux 上下文配置直接写进了部署脚本,再也没被坑过。
第三个坑是 reposync 中断后的“半成品仓库”。有一次同步 AppStream 时网络断了,重跑时我忘了加--delete参数,导致仓库里残留了一批旧版本包和半个元数据目录。客户端 dnf makecache 一直报 primary.xml.gz 解析错误。这种问题光靠 mtime 看不出来,最好的办法是给是不是历史遗留包做一个判断,但更简单的方案是:同步脚本里固定用--newest-only --delete --download-metadata三个参数一起上,保持仓库状态始终干净。
5.3 排查思路中最重要的一个原则
不管问题表象是什么,先做两件事:dnf repolist -v看当前启用了哪些仓库和 baseurl,以及dnf clean all把本地缓存清干净。很多“看起来像源坏了”的问题,其实是客户端本地缓存了旧的元数据,清理一遍就正常了。这个动作成本极低,却能排除掉一半以上的干扰项,我每次排查都从它开始。
6. 自动化运维与个人心得
6.1 定时同步脚本的完整写法
源环境不是配完就一劳永逸,必须靠定时同步保持跟上游一致。我在生产环境用的同步脚本大概是这样的:
#!/bin/bash # /usr/local/bin/repo-sync.sh set -euo pipefail REPO_BASE="/data/repo/centos/9" REPOS="baseos appstream crb extras" CURRENT_DATE=$(date +%Y%m%d) # 防止上次同步残留影响,先做一次缓存目录检查 mkdir -p "${REPO_BASE}" for repo in $REPOS; do echo "[$(date '+%F %T')] syncing ${repo} ..." dnf reposync \ --repoid="${repo}" \ --download-path="${REPO_BASE}" \ --download-metadata \ --newest-only \ --delete \ || { echo "reposync ${repo} failed"; exit 1; } # 增量重建元数据 createrepo_c --update \ "${REPO_BASE}/${repo}/x86_64/os/" \ || { echo "createrepo ${repo} failed"; exit 1; } done # 生成同步状态文件,方便监控 echo "${CURRENT_DATE}" > /data/repo/.last_synccron 配置建议每周日凌晨 2 点执行,避开工作时段带宽高峰:
0 2 * * 0 /usr/local/bin/repo-sync.sh >/var/log/repo-sync.log 2>&1这个脚本注意两点:一是set -euo pipefail,任何一步失败立即退出并留下日志,避免带着脏数据继续同步;二是同步完一定要执行 createrepo_c,仓库元数据是最容易“过期”的部分。
6.2 迁移窗口期的版本快照与回滚经验
在国产化迁移的正式切换窗口期,我的做法是给源环境做一个“快照版本”。具体来说,迁移前把仓库目录完整复制一份或打一个 tar 归档到独立存储,迁移期间所有客户端固定指向快照,迁移验收不通过可以原路回滚,验收通过后再切换到增量同步的正式源。这套操作让很多团队在迁移后期有余地处理意外问题,而不是被迫在“继续往前改”和“回到老系统”二选一。
这个习惯来自我自己的教训:有一次迁移过程中上游源更新了一个系统库版本,跟我们的应用二进制不兼容,整批机器一重启就起不来。后来所有涉及源变更的项目,我都强制要求做快照,宁可多花半小时,也不赌“上游不会动”。
6.3 一些实用的部署细节
最后分享几个零散但很实用的细节。仓库服务器的主机名和 IP 最好通过内网 DNS 固定,客户端 repo 文件里用主机名而不是 IP,这方便将来迁移仓库服务器时客户端不用改配置。nginx 访问日志要长期保留,当客户端出现问题时,日志能很快定位是哪个仓库、哪个包下载失败。磁盘监控必须加,仓库目录一旦写满,故障会非常隐蔽——客户端表现是间歇性下载失败,时好时坏,很难直接想到是磁盘问题。
我做源环境部署最深的体会是:这活儿表面上就是同步几个仓库、写几行配置,但真正决定项目顺不顺的,往往是你对版本、密钥、权限、回滚这些“边角料”的把控。把这些基础细节做到位,国产化迁移后面的应用迁移和验收工作才会顺畅得多。