背景

EDA / CAD / HPC 集群的特点是:

  • 服务器数量从几十到上千;
  • 大量主机长期处于离线或半离线状态,只能访问内网;
  • 需要长期固定在某个 Y 流(如 RHEL 8.10、9.4),避免 dnf update 把基线偷偷推到 8.11;
  • 既跑 Rocky Linux 也跑 AlmaLinux,甚至同一机房两种发行版并存;
  • 业务上还要部署一批内部 RPM(license 监控、CAD 启动器、运维 agent 等)。

公网镜像(哪怕是国内高校镜像)都不能直接当作生产仓库使用——网络抖动一次 dnf makecache,就可能把上百台 LSF/SGE 计算节点同时打挂。本文给出一套在内网长期可维护的方案,同时容纳多个版本的 AlmaLinux 与 Rocky Linux,并且可以平滑扩展到 EPEL、内部 RPM 与未来的 EL10。


一、需求拆解与策略

1.1 仓库矩阵

以一个比较真实的企业基线为例,需要同时维护:

发行版 版本 架构 必要仓库
Rocky Linux 8.10 x86_64 BaseOS / AppStream / extras / PowerTools
Rocky Linux 9.4 x86_64 BaseOS / AppStream / extras / CRB
Rocky Linux 9.5 x86_64 BaseOS / AppStream / extras / CRB
AlmaLinux 8.10 x86_64 BaseOS / AppStream / extras / PowerTools
AlmaLinux 9.4 x86_64 BaseOS / AppStream / extras / CRB
EPEL 8 / 9 x86_64 Everything
内部 RPM el8/el9 x86_64 internal

注意几个差异点:

  • Rocky / AlmaLinux 的 EL8 系叫 PowerTools,EL9 系改名为 CRB(CodeReady Builder)。
  • EL8 / EL9 都没有独立 updates 仓库,更新会直接合入 BaseOS / AppStream。
  • AppStream 是模块化仓库,禁止用 createrepo_c 覆盖官方 repodata,否则会丢掉 modules.yaml

1.2 $releasever 的坑

Rocky 与 AlmaLinux 的默认 $releasever 是大版本号(89),公网镜像里 /8/ 通常是软链接,指向当前最新的 8.Y。如果要把仓库固定在 8.10,客户端 repo 一定要写绝对路径 /8.10/,不要写 $releasever,否则未来上游切到 8.11 时,你的”基线冻结”就被绕过了。

1.3 同步方式选择

方式 适用 备注
rsync 上游提供 rsync 模块(Rocky / AlmaLinux 官方都有) 首选,能完整保留 repodatamodules.yamlupdateinfo
dnf reposync 上游只提供 HTTP / HTTPS 需要 --download-metadata,否则丢模块化信息
dnf download 一次性补少量包 不适合做仓库镜像

本文以 rsync 为主,reposync 作为补充。


二、目录规划

把”同步落盘目录”和”Web 发布目录”统一在一个根目录下,按 发行版 / 版本 组织:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/tools/yumrepo/
├── rocky/
│   ├── 8.10/
│   │   ├── BaseOS/
│   │   ├── AppStream/
│   │   ├── extras/
│   │   └── PowerTools/
│   ├── 9.4/
│   └── 9.5/
├── almalinux/
│   ├── 8.10/
│   └── 9.4/
├── epel/
│   ├── 8/
│   └── 9/
├── internal/
│   ├── el8/x86_64/Packages/
│   └── el9/x86_64/Packages/
├── .snapshots/                    # 可选,快照原子切换使用
└── logs/

这种布局在客户端 repo 里就能简单地拼出 URL:

1
2
3
4
http://yumrepo.icinfra.local/rocky/8.10/BaseOS/x86_64/os/
http://yumrepo.icinfra.local/almalinux/9.4/AppStream/x86_64/os/
http://yumrepo.icinfra.local/epel/9/Everything/x86_64/
http://yumrepo.icinfra.local/internal/el8/x86_64/

后续无论新增 EL10、ARM 架构还是其它发行版,只要在第一层加目录即可,不会扰动现有客户端。

准备工作:

