说真的,当我第一次听说要把RHEL 8升到AlmaLinux 9的时候,我的第一反应是:这能行吗?
毕竟从RHEL 8到9,中间跨越了整整一个大版本。而且RHEL和AlmaLinux虽然是”同源”——都是红帽系的——但真要动真格的迁移,那可是要脱层皮的。
我是2024年10月份动手的,到现在已经跑了三个生产环境。踩过的坑、摔过的跟头,现在回忆起来还在手心冒汗。今天就把这个过程掰开揉碎了讲给你听。
一、为什么要做这个迁移
先说动机。RHEL 8的支持周期到2029年10月,这听起来还挺远对吧?但问题在于:
- RHEL 8用的glibc是2.28,而AlmaLinux 9直接用glibc 2.34
- RHEL 8的Python是3.6,AlmaLinux 9是3.9
- RHEL 8的MySQL默认是8.0,但很多老应用还在用5.7
- 更重要的是,我手里有十几台服务器,RHEL的授权费每年都在涨
AlmaLinux 9给了一个看起来非常诱人的方案:它声称可以直接从RHEL 8升级上来。它的官网文档里甚至有个工具叫almalinux-deploy,号称能自动化这个过程。
听起来很美好,对吧?
二、准备工作——别跳过这一步
很多教程到这里就跳过了,但我必须强调:准备工作做得好不好,直接决定了你是笑着收工还是哭着重写服务器。
2.1 备份!备份!备份!
我的血泪教训:第一次迁移的时候,我觉得”反正有快照”,就没做全量备份。结果升级中途出问题,快照恢复失败,数据丢了两天。从那以后,我养成了一个习惯:
# 全系统备份脚本 - 请在使用前自行修改路径
#!/bin/bash
# 文件名:pre-migration-backup.sh
BACKUP_DIR="/backup/almalinux9-migration-$(date +%Y%m%d)"
DATE=$(date +%Y%m%d-%H%M%S)
echo "=== 开始备份,时间:$DATE ==="
echo "目标目录:$BACKUP_DIR"
# 1. 系统配置备份
mkdir -p "$BACKUP_DIR/etc"
cp -a /etc/* "$BACKUP_DIR/etc/" 2>/dev/null
# 2. 关键服务配置
echo "[*] 备份Apache/Nginx配置..."
if systemctl is-active --quiet httpd; then
cp -a /etc/httpd "$BACKUP_DIR/"
fi
if systemctl is-active --quiet nginx; then
cp -a /etc/nginx "$BACKUP_DIR/"
fi
# 3. 数据库备份
echo "[*] 备份MySQL/MariaDB数据..."
if systemctl is-active --quiet mysqld; then
mkdir -p "$BACKUP_DIR/mysql"
mysqldump --all-databases --single-transaction --routines --triggers > "$BACKUP_DIR/mysql/all-databases.sql"
fi
# 4. 应用数据备份
echo "[*] 备份应用数据..."
if [ -d /var/www/html ]; then
mkdir -p "$BACKUP_DIR/www"
rsync -av /var/www/html/ "$BACKUP_DIR/www/html/" --exclude='.*'
fi
# 5. Cron任务备份
echo "[*] 备份定时任务..."
crontab -l > "$BACKUP_DIR/crontab-root.txt" 2>/dev/null
for user in $(cut -f1 -d: /etc/passwd); do
crontab -u $user -l > "$BACKUP_DIR/crontab-${user}.txt" 2>/dev/null
done
# 6. 生成系统信息报告
echo "[*] 生成系统信息报告..."
uname -a > "$BACKUP_DIR/system-info.txt"
rpm -qa | sort > "$BACKUP_DIR/rpm-packages.txt"
df -h > "$BACKUP_DIR/disk-usage.txt"
free -m > "$BACKUP_DIR/memory-info.txt"
systemctl list-units --type=service --state=running > "$BACKUP_DIR/running-services.txt"
echo "=== 备份完成 ==="
echo "备份位置:$BACKUP_DIR"
echo "备份大小:$(du -sh $BACKUP_DIR | cut -f1)"
2.2 检查硬件兼容性
AlmaLinux 9的硬件要求比RHEL 8高一些。特别是:
- TPM 2.0:如果你要启用安全启动,服务器需要TPM 2.0模块
- CPU指令集:确认你的CPU支持SSE4.2及以上指令集(几乎2015年后的CPU都支持)
- 磁盘空间:升级过程需要额外的5-10GB临时空间
# 检查TPM状态
sudo dmesg | grep -i tpm
# 检查CPU指令集
grep -o -w 'sse4_2\|avx' /proc/cpuinfo | sort -u
# 检查磁盘空间
df -h /
2.3 记录当前环境
这一步很多人不做,但我建议你把每一行都记下来。因为升级完你得知道”原来的样子”是什么。
#!/bin/bash
# 文件名:snapshot-current-state.sh
SNAPSHOT_FILE="/tmp/rhel8-snapshot-$(date +%Y%m%d).txt"
echo "===== RHEL8 环境快照 =====" | tee $SNAPSHOT_FILE
echo "生成时间: $(date)" | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【1. 系统版本】" | tee -a $SNAPSHOT_FILE
cat /etc/redhat-release | tee -a $SNAPSHOT_FILE
cat /etc/os-release | tee -a $SNAPSHOT_FILE
uname -r | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【2. 内核模块】" | tee -a $SNAPSHOT_FILE
lsmod | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【3. 网络配置】" | tee -a $SNAPSHOT_FILE
ip addr | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
cat /etc/resolv.conf | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【4. 已安装的RPM包(前100个)】" | tee -a $SNAPSHOT_FILE
rpm -qa --qf '%{NAME}\n' | sort | head -100 | tee -a $SNAPSHOT_FILE
echo "..." | tee -a $SNAPSHOT_FILE
echo "总计: $(rpm -qa | wc -l) 个包" | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【5. 运行中的服务】" | tee -a $SNAPSHOT_FILE
systemctl list-units --type=service --state=running --no-pager | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【6. 防火墙规则】" | tee -a $SNAPSHOT_FILE
firewall-cmd --list-all | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【7. SELinux状态】" | tee -a $SNAPSHOT_FILE
getenforce | tee -a $SNAPSHOT_FILE
sestatus | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【8. 挂载点】" | tee -a $SNAPSHOT_FILE
mount | tee -a $SNAPSHOT_FILE
cat /etc/fstab | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【9. 关键配置文件哈希值】" | tee -a $SNAPSHOT_FILE
find /etc -name "*.conf" -o -name "*.cfg" -o -name "*.ini" 2>/dev/null | \
head -50 | while read f; do
echo "$(md5sum "$f" 2>/dev/null)"
done | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "【10. 用户和组】" | tee -a $SNAPSHOT_FILE
cat /etc/passwd | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
cat /etc/group | tee -a $SNAPSHOT_FILE
echo "" | tee -a $SNAPSHOT_FILE
echo "快照已保存至: $SNAPSHOT_FILE"
三、尝试官方推荐方式——almalinux-deploy
AlmaLinux官方提供了一个迁移工具,流程是这样的:
# 1. 安装迁移工具
sudo dnf install -y almalinux-deploy
# 2. 检查兼容性
sudo almalinux-deploy check
# 3. 执行迁移(会重启多次)
sudo almalinux-deploy deploy
听起来很简单对吧?
实际操作下来,这个过程在第三台服务器就崩了。
3.1 第一个坑:依赖地狱
在第二台服务器上,almalinux-deploy跑到一半报错:
Error:
Problem: package php-8.1.2-1.el9.x86_64 requires php-common(x86-64) = 8.1.2-1.el9,
but none of the providers can be installed
- package php-8.1.2-1.el9.x86_64 requires php-cli = 8.1.2-1.el9,
but none of the providers can be installed
- problem 1 persistent for identical language bindings packages php
- problem 2 persistent for identical language bindings packages php
(try to add '--skip-broken' to skip uninstallable packages or '--nobest' to use
not only best candidate packages)
我盯着这个错误看了十分钟。问题出在PHP上——RHEL 8默认是PHP 7.2⁄7.4,而AlmaLinux 9默认是PHP 8.1。这个版本跳跃太大,dpkg/apt之类的包管理器处理不了这种跨大版本的依赖关系。
3.2 第二个坑:第三方源冲突
我的服务器上装了好几个第三方源:
- EPEL 8
- Remi PHP仓库
- Docker CE仓库
- PostgreSQL官方仓库
- NodeSource(Node.js)
这些仓库在AlmaLinux 9上要么不存在,要么版本不兼容。almalinux-deploy在迁移过程中尝试升级这些仓库的包,结果全部报错。
Error: Failed to download metadata for repo 'remi-modular':
Cannot download repomd.xml: Cannot download repodata/repomd.xml:
All mirrors were tried
3.3 第三个坑:内核模块失效
这是最致命的一个。我的服务器上跑了一个自定义的内核模块(一个网络监控工具),升级后这个模块彻底失效:
modprobe: ERROR: could not insert 'netmon.ko': Exec format error
因为RHEL 8的内核是5.14,AlmaLinux 9的内核是5.14(等等,这俩内核版本一样啊?),但glibc从2.28升到了2.34,导致很多依赖glibc的内核模块编译出来的二进制文件不兼容。
四、决定手写迁移脚本
既然官方工具不好用,我只能自己动手了。经过几天的摸索,我总结出了一套分阶段迁移法,核心思路是:
不要试图一次性完成升级,而是把整个过程拆成多个可逆的小步骤。
4.1 阶段一:清理环境
#!/bin/bash
# 文件名:phase1-cleanup.sh
# 作用:在升级前清理可能引起冲突的包和仓库
set -euo pipefail
echo "=== 阶段一:环境清理 ==="
echo "当前系统: $(cat /etc/redhat-release)"
# 1. 禁用第三方仓库(临时)
echo "[1/5] 临时禁用第三方仓库..."
for repo in $(dnf repolist all --qf "%{repoid}" | grep -v -E "^(base|appstream|extras|crb)"); do
echo " 禁用: $repo"
dnf config-manager --disable "$repo"
done
# 2. 清理未使用的依赖
echo "[2/5] 清理孤立包..."
dnf autoremove -y
# 3. 清理包缓存
echo "[3/5] 清理包缓存..."
dnf clean all
# 4. 记录当前已安装的EPEL包(用于后续重新安装)
echo "[4/5] 记录EPEL包列表..."
dnf list installed --enablerepo=epel 2>/dev/null | grep epel > /tmp/epel-packages.txt || true
# 5. 卸载可能冲突的第三方包
echo "[5/5] 卸载高风险包..."
# Docker CE在AlmaLinux 9上有仓库变更
if rpm -q docker-ce &>/dev/null; then
echo " 检测到Docker CE,将暂时卸载"
systemctl stop docker
dnf remove -y docker-ce docker-ce-cli containerd.io
fi
# Node.js从NodeSource安装的也要处理
if rpm -q nodejs &>/dev/null; then
node_version=$(node --version)
echo " 检测到Node.js $node_version,将记录版本"
echo "nodejs:$node_version" > /tmp/nodejs-version.txt
fi
echo "=== 环境清理完成 ==="
4.2 阶段二:升级glibc和相关核心库
这是最危险的一步。glibc是Linux系统的核心库,几乎所有程序都依赖它。升级glibc不当可能导致系统彻底无法启动。
#!/bin/bash
# 文件名:phase2-upgrade-glibc.sh
# 作用:谨慎升级glibc和相关核心库
set -euo pipefail
echo "=== 阶段二:升级核心库 ==="
# 1. 先备份当前glibc
echo "[1/4] 备份当前glibc..."
cp -a /lib64/libc.so.6 /lib64/libc.so.6.bak.rhel8
cp -a /usr/lib64/libc.so.6 /usr/lib64/libc.so.6.bak.rhel8 2>/dev/null || true
# 2. 启用AlmaLinux 9的仓库(但先不升级)
echo "[2/4] 配置AlmaLinux 9仓库..."
# 这里我们用一种巧妙的方法:只替换仓库元数据,不实际升级包
# 我们需要手动编辑仓库文件
# 3. 使用dnf系统升级,但限制范围
echo "[3/4] 升级核心系统组件..."
# 这个命令会尝试升级所有包,但我们用--best和--allowerasing来处理依赖
dnf system-upgrade download \
--releasever=9 \
--setopt=install_weak_deps=false \
--allowerasing \
--skip-broken \
--best \
2>&1 | tee /tmp/dnf-upgrade-log.txt
echo "[4/4] 生成升级报告..."
# 检查是否有无法解决的依赖冲突
if grep -i "error\|conflict\|problem" /tmp/dnf-upgrade-log.txt | head -20; then
echo "警告:检测到潜在冲突,请检查 /tmp/dnf-upgrade-log.txt"
exit 1
fi
echo "=== 核心库升级准备完成 ==="
echo "下一步请执行: dnf system-upgrade reboot"
实际操作中,我放弃了这个全自动方案。 因为dnf system-upgrade在RHEL 8到9的跨版本升级上本身就支持得不好,而且我的服务器上有太多第三方包。
4.3 阶段三:手工逐个升级包(我的实际方案)
我最终采用的方法是分批次手动升级。这个过程很繁琐,但可控。
”`bash #!/bin/bash
文件名:manual-upgrade-pipeline.sh
作用:分批次手动升级系统包
set -euo pipefail
颜色定义
RED=‘\033[0;31m’ GREEN=‘\033[0;32m’ YELLOW=‘\033[1;33m’ NC=‘\033[0m’ # No Color
LOG_FILE=“/var/log/almalinux9-migration-\((date +%Y%m%d).log" BATCH_DIR="/tmp/upgrade-batches" mkdir -p "\)BATCH_DIR”
echo_log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
echo -e "$msg" | tee -a "$LOG_FILE"
}
check_package_version() {
local pkg=$1
local expected_version=$2
local current_version=$(rpm -q --qf '%{VERSION}' "$pkg" 2>/dev/null || echo "not-installed")
if [[ "$current_version" == "$expected_version"* ]] || [[ "$current_version" == "not-installed" ]]; then
return 0
else
return 1
fi
}
批次1:升级glibc相关(最危险)
upgrade_glibc_batch() {
echo_log "===== 批次1:glibc核心库升级 ====="
# 先升级最小的集合
local packages=(
"glibc"
"glibc-common"
"glibc-langpack-en"
"libgcc"
"libstdc++"
"nss-softokn-freebl"
"nss-util"
"nspr"
)
for pkg in "${packages[@]}"; do
echo_log " 升级 $pkg..."
if dnf
