1. 为什么是“物理迁移”:从 CentOS 7 到 Rocky 9 的真实背景
1.1 CentOS 7 停服不是一句口号,而是安全基线的裂缝
先说这次迁移发生的大背景。我手里有几台跑了好几年的CentOS 7应用服务器,系统稳定,应用也稳定,平时基本不需要人管。但CentOS 7 的主流支持周期在2024年年中正式结束,这意味着安全补丁、内核漏洞修复、库文件更新都会停在那里,网上一搜“CVE 2024”就能看到大量和已停止维护的系统相关的漏洞通告。对长期跑在公网或内网但需要合规审计的业务来说,这不是“还能不能跑”的问题,而是“出事之后谁来担责”的问题。
换系统这件事拖不得,但也不能无脑换。我评估过几条路线:上容器化方案,把现有Java应用打成镜像,迁移成本高,应用里有一些老的原生依赖、自定义JVM参数和证书库,短时间内没办法全部容器化;原地升级到CentOS Stream,版本策略变了,而且和原来“稳定优先”的定位不是一回事;继续购买商业支持,成本又不太划算。最后综合下来,Rocky Linux 9成了最合理的走向。
Rocky Linux 9 和 RHEL 9 保持二进制兼容,同时继承了一套非常成熟的OpenJDK生态。RHEL系对Java应用的支持深度是所有Linux发行版里数一数二的,从JDK 8到JDK 21都有官方打包和维护策略,这对我们这种跑着老版本JDK、短期又不可能重构代码的团队来说,吸引力非常大。
1.2 Rocky 9 与 CentOS 7 的同源关系:迁移的底层基础
很多人会把 CentOS 7 和 Rocky 9 当成两个完全不同的系统,其实从发行版血缘看,它们走的都是RHEL这条路。CentOS 7 基于 RHEL 7,Rocky 9 基于 RHEL 9,系统目录结构、服务管理方式、软件包管理思路一脉相承。这也是为什么“物理迁移”这个方案能成立——它不是把JDK从一个完全陌生的系统搬到另一个完全陌生的系统,而是在同一个大生态里做跨版本搬迁。
当然,底层库的版本差距是实实在在的。CentOS 7 的glibc是2.17,Rocky 9 的glibc是2.34;CentOS 7 默认是Xorg/X11老一代图形栈,Rocky 9 默认已经是Wayland时代;包管理器从yum换成了dnf(保留了yum兼容命令)。这些差异对普通Java应用来说是透明的,但对JDK本身、对某些老的原生库、对依赖系统加密策略的网络通信,影响可能藏在暗处。
1.3 为什么我选了“物理迁移”而不是 yum 重装
这里说的“物理迁移”,是指不改变应用部署形态,把整个JDK目录当作数据一样从老机器搬到新机器,不重装、不重新配置环境,连cacerts证书库、jre/lib/security下的安全策略文件都一并搬过去。为什么不用dnf install java-1.8.0-openjdk重装一个?
第一个原因,版本一致性。生产环境里应用对JDK版本非常敏感,尤其是某些老项目,换一个小版本都可能引发GC行为变化或者加密套件兼容问题。CentOS 7 和 Rocky 9 的OpenJDK包版本不完全一致,直接用包管理器安装,得到的JDK可能和线上验证过的版本有偏差。物理迁移能确保二进制版本号完全一致,至少应用层少一个变量。
第二个原因,离线内网环境。我这次有几台服务器在比较严格的内网环境,访问不了外部软件源,配置内部镜像源又要走一堆审批流程。物理迁移只需要SSH通道或者U盘拷贝,绕过了所有源依赖问题。
第三个原因,JDK目录本身就是自包含的。Oracle JDK和OpenJDK的tar.gz包安装方式,本来就是解压即用,目录里包含了运行所需的全部Java类库、原生库和配置文件。这和Windows上的绿色免安装软件是一个道理。既然它天生支持“复制到另一台机器就能跑”,那为迁移做一次物理拷贝就是最顺理成章的操作。
注意:这里的“物理迁移”不是指从物理机到物理机的块设备克隆,而是指应用层不重新安装、不重新构建,以目录为单位直接搬迁。如果你要连操作系统配置一起克隆,那是另一套方案,不在本文讨论范围内。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一次离线搬迁的前置检查:架构、版本与依赖
2.1 架构一致性与 JDK 版本审查
任何“物理迁移”的第一步,都是确认两台机器的架构一致。JDK里的Java字节码是跨平台的,但bin/java这个启动器本身是原生二进制,依赖特定CPU架构和操作系统ABI。x86_64的JDK不能放到aarch64的机器上跑,这点用uname -m看架构就行,两台机器都必须是x86_64(或者都是aarch64),这是硬前提。
然后是JDK版本审查。我在迁移前用java -version确认了老机器上的版本,记录格式大概是java version "1.8.0_431"。这里有个坑要提醒:如果你线上用的是Oracle JDK 8,要提前确认商用授权问题,Oracle JDK 8 的免费商用许可在2025年之后收紧了。比较稳妥的做法,是把Oracle JDK 8换到OpenJDK 8的对应u版本,再物理迁移。Java 8u系列的OpenJDK和Oracle JDK在绝大多数场景下行为一致,企业应用跑起来基本无感。如果应用跑在JDK 11或17上,直接做物理迁移会省心很多,这两个版本在Rocky 9上的兼容性几乎无可挑剔。
在动手前,我还做了另一件事:把老机器上的JVM参数完整记录下来。查看方式包括ps aux | grep java看进程启动参数,检查 /etc/default/tomcat、/etc/sysconfig/tomcat、CATALINA_OPTS、JAVA_OPTS这些环境变量,以及应用自己的启动脚本。这些参数里往往藏着-Xmx、-Xms、-XX:+UseG1GC之类的调优设置,是整个Java运行环境里不能丢的一部分。
2.2 JDK 自带的动态库依赖检查:ldd 与 glibc
JDK的bin/java原生启动器不是完全静态链接的,它会动态依赖系统里的glibc和其他底层库。从CentOS 7到Rocky 9,最大的变量就是glibc版本从2.17升级到了2.34。绝大多说情况下,新系统的glibc向后兼容老版本编译的二进制,但我们不能只说“绝大多数”,得实测。
在从老机器拷贝JDK之前,先在新机器上对JDK目录里的原生二进制做一次依赖体检。核心命令是ldd $JAVA_HOME/bin/java,有问题的话会看到类似libX11.so.6 => not found的输出。同样的命令还要跑ldd $JAVA_HOME/bin/javac、ldd $JAVA_HOME/jre/lib/amd64/libjava.so,因为JDK里有很多so文件都有动态依赖。
如果发现缺库,解决办法很直接:dnf install对应的包。比如缺libX11就装libX11,缺字体相关依赖就装fontconfig、freetype,缺libjli.so(JDK内部的JavaLauncher库)的话,检查/etc/ld.so.conf.d/里有没有包含JDK目录,必要时把$JAVA_HOME/lib/amd64/jli加进去。这一环节是物理迁移最容易翻车的地方,因为表面上看文件都拷贝过去了,真正启动时报错才发现缺少系统库。
2.3 要跟着 JDK 一起搬的配置与证书
物理迁移不光是搬一个JDK目录,还要把和JDK运行强相关的配置文件一起搬。我最常被问到的问题是:“我把/opt/java拷过去了,为什么https请求报证书错误?”——因为很多人忘记搬证书库。
Java的HTTPS调用有一个独立的根证书库,在JDK 8里路径是$JAVA_HOME/jre/lib/security/cacerts,在JDK 9+里是$JAVA_HOME/lib/security/cacerts。如果你的应用直连外部服务走HTTPS,或者连接数据库时使用了SSL加密,老系统上可能往cacerts里导入过自建CA证书。物理迁移时,这个文件必须一起拷过去,而且不能替换成Rocky 9系统自带的OpenJDK证书库,否则自建CA的信任关系会丢。
还有一类配置是自定义的jvm.cfg、logging.properties、management.properties(JMX监控配置)。如果应用团队之前在这些文件里做过修改,迁移时也要跟着走。最简单的办法是打包时把整个JDK目录完整打进去,而不是只挑“看起来有用的子目录”。
建议:打包前用
md5sum或sha256sum给JDK目录里的关键文件生成一份校验清单,拷到新机器后逐一比对,确保字节级一致。这条经验在一次U盘拷贝的场景里帮了我大忙,当时文件数量多、小文件多,拷贝完成后有文件索引损坏,比对校验直接就暴露了问题,不用等启动时再排查诡异报错。
3. JDK 搬家实操:从打包、传输到环境变量
3.1 老机器上的打包与导出
确认完前置条件,就到了实际操作阶段。第一件事是停掉Java相关服务,保证打包时没有进程正在写JDK目录里的文件。我一般是先操作一两个低峰期业务,跑通流程后,再排生产窗口。
以一台跑Tomcat和Spring Boot应用的机器为例,我的打包命令长这样:
bash复制mkdir -p /backup/jdk-migration
tar czf /backup/jdk-migration/jdk8u431.tar.gz /opt/java/jdk1.8.0_431
tar czf /backup/jdk-migration/app-config.tar.gz \
/etc/profile.d/java.sh \
/etc/systemd/system/app.service \
/etc/default/tomcat \
/opt/app/config
这里的核心原则是:不要只打包JDK,还要把和Java应用相关的启动配置、systemd unit文件、环境变量脚本、应用配置文件全部打包。虽然目标是“物理迁移”,但搬过去的是一整套“能跑起来的环境”,不只是“能解析字节码的虚拟机”。
打包完成后,顺手生成校验值:
bash复制sha256sum /backup/jdk-migration/*.tar.gz
3.2 传送到新机器:rsync 与校验
传输方式我优先推荐rsync,它有断点续传和增量复制能力,适合大目录多文件场景。命令没有太花哨:
bash复制rsync -avz --progress /backup/jdk-migration/ root@新机器IP:/backup/jdk-migration/
如果没有内网专线,SSH直连也能跑,就是慢一点。更极端的情况是离线环境,用移动硬盘或者U盘做中转,文件小就FAT32,文件大就得exFAT或者分段zip。
到了新机器上,先做一次校验:
bash复制cd /backup/jdk-migration
sha256sum -c sha256sum.txt
校验通过后解压。我习惯先把JDK放到/opt/java/,然后建一个指向当前版本的软链接:
bash复制mkdir -p /opt/java
tar xzf /backup/jdk-migration/jdk8u431.tar.gz -C /opt/java/
ln -s /opt/java/jdk1.8.0_431 /opt/java/current
用软链接而不是直接修改应用配置里的JDK路径,是为了后续方便回滚。万一新JDK版本有问题,改一下软链接指回旧版本,应用重启就能恢复,不用一个文件一个文件去改路径。
3.3 JAVA_HOME 与 PATH 配置:profile.d 方案
JDK物理搬迁本身没有技术含量,真正的关键点在于环境变量配置。网上很多教程让你直接改/etc/profile,我个人不推荐,因为/etc/profile是登录Shell的主配置,改坏了影响面大,而且维护多个Java版本时不方便动态切换。
更规范的做法是在/etc/profile.d/下新建一个独立脚本,比如java.sh:
bash复制cat > /etc/profile.d/java.sh << 'EOF'
export JAVA_HOME=/opt/java/current
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
EOF
需要说明一下CLASSPATH。老一代Java教程喜欢设置它,但从JDK 9开始官方已经不建议手动设置全局CLASSPATH了,容易干扰应用的类加载逻辑。如果应用不需要,建议注释掉这一行。这里保留是因为有些老项目的启动脚本会读取CLASSPATH变量,没有的话会空指针,属于兼容性妥协。
新开终端让配置生效:
bash复制source /etc/profile.d/java.sh
echo $JAVA_HOME
which java
which java应该指向/opt/java/current/bin/java,java -version输出应该和旧机器完全一致。如果输出的是其他JDK,说明之前这台机器上已经装过系统自带的OpenJDK,是/usr/bin/java先被PATH找到了。处理方式:要么把/opt/java/current/bin在PATH里排到/usr/bin之前,要么用alternatives --config java调整系统默认java优先级。不过用了物理迁移的机器,我一般建议直接把系统自带OpenJDK卸载或者不装,省得后续人肉排查PATH优先级。
3.4 验证启动:java -version 和第一个 Java 进程
配置完环境变量后,不能只满足于java -version能打印出来。我还会快速跑一个真实的Java进程验证原生库加载是否正常。
最简单的验证方法是启动一个Spring Boot jar包:
bash复制nohup java -Xmx512m -jar /opt/app/myapp.jar > /tmp/app.log 2>&1 &
sleep 15
ss -lntp | grep java
看两个东西:一是应用日志里有没有UnsatisfiedLinkError、ClassNotFoundException、java.lang.UnsupportedOperationException之类的异常;二是端口是否正常监听。ss -lntp能看到当前Java进程监听了哪些端口,说明网络栈、socket相关类库加载正常。
如果这一步顺利,基本可以判断迁移成功了一大半。剩下的工作是对系统级差异做专项排查,也就是下一章要说的那些“跑起来但不对劲”的隐性坑。
4. Rocky 9 上的隐形差异:那些让老 JDK“跑起来但不对劲”的坑
4.1 系统库与加密策略差异:TLS 和 HTTPS 的静默变化
搬迁做完后,应用正常启动,端口也监听了。但真正进入业务验证阶段,麻烦一个接一个浮出来。第一个问题出在HTTPS调用上——应用连外部服务时报错,日志里出现:
code复制javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
这个报错眼熟吗?如果你从CentOS 7迁到Rocky 9,遇到这个的概率非常高。原因不在JDK本身,而在RHEL 9系列系统默认启用了更严格的系统加密策略。老版本JDK默认支持的TLS 1.0、TLS 1.1、以及一些老旧的Diffie-Hellman密钥交换套件,在新系统策略下默认被禁用。
解决思路有两个方向,取决于你的应用场景。
如果应用对接的外部系统很老,只支持TLS 1.0/1.1,而且你短期内改不了对方,那可以调整JDK自己的加密策略。JDK里有一个配置文件$JAVA_HOME/jre/lib/security/java.security(JDK 9+在$JAVA_HOME/conf/security/java.security),里面有一段:
properties复制jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, ...
把TLSv1, TLSv1.1从禁用列表里去掉,保存后重启Java应用,问题就能解决。这个方法纯粹是JDK层面生效,不影响操作系统全局策略,风险可控。
如果对接的服务端支持TLS 1.2及以上,那我建议不要动java.security,而是去升级应用依赖的HTTP客户端库版本,让它用新的TLS实现。物理迁移的终极目标是“什么都不改就能跑”,但加密这块如果完全不做适配,等于把老系统的已知问题原封不动地带进了新环境,安全审计那边也不好看。
还有一个容易忽略的点:如果应用通过JSSE和FIPS模式交互,Rocky 9默认的FIPS策略比CentOS 7严得多,有时候Java进程启动时直接报错提示算法被禁用。这时候不能只在JDK层面调整,得先确认系统是否开启了FIPS模式(fips-mode-setup --check),如果开了,再检查JDK是否支持当前加密模块。这个复杂度比较高,我一般建议非金融合规场景不要随意开启FIPS,涉及合规要求的另说。
现场排查小技巧:遇到TLS/HTTPS问题,不要急着改配置。先用
openssl s_client -connect 目标域名:443看看对端到底支持哪些协议和加密套件,再针对性调JDK,能少走很多弯路。
4.2 SELinux 与 firewalld:新系统给老应用设的门槛
第二个坑来自Rocky 9默认开启的SELinux。CentOS 7时代,很多人为了图省事直接setenforce 0把SELinux关掉了,到了Rocky 9,这个习惯必须改。新系统的SELinux策略更细化,Java应用如果监听一个非标准端口,比如8443、18080,SELinux可能直接阻断,现象是应用看起来正常启动了,但外部死活连不上端口。
排查命令:
bash复制getenforce
# Enforcing
ausearch -m avc -ts recent | grep java
如果SELinux的AVC日志里出现了和java进程相关的拒绝记录,可以通过semanage放行端口:
bash复制dnf install -y policycoreutils-python-utils
semanage port -a -t http_port_t -p tcp 8443
http_port_t这个类型虽然名字带http,但它适用于各种Java Web应用监听的TCP端口。如果是Tomcat、Spring Boot这类应用,用这个类型没问题。
另外,Rocky 9默认firewalld是开启的。CentOS 7虽然也有firewalld,但那台老机器可能早就被关闭了。新机器如果端口不通,先检查:
bash复制firewall-cmd --list-all
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --reload
物理迁移最忌“直觉判断”端口通了就行,还是用telnet或者nc -vz 新机器IP 端口测一下最稳。
4.3 systemd 单元文件:从 service 到 Unit 的适配
第三类问题发生在服务托管层面。老CentOS 7机器上的服务有些是用SysV init脚本启动的,有些是systemd unit但写得比较随意。Rocky 9已经全面转向systemd,如果还照搬旧配置,轻则服务起不来,重则启动后立马被systemd杀掉。
我这次迁移时看到的一个典型问题是,老的unit文件里没有设置User=字段,默认以root身份启动。到了Rocky 9,直接以root起Java进程虽然也能跑,但日志目录、PID文件的权限管理会比较混乱。更好的方式是明确指定运行用户:
ini复制[Unit]
Description=My Java Application
After=network.target
[Service]
Type=simple
User=tomcat
Group=tomcat
Environment=JAVA_HOME=/opt/java/current
Environment=CATALINA_HOME=/opt/tomcat
ExecStart=/opt/java/current/bin/java -Xms1g -Xmx2g -jar /opt/app/myapp.jar
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
另一个被忽略的配置是LimitNOFILE。Java应用在高并发下需要大量文件描述符,CentOS 7的老unit可能没有显式设置,默认1024,一旦并发上来就会报Too many open files。在Rocky 9的unit文件里加上:
ini复制LimitNOFILE=65536
这类配置不算复杂,但它直接影响生产稳定性。物理迁移时,我建议把每个服务的unit文件都重新过一遍,不要直接复制老文件。
4.4 字体、时区与区域设置:低概率但致命的故障
字体和时区问题不在所有Java应用上出现,但只要出现,基本就是“启动就崩”级别的故障。
先说字体。如果你的Java应用用了AWT/Swing图形界面,或者生成图表、验证码、PDF文件时会用到Java图形渲染能力,Rocky 9可能是完全干净的服务器环境,一个字体包都没装。运行时会抛出:
code复制java.lang.NullPointerException
at sun.awt.FontConfiguration.getVersion(FontConfiguration.java:1264)
或者更直接的HeadlessException。解决办法是安装字体相关依赖:
bash复制dnf install -y fontconfig freetype dejavu-sans-fonts
中文字体看需要装wqy-zenhei-fonts或者wqy-microhei-fonts,否则生成的图片上中文会变成方框。
时区问题则是“迁完后发现定时任务全部偏移”。Rocky 9默认时区可能是UTC,而老业务按北京时间跑。Java里System.currentTimeMillis()拿的是UTC时间戳不受影响,但new Date()打印出来的时间、SimpleDateFormat格式化、定时任务调度(比如Quartz)都会受影响。
bash复制timedatectl set-timezone Asia/Shanghai
date
设定完时区,再把应用重启一遍,确保JVM启动时读到的是正确时区。这一点很多人容易漏,因为Java应用的时区不一定跟随系统,它可能读user.timezone系统属性。老CentOS 7上可能有人为了方便,在启动脚本里加了-Duser.timezone=Asia/Shanghai,那这行参数得继续保持。
5. 从单机验证到回归测试:确认兼容性的完整清单
5.1 用真实应用而非 HelloWorld 验证
很多人在迁移后喜欢用java -version或者跑个HelloWorld来安慰自己“迁移成功了”。说实话,这个验证强度远远不够。物理迁移真正要回答的问题是:线上那个跑了几百个线程、连了数据库、访问了外部HTTP服务、生成了PDF报表的应用,在Rocky 9上是否还和原来一样干活。
我的做法是列一份业务冒烟清单,逐项执行:
- 启动应用后观察启动日志,重点看有没有WARN/ERROR级别异常。
- 调用一个基础健康检查接口,比如
GET /actuator/health或自定义的/ping。 - 走一遍核心业务流程,不要求全量回归,但主链路必须覆盖。
- 检查数据库连接池是否正常初始化,慢查询是否异常增多。
- 检查定时任务是否按预期触发,时间是否和业务时区一致。
- 检查日志文件是否正常滚动,权限是否可写。
这套冒烟清单跑完,基本能把4.1到4.4里说的隐性坑全部暴露出来。与其在迁移窗口里手忙脚乱地排查,不如提前写好脚本,一键触发,逐步查看输出。
5.2 监控指标与 JVM 参数调校
物理迁移后无论测得多顺,都要把监控重新接上。JVM层面的指标看这四类就够:堆内存使用率、GC频率和停顿时间、线程数量、类加载数量。在JDK 8里,推荐加一组参数把GC日志打出来:
bash复制-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/opt/app/logs/gc.log -XX:+UseG1GC
如果原JDK用的-XX:+UseConcMarkSweepGC,也就是CMS垃圾回收器,要提醒你:CMS在JDK 9之后被标记废弃,JDK 14之后被移除。如果你物理迁移的是JDK 8,那还能继续用CMS,属于老项目惯例;但如果你实际上迁的是JDK 11或17,却仍然照抄老启动参数里的CMS,进程会直接报Unrecognized VM option拒绝启动。遇到这种情况,把-XX:+UseConcMarkSweepGC改成-XX:+UseG1GC,重启即可。
性能调校上,我倾向于迁移初期“参数保持不变、只换底层系统”。先让业务正常跑,确认系统层面稳定,再逐步根据监控数据调整堆大小和GC策略。物理迁移最大的优势就是变量少,如果你一上来就大改JVM参数,出了性能问题你根本分不清是新系统引起的还是参数改的。
5.3 回滚预案:留好“后悔药”
每次迁移我都要准备一张“后悔药”——回滚方案。老机器最好不要立刻清空,保留原样运行至少一周,确认新环境完全稳定后再处理。新机器上保留旧JDK目录不删除,靠软链接切换版本,这部分在3.2里已经埋下伏笔。
万一迁移后发现严重问题需要回滚,步骤是这样的:
bash复制# 先停掉新环境上的应用
systemctl stop myapp
# 把JAVA_HOME软链接指回旧版本(假设旧版本目录还在)
ln -sfn /opt/java/jdk1.8.0_401 /opt/java/current
# 重启应用
systemctl start myapp
整个过程只需要改一个软链接,应用配置里的$JAVA_HOME引用不需要动。这就是软链接方案在迁移场景里最大的优势——不是省那几行配置,而是给了你一个随时能“倒车”的开关。
还有一层回滚要考虑:数据库结构或数据在迁移过程中如果有变更,回滚就不只是重启应用的事。所以我在迁移前会要求在数据库层面不出变更,避免“应用版本与数据库版本不匹配”的尴尬场景。如果必须有变更,那回滚方案里要包含数据库回滚步骤,这个往往比JDK本身更费时间。
写在最后的一点体感
这次从CentOS 7到Rocky 9的JDK环境物理迁移,整体节奏比我想象得平稳,但踩坑的点一个不少。最花时间的不是打包和传输,而是迁完之后对TLS策略、SELinux、systemd unit这些系统级差异的适配。物理迁移的价值也正在于此——应用代码不用动、配置文件不用动、JDK版本不用动,所有的变化都集中在操作系统层,排查问题时方向非常清晰。
最后分享一个我个人的习惯:正式迁移之前,先找一台配置和线上一致的Rocky 9测试机,把“打包、传输、解压、配环境、启动、冒烟”全流程跑三遍。第一遍记录命令和输出,第二遍确认每个步骤的注意事项,第三遍掐表计时。三遍跑完,正式迁移时大约只需要30分钟,而且大概率不会出意外。这台测试机别急着删,后续JDK升级、参数调校、补丁验证都能继续用,算是一举多得。