1
2
3
4
5
useradd -r -m -d /var/lib/repomirror -s /sbin/nologin repomirror
mkdir -p /tools/yumrepo/{rocky,almalinux,epel,internal,logs,.snapshots}
mkdir -p /var/lock/repomirror
mkdir -p /tools/common
chown -R repomirror:repomirror /tools/yumrepo /var/lock/repomirror

三、上游源选择

近源优先,公网链路抖动时也建议至少配置 1~2 个备份源:

发行版 推荐 rsync 源(示例)
Rocky rsync://mirror.nyist.edu.cn/rocky/
Rocky 备份 rsync://msync.rockylinux.org/rocky-linux/(官方 master)
AlmaLinux rsync://mirrors.tuna.tsinghua.edu.cn/almalinux/
AlmaLinux 备 rsync://rsync.repo.almalinux.org/almalinux/
EPEL rsync://mirrors.tuna.tsinghua.edu.cn/fedora/epel/

官方 master mirror 不建议直接打。Rocky / AlmaLinux 镜像管理文档都建议生产环境优先使用就近的二级镜像,并把同步频率控制在每天 4~6 次、避开整点,分散上游压力。


四、统一的 exclude 列表

EL8 / EL9 / EL10 共用同一份 exclude 文件,按”按需打开”思路写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
cat > /tools/common/el-exclude.list <<'EOF'
# === 镜像中常见的非仓库目录 ===
isos/
Live/
images/
isolinux/
EFI/
kickstart/

# === source / debug ===
*/source/
*/debug/
*/debuginfo/
*/debugsource/
*/Source/
*/Devel/

# === 不需要的架构 ===
*/aarch64/
*/ppc64le/
*/s390x/

# === 不需要的仓库(按需开关) ===
HighAvailability/
ResilientStorage/
NFV/
RT/
Plus/
SAP/
SAPHANA/

# === 临时文件 ===
*.tmp
*.~tmp~
.~tmp~/
EOF

如果用到 Pacemaker / Corosync / GFS2,把 HighAvailability/ResilientStorage/ 注释掉即可。


五、统一同步脚本:参数化驱动

不要给每个版本写一份脚本,让发行版和版本号变成参数,这样新增 9.5、9.6 时只改 cron / timer,不动脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
cat > /usr/local/sbin/sync-elrepo.sh <<'EOF'
#!/usr/bin/env bash
# Usage: sync-elrepo.sh <distro> <version> [extra rsync args...]
#   distro:  rocky | almalinux
#   version: 8.10 | 9.4 | 9.5 | ...
set -euo pipefail

DISTRO="${1:?distro required: rocky|almalinux}"
VERSION="${2:?version required: e.g. 8.10}"
shift 2 || true

case "$DISTRO" in
  rocky)
    SRC="rsync://mirror.nyist.edu.cn/rocky/${VERSION}/"
    ;;
  almalinux)
    SRC="rsync://mirrors.tuna.tsinghua.edu.cn/almalinux/${VERSION}/"
    ;;
  *)
    echo "unsupported distro: $DISTRO" >&2
    exit 2
    ;;
esac

DST="/tools/yumrepo/${DISTRO}/${VERSION}/"
EXCLUDE="/tools/common/el-exclude.list"
LOCK="/var/lock/repomirror/${DISTRO}-${VERSION}.lock"
LOG_DIR="/tools/yumrepo/logs"
LOG_FILE="${LOG_DIR}/${DISTRO}-${VERSION}-$(date +%F).log"

mkdir -p "$DST" "$LOG_DIR"

exec 9>"$LOCK"
if ! flock -n 9; then
  echo "$(date '+%F %T') [$DISTRO $VERSION] another sync running, skip." | tee -a "$LOG_FILE"
  exit 0
fi

echo "========== $(date '+%F %T') sync $DISTRO $VERSION start ==========" | tee -a "$LOG_FILE"

rsync -aHvi4 \
  --numeric-ids \
  --safe-links \
  --delete \
  --delete-delay \
  --delay-updates \
  --partial-dir=.rsync-partial \
  --exclude-from="$EXCLUDE" \
  "$@" \
  "$SRC" "$DST" 2>&1 | tee -a "$LOG_FILE"

# 清理半成品目录
find "$DST" -type d -name ".rsync-partial" -prune -exec rm -rf {} + || true

