把跑在一台 Windows 桌面机上的 2 个 Java 应用(app.jar / admin.jar)+ 1 个 Nginx 全量迁到 Linux(CentOS Stream 9)。环境重建(JDK 1.8 固定版本、Nginx 句柄上限 65535)、Jar 迁移、systemd 开机自启、Nginx 多域名 HTTPS 反代(含 WebSocket)与双层防火墙收敛,端口/目录与源保持一致,业务无感切换。
CHART 01 // 服务 · 端口 · 迁移后归属
service · port · destinationCHART 02 // 迁移关键处置
pitfalls handled during migration迁移任务:
[客户名称](已隐去)· 完成日期:2026-08-18 迁移方式:全量搬迁(目标机环境重建 + 应用与反代迁移,端口/目录与源保持一致) 脱敏说明:客户名、真实域名、服务器 IP 与实例信息均已隐去。
环境与系统信息
迁移前(源)
| 项目 | 信息 |
|---|---|
| 运行载体 | Windows 桌面机(同时承担开发 / 部署 / 对外服务) |
| 操作系统 | Windows 10 专业版(具体版本已隐去) |
| 业务形态 | 2 个 Java 应用 + 1 个 Nginx,全部跑在这台桌面机上 |
| 应用组成 | app.jar(APP 对外服务,端口 8080)、admin.jar(后台管理,端口 8081) |
| Web | Nginx 监听 80,按域名反代到两个端口 |
| 数据依赖 | 外部 MySQL / 中间件(保持原样,不在本次迁移范围) |
迁移后(目标)
| 项目 | 信息 |
|---|---|
| 操作系统 | CentOS Stream 9 · x86_64(dnf 包管理) |
| JDK | 1.8.0(java-1.8.0-openjdk-devel,versionlock 固定版本) |
| 应用目录 | /data/java/app(app.jar)、/data/java/admin(admin.jar) |
| 服务端口 | app 8080 / admin 8081(与迁移前完全一致) |
| Web | Nginx:多域名 HTTPS + 双后端反代(8080/8081)+ WebSocket |
| 防火墙 | firewalld,放行 22 80 443 8080 8081 |
| 开机自启 | systemd(app-java.service / admin-java.service) |
迁移背景
两个 Java 应用(Spring Boot JAR)和一个 Nginx 一直跑在一台 Windows 桌面机上。桌面机当服务器用有几个硬伤:
- 稳定性差:重启后依赖用户登录会话,无人登录/无人手工启动,服务就不起来;
- 无统一托管:靠任务计划或手工
nohup,进程管理、开机自启、异常拉起全靠人工; - 资源被抢占:桌面负载(浏览器/办公软件)与应用抢 CPU 和内存;
- 运维不透明:没有统一日志、没有防火墙收敛,安全基线无从谈起。
目标很明确:把 2 个 Java 应用 + Nginx 完整迁到 Linux 服务器,用 systemd 托管,做到重启自愈、端口收敛、日志统一,业务侧无感切换。
迁移思路
全量搬迁,三步走:目标机环境就绪 → 应用与配置迁移 → 反代接入与验证。
- 把每步命令固化成幂等脚本(可重复执行不报错),同时拆成手动分步说明,供逐条执行、逐条验证;
- 端口、目录、域名结构与迁移前保持一致,切换只发生在 DNS/IP 解析层,业务侧无感;
- 先在一台测试机跑通全流程,再上目标机,避免"边改边生产"。
迁移实施
第 1 步 · 目标机环境:安装并固定 JDK 1.8
dnf -y --setopt=skip_if_unavailable=True install java-1.8.0-openjdk-devel
关键坑:机器上可能同时存在 JDK 11/17,而 JDK 8 在 alternatives 里优先级最低,不强制切换的话 java -version 看到的还是旧版本:
J8=$(readlink -f /usr/lib/jvm/java-1.8.0-openjdk)
alternatives --set java "$J8/jre/bin/java"
alternatives --set javac "$J8/bin/javac"
再补一刀:固定版本,防止日后 dnf update 悄悄把 JDK 升走,应用运行时版本被偷偷换掉:
dnf -y --setopt=skip_if_unavailable=True install "dnf-command(versionlock)"
dnf versionlock add java-1.8.0-openjdk java-1.8.0-openjdk-devel java-1.8.0-openjdk-headless
验证:java -version / javac -version 均须显示 1.8.0_xxx。
第 2 步 · 目标机环境:Nginx 安装与连接数优化
dnf -y --setopt=skip_if_unavailable=True install nginx
默认句柄上限只有 1024,高并发下 Nginx 会先顶不住。只改 /etc/security/limits.conf 对 nginx 无效(systemd 服务不读它),要做四处:
events { worker_connections 65535; multi_accept on; }worker_rlimit_nofile 65535;(关键:只有这个对 nginx 进程直接生效)- systemd drop-in:
/etc/systemd/system/nginx.service.d/override.conf写[Service] LimitNOFILE=65535,然后systemctl daemon-reload - limits.conf 补
* soft/hard nofile 65535
启动后核对句柄上限,应显示 65535 而不是 1024:
grep 'Max open files' /proc/$(systemctl show -p MainPID --value nginx)/limits | awk '{print $4}'
第 3 步 · 防火墙放行
systemctl start firewalld; systemctl enable firewalld
for p in 22 80 443 8080 8081; do
firewall-cmd --permanent --add-port=${p}/tcp
done
firewall-cmd --reload
云服务器注意:控制台安全组是第二道墙,本机 firewalld 放行了,安全组不放行同样不通。
第 4 步 · 应用迁移:目录、Jar 上传与启动脚本
从 Windows 迁移过来,两个 Jar 直接 scp/rz 上传到目标目录:
/data/java/app/app.jar
/data/java/admin/admin.jar
每个应用配一个启动脚本 start.sh,这里有两个从 Windows 习惯带过来的经典坑:
# 关闭原有进程
PID=$(ps -ef | grep "app.jar" | grep -v grep | awk '{print $2}')
[ -n "$PID" ] && kill -9 $PID
# 关键:& 必须(否则前台运行脚本永不返回);
# </dev/null 必须(否则 SSH 会话会被挂住不退出)
nohup java -jar /data/java/app/app.jar > /data/java/app/app.log 2>&1 </dev/null &
Windows 下双击/任务计划运行 jar 从来不关心"会话依赖";Linux 下不加
</dev/null,SSH 一退出进程就可能被挂住或带走。
第 5 步 · systemd 开机自启(替代 Windows 任务计划)
app-java.service / admin-java.service,用 Type=oneshot + RemainAfterExit 套启动脚本,ExecStop 用 pkill -f '^java -jar <路径>' 精确停(防止误杀其他 java 进程):
[Unit]
Description=app Java Jar Service
After=network.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/data/java/app/start.sh
ExecStop=/usr/bin/pkill -f '^java -jar /data/java/app/app.jar'
WorkingDirectory=/data/java/app
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable app-java.service admin-java.service
比 Windows 任务计划/
rc.local可靠:开机自动拉起,systemctl status可查、journalctl -u可看日志。
第 6 步 · Nginx:Windows 正式配置转换到 Linux
生产 Nginx 配置由 Windows 正式环境的配置原样转换而来,server 块与业务规则保留,改动集中在三处——路径、事件模型、日志:
- 路径转换:Windows 盘符/反斜杠路径 → Linux 绝对路径(证书
/etc/nginx/ssl/*.pem、站点根/var/www/*、日志/var/log/nginx/); - 事件模型:Windows 的 IOCP → Linux
use epoll;+multi_accept on;; - 按日分割日志:用
map $time_iso8601提取年月日,access_${year}${month}${day}.log每天一个文件。
多域名 + HTTPS:80 端口统一 301 跳转 HTTPS;每个站点独立 443 server 块(TLS 1.2/1.3、ECDHE 套件、OCSP stapling)。双后端反代对应两个 Java 应用(8080 / 8081):
# 后端① 8080 —— 核心业务 / 充值提现
location /api1/ { proxy_pass http://127.0.0.1:8080; ... }
location [充值提现接口] { proxy_pass http://127.0.0.1:8080; ... }
# 后端② 8081 —— 后台管理 / WebSocket
location /api/ { proxy_pass http://127.0.0.1:8081; ... }
# WebSocket 长连接(聊天推送)
location /ws/ {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
WebSocket 是最容易漏的一条:反代必须显式带
Upgrade/Connection头并proxy_http_version 1.1,否则升级握手失败、推送全断。配反代前先查 80 端口,避免与已占用(如 docker)冲突导致绑定失败、reload 静默保留旧配置:
ss -tln | grep -q ':80 ' && echo "80 被占用,先处理" || echo "80 空闲,继续"
nginx -t && systemctl reload nginx
第 7 步 · 验证
systemctl restart app-java admin-java # 启动两个服务
tail -f /data/java/app/app.log /data/java/admin/admin.log # 日志确认启动
systemctl is-active nginx && systemctl is-enabled app-java admin-java # 状态 + 自启
浏览器验证:访问 http://[app域名] 进 APP 页面、http://[admin域名] 进后台页面;同时核对 ss -tlnp 只开放既定端口。
生产 Nginx 配置要点(来自正式环境)
从 Windows 正式环境配置中提取的高价值项,迁移到 Linux 后原样保留:
| 维度 | 配置 | 说明 |
|---|---|---|
| 事件模型 | epoll · worker_connections 10240 · multi_accept on |
Linux 高并发事件驱动 |
| 句柄上限 | worker_rlimit_nofile 20480 |
进程级也要放开,只改 limits.conf 无效 |
| 大文件上传 | client_max_body_size 1000m |
保持正式环境 1000M 上传能力 |
| 长超时 | proxy_read/send_timeout 1800s |
充值/提现类接口耗时较长,默认 60s 会断 |
| WebSocket | map $http_upgrade $connection_upgrade |
聊天推送长连接 |
| 限流 | limit_req rate=50r/s burst=10 nodelay + limit_conn |
单 IP 每秒 50 请求,防刷 |
| 黑名单 | deny [黑名单IP] |
攻击 IP 直接拒绝 |
| 隐藏版本 | server_tokens off |
不暴露 Nginx 版本号 |
| SSL | TLSv1.2/1.3 · ECDHE · OCSP stapling | 证书需带完整链,否则 stapling 告警 |
| Gzip | gzip on level 6 · gzip_vary on |
文本 / JS / CSS 压缩 |
| 日志 | 按日分割 + log_format 含 request_body |
按日追溯、保留请求体便于排查 |
迁移经验:这套配置的 server 块与业务规则(反代路径、限流、黑名单)原样搬过来零改动,真正要动的是路径、事件模型、日志这三处"环境相关项"——这正是 Windows → Linux 迁移里"配置翻译"的核心。
Windows → Linux 关键差异(踩坑记录)
| # | 差异点 | Windows 习惯 | Linux 处置 |
|---|---|---|---|
| 1 | 进程后台化 | 双击/任务计划,无会话概念 | nohup … </dev/null &,& 与 </dev/null 缺一不可 |
| 2 | JDK 多版本 | 手动指定 JDK | alternatives --set 强制切换 + versionlock 固定 |
| 3 | 句柄上限 | 桌面无此概念 | 只改 limits.conf 无效,需 worker_rlimit_nofile + systemd drop-in |
| 4 | 服务托管 | 任务计划 / 开机启动文件夹 | systemd(oneshot + RemainAfterExit),开机自启 + 可观测 |
| 5 | 端口冲突 | 常见但难察觉 | 配反代前先 ss 预检,避免 nginx 绑定失败静默保留旧配置 |
| 6 | 安全层 | 无防火墙概念 | firewalld + 云安全组双层都要放行 |
| 7 | 权限模型 | 全管理员 | SELinux 默认 enforcing 会拦反代/访问,调试期关闭、生产按需启用 |
| 8 | 事件模型 | Windows 用 IOCP | Linux 配 use epoll; + multi_accept on; |
| 9 | WebSocket 反代 | 配置里没写过 | 必须 proxy_http_version 1.1 + Upgrade/Connection 头,否则握手失败 |
迁移前后对照
服务 · 端口 · 归属
- app.jar —— 8080 —— APP 对外服务(
/api1/、[充值提现接口])——/data/java/app - admin.jar —— 8081 —— 后台管理 + WebSocket(
/api/、/ws/)——/data/java/admin - nginx —— 80/443 —— 多域名 HTTPS 反代 —— 反代 → 8080 / 8081
- ssh —— 22 —— 远程管理 —— OpenSSH
- firewalld —— — — 防火墙 —— 放行 22/80/443/8080/8081
关键处置
- JDK 8 被 11/17 顶掉 —— alternatives 强制切换 + versionlock 固定
- SSH 会话挂起 —— nohup 加
&与</dev/null - nginx 句柄 1024 —— systemd override + worker_rlimit_nofile 65535
- 80 端口冲突 —— 配反代前
ss预检 - WebSocket 握手失败 ——
proxy_http_version 1.1+Upgrade/Connection头 - SELinux 拦截 —— 调试期
setenforce 0
验证结果
- ✅ 两个 Java 应用均上线:
app.jar:8080、admin.jar:8081正常响应; - ✅ Nginx 反代通:
http://[app域名]、http://[admin域名]分别命中两个应用; - ✅ 开机自启生效:
systemctl is-enabled app-java admin-java均为 enabled; - ✅ 端口收敛:对外仅暴露 80/443/22 + 业务端口,与迁移前一致,业务无感切换。
经验与预防
- 迁移前先列"差异清单":Windows → Linux 最容易出问题的不是应用本身,而是进程托管、句柄上限、权限、防火墙这些"环境认知差",先逐项对齐再动手;
- 步骤固化成幂等脚本 + 手动分步说明双轨:自动执行防漏,分步说明可逐条验证,出问题能精确定位到第几步;
- 版本固定必须做:JDK 用
versionlock钉死,防止dnf update顺带升级导致运行环境漂移; - 端口/目录与源保持一致:减少切换期的心智成本,业务侧只认 IP/域名变化;
- 先测试机、再目标机:完整走一遍(含真实上传 Jar、起服务、反代),确认无误再上生产。