
最近接手了一个国企客户的项目要求把现有业务系统从旧平台逐步迁移到国产化技术栈。刚开始还挺顺利直到他们提了一个需求内网服务器全部离线不能连外网但开发测试环境还要持续部署新软件包。当时就意识到源环境的问题不解决整个迁移进度都会卡住。于是就有了这个“国产化迁移-CentOS 9 源环境部署”项目。这篇内容面向的读者很明确正在做国产化替代、CentOS 迁移、内网环境运维的工程师或者被“服务器连不上外网但还要装软件”这个问题折磨过的人。我会把整个部署过程完整拆解包含选型理由、同步策略、客户端配置、常见故障排查全程用实际操作说话。1. 为什么国产化迁移时一定要把“源环境”单独当项目做很多人觉得源环境不就是一台服务器上装个nginx、然后把仓库文件丢进去吗。实际做过才发现这个环节的坑比想象中多得多尤其是在国产化迁移这种特定场景下它承担的角色远超“软件下载服务器”本身。1.1 迁移场景下的特殊约束条件国产化迁移项目通常面对的是内网隔离环境安全等保基本是三级起跳。这就带来一个现实约束生产网段不能直接访问公网但业务系统要安装补丁、部署中间件、安装数据库客户端这些都需要操作系统软件源的支持。没有源环境所有机器都成了孤岛。同时迁移本身是一个渐进过程。新系统上线初期开发、测试、生产环境并行运转每个环境都需要安装依赖包。如果每次都在一台机器上手动rpm安装然后到处拷效率极低而且版本会迅速漂移。源环境能把“包管理”这件事标准化让所有机器从同一个仓库获取一致版本这对后续的配置管理、自动化运维是地基级的前置条件。1.2 为什么选中 CentOS Stream 9 作为源的基础系统既然要做国产化迁移为什么会选 CentOS Stream 9而不是干脆上一个国产系统这里有几个实际考量。首先当前国内大量存量服务器仍然基于 CentOS 系的生态很多业务软件的兼容性验证也是基于这个体系完成的。国产化替代过程不是一蹴而就中间需要过渡系统。CentOS Stream 9 与 RHEL 9 高度同源软件包的体系结构、编译参数、目录规范几乎一致用它对存量 CentOS 7/8 的运维习惯冲击最小。其次阿里云、清华 TUNA 等镜像站都提供了 CentOS Stream 9 的完整仓库同步速度快带宽稳定不容易出现同步到一半断流的问题。这对于源服务器初始建立仓库时特别重要因为首次同步的数据量动辄几十 GB如果上游镜像不稳定整个同步过程会非常痛苦。不过要明确一点CentOS Stream 9 是滚动发布的中间版本它的软件包更新节奏比传统 CentOS 快也更激进。生产环境如果追求极致稳定建议基于它再锁版本。但这不影响它作为源服务器的底层系统因为源服务器本身对实时性要求不高重要的是仓库内容覆盖完整、客户端能正常解析。1.3 源服务器在迁移项目里的整体定位在这个项目里源服务器承担了三个角色软件分发中心、版本标准基线、补丁更新入口。软件分发中心是最基础的功能内网所有机器的 yum/dnf 都指向它任何机器需要新软件包直接 install 就行。版本标准基线意味着你们团队在哪个时间点同步了仓库那么全网的机器都以这个时间点的包集合为准不会出现开发机用了新版本、生产机还是旧版本的割裂场景。补丁更新入口则是说后续需要修复 CVE 漏洞时只要在源服务器上更新仓库全网客户端通过增量刷新就能获取安全补丁。这个定位想清楚之后后面的所有部署动作都有了判断依据磁盘怎么规划、同步策略怎么定、客户端怎么配置全部围绕这三个角色展开。2. 部署前的环境准备与关键选型对比部署源环境其实不复杂但如果前期准备不到位后面全是坑。按照我的习惯先把环境规划好再动手。2.1 硬件与操作系统基础规划源服务器不需要很高的配置核心瓶颈在磁盘和网络。CPU2 核足够同步和压缩元数据时略有负载但不会持续高占用。内存4 GB 起步如果仓库数量多、客户端并发高建议 8 GB。内存太小会在 createrepo 和 dnf 客户端并发请求时出现卡顿。磁盘这是重点。CentOS Stream 9 的 BaseOS、AppStream、Extras 仓库全量同步大概需要 60~80 GB如果还要同步 CRBCodeReady Builder仓库再加 30 GB。建议至少准备 200 GB 的独立数据盘给增长留余量。网络同步阶段需要能访问外网镜像源同步完成后可以断外网内网网卡负责服务分发。操作系统我建议直接选 CentOS Stream 9 最小化安装不装图形界面保持纯净。安装时磁盘分区把 /var 单独分区因为源仓库数据默认会放在 /var 下其实我们后续会改到数据盘这样可以避免系统盘和数据盘互相挤占。2.2 Web 服务形态nginx 还是 httpd源仓库对外提供服务无非是 HTTP 或 HTTPS。最常见的选择是 nginx 和 httpd我推荐 nginx理由有三点nginx 的并发处理能力更强当内网有几十上百台机器同时跑 dnf update 时nginx 的连接处理比 httpd 稳。nginx 配置简单直观一个 server 块就能搞定仓库的根目录映射、目录浏览、gzip 压缩等。nginx 的访问日志格式更易读后续做仓库访问统计、异常请求排查都很方便。当然用 httpd 也没有问题关键是配置上要开启目录浏览和 follow symlinks否则客户端访问目录索引时会 404。下面的配置都基于 nginx 展开。2.3 同步方案的取舍直接镜像还是自定义仓库这里要做一个非常重要的决策是把上游仓库完整镜像下来还是只同步需要的包做自定义仓库。完整镜像的优点是简单、干净客户端可以像使用官方源一样自由安装任何包。缺点是数据量大、磁盘占用高、同步耗时长。自定义仓库的优势是只需要同步项目依赖的软件包数据量可能只有几百 MB非常轻量。缺点是需要自己维护包列表新环境验证时如果缺了某个依赖还得回去补包再同步。对于国产化迁移项目我的建议是第一步先做完整镜像把 BaseOS 和 AppStream 两个核心仓库全量同步下来。原因很简单迁移阶段环境不确定性高开发测试人员随时可能安装新包完整仓库能降低沟通成本。当系统稳定之后再考虑裁剪仓库只保留经过认证的包集合。2.4 同步工具与上游源选择对比同步上游仓库主力工具是 reposync配合 upstream 镜像站。这里我整理一个对比表方便大家选择对比项阿里云镜像清华 TUNA 镜像官方 CentOS 镜像国内访问速度极快带宽充足快带宽稳定慢经常超时仓库完整性完整含各架构完整完整同步策略支持 rsync 协议支持 rsync 协议支持 rsync但连接不稳经验推荐最推荐备选不推荐尤其大数据量实际执行中我首选阿里云镜像的 rsync 端点原因在于国内链路质量更稳定。reposync 本身支持从 http/https 的 repo 地址同步也可以通过配置本地 repo 文件指向镜像站的仓库 URL然后执行 reposync -r 仓库名。同步时还有一个不能忽略的参数--download-metadata。从 CentOS Stream 9 开始reposync 默认会下载仓库原生的 metadata包括 filelists、group 等。如果你用的是 dnf 客户端建议保留 --download-metadata否则后续运行时 dnf 会试图从上游重新拉 metadata内网客户端访问会失败。这个细节非常容易被忽略后面客户端报 “Error determining repository URL” 之类的错时很多人一脸蒙。3. 源服务基础设施搭建从 nginx 到仓库目录结构做完了选型和规划这部分开始落地。我按实际执行的顺序把关键环节都过一遍包括 nginx 安装、目录规划、仓库同步脚本、定时任务。3.1 nginx 安装与仓库站点配置用 dnf 安装 nginx 就不赘述了重点说配置。源服务器上配置如下server { listen 80; server_name repo.internal.local; root /data/repos; autoindex on; autoindex_exact_size off; autoindex_localtime on; location / { try_files $uri $uri/ 404; } access_log /var/log/nginx/repo_access.log; error_log /var/log/nginx/repo_error.log; }这段配置里有几个细节autoindex on 开启目录浏览没有它客户端虽然能通过 dnf 正常拉文件但你在浏览器里定位某个包是否存在时会非常不方便。autoindex_exact_size off 让目录列表显示文件大小而不是字节数排查同步完整性时更直观。root /data/repos 指向数据盘上的仓库根目录而不是默认的 /usr/share/nginx/html。这也是规划阶段就预留好的。配置完成后执行 nginx -t 校验语法然后 systemctl enable --now nginx 启动并设置开机自启。3.2 仓库目录结构与多仓库并存设计源服务器上不会只有一个仓库后续可能还要放 EPEL、本地 RPM 包目录、自定义软件包目录。所以目录结构要提前设计好。我的经验是统一放在 /data/repos 下用仓库名作为子目录名。/data/repos/ ├── centos-stream/ │ ├── 9-stream/ │ │ ├── BaseOS/ │ │ ├── AppStream/ │ │ └── Extras/ │ └── 9-stream-crb/ │ └── CRB/ ├── epel/ │ └── 9/ └── custom/ └── noarch/这样的分层有好处客户端在 .repo 文件里可以写多个 baseurl当某个仓库不存在时 dnf 会自动尝试下一个而且结构清晰磁盘使用统计也方便du -sh 就能看到每个仓库占了多少。3.3 数据盘挂载与目录迁移安装系统时如果已经分好数据盘挂载即可如果没分需要现做逻辑卷。这个步骤不复杂但有一个点要特别注意仓库同步是一个持续数小时甚至一整天的过程如果数据盘文件系统出问题导致同步中断重来一遍很浪费时间。因此我建议数据盘用 XFS 文件系统这是 CentOS 默认也是比较稳的选择。如果你是系统装完后才发现仓库才 2 GB 的小仓库放系统盘也行。但既然要面对几十 GB 的仓库建议还是按正规流程来mkfs.xfs /dev/sdb1 mkdir -p /data mount /dev/sdb1 /data echo /dev/sdb1 /data xfs defaults 0 0 /etc/fstab3.4 仓库同步脚本的完整实现同步脚本是整个源环境的核心组件。我写了一个脚本结合 reposync 和 createrepo 的流程做增量同步。先看脚本主体#!/bin/bash # ------------------------------------------------------------ # 源仓库同步脚本 - CentOS Stream 9 # 适用: 内网源服务器 # ------------------------------------------------------------ set -e REPO_BASE/data/repos/centos-stream/9-stream UPSTREAM_BASEhttps://mirrors.aliyun.com/centos-stream/9-stream LOG_FILE/var/log/repo-sync/centos9-sync.log LOCK_FILE/var/lock/repo-sync-centos9.lock mkdir -p $(dirname $LOG_FILE) $(dirname $LOCK_FILE) # 防重入锁 if [ -f $LOCK_FILE ]; then echo sync already running, exit $LOG_FILE exit 0 fi touch $LOCK_FILE trap rm -f $LOCK_FILE EXIT # 同步 BaseOS reposync \ --repoidBaseOS \ --download-path$REPO_BASE \ --download-metadata \ --newest-only \ --archx86_64 \ --source \ --urls \ --retries3 \ $LOG_FILE 21 # 同步 AppStream 同理下面解释几个关键参数的选择理由--newest-only 表示只同步最新版本去掉历史旧包。这能显著减少磁盘占用和同步时间。对于源环境来说历史版本只有在极少数回滚场景才有价值一般情况下可以舍弃。--archx86_64 限定架构。除非你的内网还有 aarch64 架构机器否则不要把多架构的都同步下来磁盘消耗直接翻倍。--download-metadata 把仓库的元数据下载下来客户端在使用时可以免于额外请求。至于是否启用 --source我个人建议第一轮同步不要同步源码包源码包体积巨大且绝大多数场景用不到。等项目稳定后再决定是否补充。同步完成之后还需要执行一次 createrepo 来生成仓库索引如果 reposync 已经在本地生成了元数据则这一步可跳过但增量场景下建议统一跑一次以刷新createrepo --update $REPO_BASE/BaseOS createrepo --update $REPO_BASE/AppStream这个步骤的作用是告诉 dnf 客户端“仓库有哪些包、它们之间的依赖关系是什么”。不过要注意--update 只会在已有元数据时有效首次同步请去掉 --update 参数。最后把脚本挂到 crontab 里0 3 * * 0 /usr/local/bin/sync-repos9.sh每周日凌晨三点执行一次。避开白天业务高峰同时也给同步过程足够的时间窗口。4. 客户端配置与仓库切换的实际操作源服务器就绪后剩下的就是让内网所有机器“认”这个源。这部分看起来简单其实涉及不少细节尤其是替换默认源时的备份与验证。4.1 客户端仓库配置模板配置客户端前建议先备份原始源。很多人在内网环境配完自定义源之后发现 dnf update 报错想回退却发现原始源文件已经没了这时才拍大腿。这里有一个最简单的备份命令mkdir -p /etc/yum.repos.d/backup mv /etc/yum.repos.d/CentOS-*.repo /etc/yum.repos.d/backup/随后创建新的 repo 文件以 CentOS-Stream-9.repo 为例[BaseOS] nameCentOS Stream 9 - BaseOS baseurlhttp://repo.internal.local/centos-stream/9-stream/BaseOS/x86_64/os/ enabled1 gpgcheck0 [AppStream] nameCentOS Stream 9 - AppStream baseurlhttp://repo.internal.local/centos-stream/9-stream/AppStream/x86_64/os/ enabled1 gpgcheck0这里 gpgcheck0 是一个值得讨论的点。生产环境我非常不推荐关闭 GPG 校验因为包签名校验是软件供应链安全的重要防线。但国产化迁移项目中经常遇到的情况是内网源服务器没有配置导入 GPG 公钥或者密钥没有随着仓库元数据一起被客户端读取到。于是客户端在安装时就会报 GPG 签名验证失败。解决方案有两个一是把上游 GPG 公钥下载到源服务器然后在客户端 repo 配置里用 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial 来指定二是在同步时确保 repomd.xml.key 也被同步下来。如果实在嫌麻烦在可控内网环境里临时将 gpgcheck0 也可以接受但一定要在建好信任链之后尽快恢复开启。4.2 dnf 命令的行为验证配置完成后先执行dnf clean all dnf repolist如果输出的仓库状态显示“启用”的仓库数量符合预期说明基本通道已经通了。接着可以做个真实测试dnf install -y nginx如果这台客户端原本没有 nginx执行安装后能顺利拉到包并完成安装说明依赖解析、元数据传入、文件下载都正常。实际运维里我更推荐用一个更冷门的包做测试比如 dnf install -y sysstat 或者 dnf install -y lrzsz因为这类包在某些环境里不会被预装能真正走一遍完整安装流程。4.3 多仓库优先级与预配置环境当源服务器上存在多个仓库比如 BaseOS、AppStream、EPEL、自定义仓库时需要注意优先级设置。默认情况下dnf 在解析依赖时是同时考虑所有 enabled 仓库的如果自定义仓库里有一个包版本较旧而它又是某软件的依赖dnf 可能选中旧版本导致后续安装出现问题。一个可行的做法是给核心仓库设置优先级。安装 dnf-plugin-priorities 之后在 repo 配置文件里添加priority1数字越小优先级别越高。把 BaseOS 和 AppStream 设为优先级 1自定义仓库设为优先级 90这样 dnf 会优先从官方同步源安装包自定义仓库只用做补充避免版本污染。对于重复包的情况还有一个压箱底的经验在自定义仓库里尽量只放官方源里没有的包同名包不要包含否则排查依赖冲突时会非常头疼。5. 排障实录我踩过的几个比较隐蔽的坑写这一节是希望大家能绕开我踩过的雷。整个部署过程中有几个问题非常隐蔽报错信息也容易误导人。5.1 元数据时机引发的 404 错误现象是客户端执行 dnf update 时报错“Error: Failed to download metadata for repo AppStream: Cannot prepare internal mirrorlist: No URLs in mirrorlist”。这个报错看着像网络问题实际上罪魁祸首是仓库同步时没有下载 metadata或者下载了但位置不对。解决方法是回到源服务器进入仓库目录检查 repodata 目录是否存在ls /data/repos/centos-stream/9-stream/AppStream/x86_64/os/repodata/如果发现 repodata 目录为空那就重新执行一次 reposync并且一定要加上 --download-metadata。如果 repodata 非空但客户端仍报错检查客户端的 baseurl 是否精确指向了 repodata 所在的父目录因为 dnf 会主动去 baseurl 下找 repodata/repomd.xml路径多一层少一层都会失败。5.2 全量与增量同步引起的仓库损坏reposync 默认是全量同步但在二次同步时如果不小心删除了旧目录很可能会把仓库状态搞坏。这里最典型的坑是同步脚本里没有加 --download-metadata但第二次同步设置了此时 dnf 客户端会看到两份不同的元数据一个可能不完整导致客户端在安装时解析依赖失败。我的做法是所有同步统一加 --download-metadata并定期对仓库执行 createrepo --update。这样元数据始终是重新生成的状态可控。5.3 客户端缓存与旧源残留的混合问题很多内网机器是从 CentOS 7 直接升级或迁移过来的/etc/yum.repos.d 下面还残留着旧版 CentOS 7 的源文件。如果只是简单新增了一个新源文件旧源文件仍然存在且 enabled1dnf 会尝试访问已经失效的 mirrorlist导致解析超时整体更新速度极慢。处理方式是迁移过程中写一个脚本统一清理旧源、导入新源。脚本路径可以放在配置管理工具里如 Ansible、SaltStack保证所有机器进入一致状态。这个环节看似小但批量执行时能省下大量排查时间。5.4 从系统盘迁移仓库目录踩过的坑我最初搭建源时图省事仓库目录放在了系统盘 /var/repos 下。结果同步跑完、客户端开始安装包时系统的根分区被仓库数据撑爆直接导致系统异常连 dnf 都跑不了。后来不得不把仓库搬到数据盘。这次经历给我的教训是仓库目录从一开始就放在独立数据盘不要在系统盘上试探。如果你的源服务器已经建好但目录在系统盘迁移操作如下mkdir -p /data/repos rsync -av /var/repos/ /data/repos/然后修改 nginx 的 root 指向并重载。别忘记更新 nginx 配置和仓库同步脚本里的路径否则下次同步又写回系统盘了。6. 仓库验证与维护的日常工作模式源环境建立初期很顺利不代表后续就一直顺利。仓库维护是一个持续过程我养成了几个比较有效的工作习惯可以分享给大家。6.1 定时校验仓库状态每月做一次仓库健康检查主要看三点repodata 时间戳、关键软件包的版本、磁盘空间变化。我会写一个简单的巡检脚本对比同一天之前的状态自动发提醒#!/bin/bash # 仓库健康巡检 REPO_ROOT/data/repos LOG_FILE/var/log/repo-check.log date $LOG_FILE for repo in BaseOS AppStream; do dir$REPO_ROOT/centos-stream/9-stream/$repo/x86_64/os if [ -f $dir/repodata/repomd.xml ]; then echo $repo repodata OK $LOG_FILE else echo $repo repomd.xml MISSING $LOG_FILE fi done df -h /data $LOG_FILE有巡检意识之后再遇到问题至少知道什么时候开始坏的不至于连排查起点都没有。6.2 增量同步时保持仓库干净前面提到过同步策略是每周一次全量、增量结合。增量同步确实能节省很多带宽和时间但也容易留下大量旧包占磁盘。建议每三个月做一次全量重建流程删除仓库目录、重新同步、重新生成元数据。这样能清理掉积攒的 RPM 旧包和残缺缓存文件退回一个干净基线。7. 项目交付后的维护建议与个人体会源环境上线不是终点只是迁移的基础条件达标了。项目交付后我总结了几条比较现实的维护建议供大家参考。首先建立源服务器的监控告警。仓库磁盘用尽是最容易发生的故障建议将磁盘使用率、仓库目录大小、最近同步时间三个指标纳入监控。因为有同步脚本的存在只要记录同步日志时间戳就能判断同步是否卡住。一个简单的做法是每天看同步日志的更新时间如果三天都没有更新就可以报警了。其次尽量保持所有客户端在同一个仓库版本线。源环境建立好之后开发人员在测试机上手动 dnf install 了某个最新版软件包却在生产机上报找不到包这类问题大概率是源仓库没同步该包到最新版。让所有人理解“统一源、统一版本”的意义非常关键否则源环境形同虚设。最后我建议后续把源仓库与自动化配置管理工具联动起来。比如在 Ansible 的 playbook 里直接写好源配置、缓存清理动作新机器交付时跑一轮 playbook 就能把源环境接入工作完成。这不复杂但能把很零碎的运维步骤沉淀成标准化资产对团队扩编和新人上手都有很大帮助。实际落地过程中我最大的感受源环境表面上只有一台服务器和一个脚本但它牵涉到全网的软件供应链稳定性与一致性。前期多花点时间把目录规划、同步策略、客户端配置这些设计做好比后期反复救火要划算得多。