# 校验关键 repodata
fail=0
for repo in BaseOS AppStream extras PowerTools CRB; do
  d="$DST/$repo/x86_64/os"
  [[ -d "$d" ]] || continue
  if [[ ! -s "$d/repodata/repomd.xml" ]]; then
    echo "WARN: missing repomd.xml in $d" | tee -a "$LOG_FILE"
    fail=1
  fi
done

echo "========== $(date '+%F %T') sync $DISTRO $VERSION done (fail=$fail) ==========" | tee -a "$LOG_FILE"
exit "$fail"
EOF

chmod 0755 /usr/local/sbin/sync-elrepo.sh

调用示例:

1
2
3
4
sudo -u repomirror /usr/local/sbin/sync-elrepo.sh rocky     8.10
sudo -u repomirror /usr/local/sbin/sync-elrepo.sh rocky     9.4
sudo -u repomirror /usr/local/sbin/sync-elrepo.sh almalinux 8.10
sudo -u repomirror /usr/local/sbin/sync-elrepo.sh almalinux 9.4

几个细节:

  • 没有 -C 参数。-C 是 CVS 风格隐式排除,做仓库镜像时容易误伤合法文件,必须显式用 --exclude-from
  • -H 必须保留。EL 仓库内大量包之间存在硬链接,去掉 -H 会让磁盘膨胀一倍以上。
  • --delete-delay + --delay-updates:先把所有文件下完再原子地切换,避免客户端正好访问到只下了一半的 repodata
  • 每个 <distro>-<version> 用独立 lock 文件,可以多个版本并行同步,不会互相阻塞。

六、生产推荐:快照 + 软链原子切换

如果集群里有上千台机器同时 dnf makecache,rsync 期间哪怕一秒钟的 repodata 不一致都可能让客户端报 Status code: 404。这种规模建议用快照模式:

1
2
3
4
/tools/yumrepo/rocky/8.10 -> ../.snapshots/rocky-8.10-20260506-220000
/tools/yumrepo/.snapshots/
    ├── rocky-8.10-20260505-220000/
    └── rocky-8.10-20260506-220000/

脚本核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
cat > /usr/local/sbin/sync-elrepo-snapshot.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DISTRO="${1:?}"
VERSION="${2:?}"

BASE="/tools/yumrepo"
SNAP_DIR="${BASE}/.snapshots"
CURRENT="${BASE}/${DISTRO}/${VERSION}"
EXCLUDE="/tools/common/el-exclude.list"
LOCK="/var/lock/repomirror/${DISTRO}-${VERSION}.lock"
STAMP="$(date +%Y%m%d-%H%M%S)"
NEW_SNAP="${SNAP_DIR}/${DISTRO}-${VERSION}-${STAMP}"

case "$DISTRO" in
  rocky)     SRC="rsync://mirror.nyist.edu.cn/rocky/${VERSION}/" ;;
  almalinux) SRC="rsync://mirrors.tuna.tsinghua.edu.cn/almalinux/${VERSION}/" ;;
  *) echo "bad distro"; exit 2 ;;
esac

mkdir -p "$SNAP_DIR" "$(dirname "$CURRENT")" "$NEW_SNAP"

exec 9>"$LOCK"
flock -n 9 || { echo "skip: locked"; exit 0; }

PREV=""
[[ -L "$CURRENT" ]] && PREV="$(readlink -f "$CURRENT" || true)"

LD=()
[[ -n "$PREV" && -d "$PREV" ]] && LD=(--link-dest="$PREV")

rsync -aHvi4 --numeric-ids --safe-links \
  --delete --delete-delay --delay-updates \
  --partial-dir=.rsync-partial \
  --exclude-from="$EXCLUDE" \
  "${LD[@]}" \
  "$SRC" "$NEW_SNAP/"

# 关键 repodata 校验
for r in BaseOS AppStream extras PowerTools CRB; do
  d="$NEW_SNAP/$r/x86_64/os"
  [[ -d "$d" ]] && test -s "$d/repodata/repomd.xml"
done

ln -sfn "$NEW_SNAP" "$CURRENT"

# 只保留最近 7 个快照
find "$SNAP_DIR" -maxdepth 1 -type d -name "${DISTRO}-${VERSION}-*" \
  | sort | head -n -7 | xargs -r rm -rf
EOF
chmod 0755 /usr/local/sbin/sync-elrepo-snapshot.sh

--link-dest 让相邻两次快照之间未变化的 RPM 通过硬链接共享 inode,磁盘占用几乎不增长——这是把”快照”变成生产可行方案的关键。


七、systemd timer 调度

每个 <distro>-<version> 用一个 timer,错峰执行,避免同时打满上游:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
cat > /etc/systemd/system/sync-elrepo@.service <<'EOF'
[Unit]
Description=Sync EL repository %i
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=repomirror
Group=repomirror
# %i 形如 rocky-8.10
ExecStart=/bin/bash -c '/usr/local/sbin/sync-elrepo.sh $(echo %i | cut -d- -f1) $(echo %i | cut -d- -f2-)'
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
TimeoutStartSec=12h
EOF

cat > /etc/systemd/system/sync-elrepo@.timer <<'EOF'
[Unit]
Description=Periodic sync for EL repository %i

[Timer]
OnCalendar=*-*-* 02,06,10,14,18,22:25:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now sync-elrepo@rocky-8.10.timer
systemctl enable --now sync-elrepo@rocky-9.4.timer
systemctl enable --now sync-elrepo@almalinux-8.10.timer
systemctl enable --now sync-elrepo@almalinux-9.4.timer

systemctl list-timers --all | grep elrepo

RandomizedDelaySec=30m 能让多个 timer 自动错开,避免同一秒拉同一个上游。

手动触发:

1
2
systemctl start sync-elrepo@rocky-9.4.service
journalctl -u sync-elrepo@rocky-9.4.service -f

八、HTTP 发布:Nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
dnf install -y nginx policycoreutils-python-utils

cat > /etc/nginx/conf.d/yumrepo.conf <<'EOF'
server {
    listen 80;
    server_name yumrepo.icinfra.local;

    root /tools/yumrepo;

    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;

    # 仓库元数据不应被中间代理长时间缓存
    location ~* /repodata/ {
        add_header Cache-Control "no-cache, max-age=0";
    }

    # RPM 包可以放心缓存
    location ~* \.(rpm)$ {
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
    }

    access_log /var/log/nginx/yumrepo_access.log;
    error_log  /var/log/nginx/yumrepo_error.log;
}
EOF

semanage fcontext -a -t httpd_sys_content_t "/tools/yumrepo(/.*)?"
restorecon -Rv /tools/yumrepo

firewall-cmd --add-service=http --permanent && firewall-cmd --reload
systemctl enable --now nginx

烟雾测试:

1
2
3
curl -I http://yumrepo.icinfra.local/rocky/8.10/BaseOS/x86_64/os/repodata/repomd.xml
curl -I http://yumrepo.icinfra.local/rocky/9.4/AppStream/x86_64/os/repodata/repomd.xml
curl -I http://yumrepo.icinfra.local/almalinux/8.10/BaseOS/x86_64/os/repodata/repomd.xml

九、客户端 repo 文件

9.1 Rocky 8.10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# /etc/yum.repos.d/Rocky-Local-8.10.repo
[local-baseos]
name=Rocky Linux 8.10 - BaseOS - Local
baseurl=http://yumrepo.icinfra.local/rocky/8.10/BaseOS/$basearch/os/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-8

[local-appstream]
name=Rocky Linux 8.10 - AppStream - Local
baseurl=http://yumrepo.icinfra.local/rocky/8.10/AppStream/$basearch/os/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-8

[local-extras]
name=Rocky Linux 8.10 - extras - Local
baseurl=http://yumrepo.icinfra.local/rocky/8.10/extras/$basearch/os/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-8

[local-powertools]
name=Rocky Linux 8.10 - PowerTools - Local
baseurl=http://yumrepo.icinfra.local/rocky/8.10/PowerTools/$basearch/os/
enabled=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-8

9.2 Rocky 9.4(注意 PowerTools 改名为 CRB)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# /etc/yum.repos.d/Rocky-Local-9.4.repo
[local-baseos]
name=Rocky Linux 9.4 - BaseOS - Local
baseurl=http://yumrepo.icinfra.local/rocky/9.4/BaseOS/$basearch/os/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9

[local-appstream]
name=Rocky Linux 9.4 - AppStream - Local
baseurl=http://yumrepo.icinfra.local/rocky/9.4/AppStream/$basearch/os/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9

[local-crb]
name=Rocky Linux 9.4 - CRB - Local
baseurl=http://yumrepo.icinfra.local/rocky/9.4/CRB/$basearch/os/
enabled=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9

9.3 AlmaLinux 9.4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /etc/yum.repos.d/Alma-Local-9.4.repo
[local-baseos]
name=AlmaLinux 9.4 - BaseOS - Local
baseurl=http://yumrepo.icinfra.local/almalinux/9.4/BaseOS/$basearch/os/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9

[local-appstream]
name=AlmaLinux 9.4 - AppStream - Local
baseurl=http://yumrepo.icinfra.local/almalinux/9.4/AppStream/$basearch/os/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9

9.4 禁用默认公网源

1
2
3
mkdir -p /etc/yum.repos.d/backup
mv /etc/yum.repos.d/Rocky-*.repo     /etc/yum.repos.d/backup/ 2>/dev/null || true
mv /etc/yum.repos.d/almalinux*.repo  /etc/yum.repos.d/backup/ 2>/dev/null || true

或者更稳妥的方式:保留但禁用。

1
dnf config-manager --set-disabled baseos appstream extras powertools crb 2>/dev/null || true

9.5 客户端验证

1
2
3
4
5
6
dnf clean all
dnf makecache
dnf repolist -v
dnf module list           # 必须能列出 AppStream 模块流
dnf updateinfo summary    # 必须能看到 RHSA / RHBA / RHEA 计数
dnf update --assumeno

如果 dnf module list 为空、updateinfo summary 是 0,几乎可以肯定上游 repodata 没完整同步,或者你手动跑过 createrepo_c 把官方 metadata 覆盖了。这是后续排障的两个最大坑点。


十、reposync 备用方案

少数上游(例如某些第三方仓库、商业产品自带的 yum 源)只提供 HTTPS,没有 rsync 模块,这时改用 dnf reposync

1
2
3
4
5
6
7
8
9
10
11
dnf install -y dnf-plugins-core createrepo_c

# 先在镜像服务器本地配置好上游 repo(仅本机使用)
# 然后:
dnf reposync \
  --repoid=baseos --repoid=appstream --repoid=extras --repoid=crb \
  --download-path=/tools/yumrepo/reposync/rocky9.4 \
  --download-metadata \
  --delete \
  --remote-time \
  --arch=x86_64 --arch=noarch

要点:

  • 必须加 --download-metadata,否则只下载 RPM、丢掉 modules.yamlupdateinfo.xml,AppStream 模块化机制会全部失效。
  • --delete 让本地与上游保持一致,移除已经被上游下架的包。
  • 同步出来的目录可以直接当 yum repo 用,不需要再跑 createrepo_c

十一、内部 RPM 仓库

公司自研工具(CAD 启动器、license agent、内部 Python 工具链等)应该走独立目录,与上游镜像隔离开:

1
2
3
4
5
6
7
8
mkdir -p /tools/yumrepo/internal/el8/x86_64/Packages
mkdir -p /tools/yumrepo/internal/el9/x86_64/Packages

cp build-output/*.el8.*.rpm /tools/yumrepo/internal/el8/x86_64/Packages/
cp build-output/*.el9.*.rpm /tools/yumrepo/internal/el9/x86_64/Packages/

createrepo_c --update --retain-old-md=2 /tools/yumrepo/internal/el8/x86_64
createrepo_c --update --retain-old-md=2 /tools/yumrepo/internal/el9/x86_64

--update 会复用未变包的旧 metadata,对几千个 RPM 的内部仓库可以把元数据生成时间从分钟级降到秒级。

客户端 repo:

1
2
3
4
5
6
7
# /etc/yum.repos.d/Internal-EL.repo
[internal-el8]
name=Internal RPM Repository - EL8
baseurl=http://yumrepo.icinfra.local/internal/el8/$basearch/
enabled=1
gpgcheck=1
gpgkey=http://yumrepo.icinfra.local/internal/RPM-GPG-KEY-icinfra

不要把 gpgcheck=0 当作”图省事”的默认值。即使是内部 RPM,签名也是阻止”运维同事手滑把恶意/损坏的包丢进仓库导致全网中招”的最后一道闸。


十二、ACL:让某些 repo 只对特定网段开放

EDA license agent、HPC 调度器 agent 这类内部包,往往不希望对全公司开放。Nginx 上做一层 IP ACL 就够:

1
2
3
4
5
location /internal/ {
    allow 10.20.0.0/16;     # EDA 集群
    allow 10.30.0.0/16;     # HPC 集群
    deny  all;
}

如果有更细粒度的需求(例如按用户、按 client cert),再考虑接入企业 SSO,但这超出本文范围。


十三、磁盘容量与保留策略

经验值(仅供参考):

仓库 单版本 x86_64 占用
Rocky / Alma 8.x BaseOS+AppStream+PowerTools 约 70~90 GB
Rocky / Alma 9.x BaseOS+AppStream+CRB 约 60~80 GB
EPEL 8 / 9 Everything 约 80~120 GB
7 份快照(依靠 hardlink) ~ 1.1×–1.3× 单份大小

实际部署时建议:

  • 生产仓库放在独立 LV 或独立挂载点上,至少预留 1 TB;
  • 每次同步后跑一遍 du -sh 写入 metric 系统,触发容量告警;
  • 快照保留 7 份,足以回滚一周内的任何 baseline 漂移。

十四、常见问题排查

14.1 Error: Failed to download metadata for repo 'xxx'

99% 是这两个原因之一:

  • rsync 还没跑完,客户端正好访问到半成品 repodata:把 --delay-updates 加上,或者改用快照原子切换;
  • 客户端 repo 里写了 $releasever,而上游 /8/ 软链接此时指向了一个尚未完整同步的 8.Y。

14.2 dnf module list 为空

上游 repodata/*.modules.yaml.gz(或新格式 *.modulemd.yaml.gz)没有同步过来。检查:

  • exclude 列表是不是把 */repodata/ 误排除了;
  • 是不是手工跑过 createrepo_c /path/to/AppStream/x86_64/os,把官方带模块化信息的 metadata 覆盖了——这个动作对 AppStream 是破坏性的,恢复办法是重新 rsync 一次。

14.3 客户端 GPG 校验失败

1
The GPG keys listed for the "Rocky Linux 8.10 - BaseOS - Local" repository are already installed but they are not correct for this package.

通常是 gpgkey 指向了错误的版本。Rocky 8 / Rocky 9 / AlmaLinux 8 / AlmaLinux 9 各有独立的 key 文件,不要混用

1
2
3
4
/etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-8
/etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9
/etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-8
/etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9

14.4 同步后磁盘占用比预期大很多

最常见的原因是少了 -H,导致硬链接被展开成独立文件。检查:

1
2
rsync -aHvi4 ...   # 必须有 H
du -sh /tools/yumrepo/rocky/8.10

十五、最终落地清单

按本文方案,最终能拿到的能力:

  • 一台镜像服务器同时承载 Rocky 8.10 / 9.4 / 9.5 + AlmaLinux 8.10 / 9.4 + EPEL 8 / 9 + 内部 RPM
  • 客户端 repo 通过 /<distro>/<version>/... URL 就能定位到任意基线,不依赖 $releasever,可固定 Y 流;
  • AppStream 模块化、updateinfo、GPG 校验全部可用;
  • 同步靠 systemd timer,每天 6 次错峰,不打爆上游;
  • 生产可选快照原子切换 + --link-dest,磁盘不爆,客户端零中断;
  • 内部 RPM 与上游隔离,createrepo_c --update 增量更新,敏感仓库可按网段做 ACL。

后续要扩展时只需要:

  1. 新增上游版本 → systemctl enable --now sync-elrepo@<distro>-<version>.timer
  2. 新增发行版(比如 EL10 出来)→ 在 sync-elrepo.shcase 里加一条上游 URL;
  3. 新增内部包 → 丢到 internal/el8/x86_64/Packages/,跑一次 createrepo_c --update

整套体系的”演进成本”被压到了配置层面,而不是脚本层面,这是企业内网仓库长期可维护的关键。