上一篇 下一篇 分享链接 返回 返回顶部

如何将香港服务器中的业务从 Windows Server 迁移到 RHEL 9,避免中间件兼容性导致的服务中断

发布人:Minchunlin 发布时间:2025-08-22 10:20 阅读量:699


半夜两点,我站在香港葵涌的机柜前,iKVM 屏幕上还保持着白天画的迁移甘特图。电源指示灯规矩地闪,旁边的工单写着今晚的窗口:02:00–05:00 HKT。

这是一次看似“常规”的跨系统迁移:从 Windows Server 迁到 RHEL 9。但我知道,真正让服务“翻车”的,往往不是系统本身,而是“一堆中间件兼容性的小钉子”——IIS 到 Nginx 的重写规则、.NET Framework 的跨平台问题、PFX 证书到 PEM 的转换、SELinux 的策略、SQL Server 复制链路的时延……每一颗都能卡住发布窗口。

这篇文章,把我这次“夜里把业务从 Windows 背到 RHEL 9”的实操过程完整复盘出来。不是广告词,不是泛泛而谈,而是我真实踩坑与复盘的记录。你可以拿它当 SOP,也可以当“避坑指南”。

场景与目标

现状:

  • 应用一:BFF(.NET 6,自托管 Kestrel,在 Windows 上通过 IIS 反代)
  • 应用二:库存服务(Java 17,Spring Boot)
  • 应用三:老的后台(PHP 7.4 + IIS FastCGI,部分扩展仅有 Windows 版本)

数据库:SQL Server 2019(Windows)

证书:PFX 存在 Windows 证书库

定时任务:Windows 任务计划程序

目标:

  • 迁到 RHEL 9,尽量零中断;
  • 替换 IIS→Nginx/Apache(我选 Nginx+PHP-FPM);
  • 保留数据库,过渡期先跨系统访问(同机房低时延),后续再迁 SQL 至 Linux;
  • 完整的回滚预案与蓝绿切换;
  • 对“中间件兼容性”有预案和验收标准。

1. 资产盘点与兼容性评估(务必做成表)

项目 现状 Linux 替代/方案 风险等级 备注
反向代理 IIS Nginx 1.24(RHEL 9) web.config → Nginx rewrite 转换
BFF (.NET) .NET 6 .NET 8 LTS(RHEL 9 官方包) Kestrel + Nginx;health-check
Java 服务 Java 17 OpenJDK 17(RHEL 9) systemd 管理
PHP 后台 PHP 7.4 + IIS FastCGI PHP 8.2 + PHP-FPM 部分 Windows-only 扩展替代
证书 PFX PEM(Nginx/Java Keystore) PFX→PEM/PKCS#12 转换
定时任务 任务计划程序 systemd timer/cron 1:1 映射
DB SQL Server 2019/Win 暂留 Win,后迁 Linux 先跨 OS 访问,后日志传送切换
身份 AD 加域 SSSD/realmd 仅运维登陆需要

决策:

  • 先 App 后 DB:应用先迁,数据库暂不动,降低窗口风险;
  • 蓝绿切换:搭一套“绿色”RHEL 9,流量灰度后再全量;
  • 高风险组件优先建替代方案:PHP Windows-only 扩展先改造或降级功能,确保可上线。

2. 目标硬件与系统准备

机型(香港托管裸金属):

  • CPU:2 × AMD EPYC 7452(32C/64T)
  • 内存:256 GB
  • 磁盘:2 × NVMe 3.84 TB(RAID1 via mdadm)+ 2 × SATA SSD 1.92 TB(RAID1,日志/备份)
  • 网卡:2 × 10GbE(bonding mode 4 LACP,交换机双上联)
  • 带宽:1 Gbps 保底,峰值 10 Gbps(同机房内网 < 0.5ms RTT)

RHEL 9 装机注意:

分区/LVM:

/(XFS) 200G;/var/log 单独 100G;/data 剩余;

swap 32G;

BIOS:开 IOMMU、虚拟化支持;

启动后调优:

# 订阅与基础包
subscription-manager register --username <xxx> --password <xxx>
subscription-manager attach --auto
dnf update -y

# 常用组件
dnf install -y vim htop tmux git jq unzip wget curl tar policycoreutils-python-utils \
               net-tools bind-utils chrony tuned pciutils lsof

# .NET / Java / Nginx / PHP
dnf install -y dotnet-sdk-8.0 dotnet-runtime-8.0
dnf install -y java-17-openjdk java-17-openjdk-devel
dnf install -y nginx php php-fpm php-cli php-mbstring php-xml php-json php-gd php-mysqlnd

# 性能模式
systemctl enable --now tuned
tuned-adm profile latency-performance

内核/网络调优(保守值):

cat >/etc/sysctl.d/99-tuning.conf <<'EOF'
fs.file-max = 2000000
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65536
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 262144
EOF

sysctl --system

# 文件句柄
echo '* - nofile 1048576' >/etc/security/limits.d/99-nofile.conf

# 时钟与时区(香港)
timedatectl set-timezone Asia/Hong_Kong
systemctl enable --now chronyd

注:不要设置已废弃/危险项(如 tcp_tw_recycle)。上述值偏保守,结合业务压测再调。

SELinux/Firewalld 基准:

# 建议保持 enforcing,按需加白
setenforce 1
getsebool -a | grep httpd

# 放行经典端口
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

3. Nginx 替代 IIS(含重写规则迁移)

典型 web.config 片段(IIS):

<rewrite>
  <rules>
    <rule name="ForceHTTPS" stopProcessing="true">
      <match url="(.*)" />
      <conditions>
        <add input="{HTTPS}" pattern="off" ignoreCase="true" />
      </conditions>
      <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
    </rule>
    <rule name="SpaFallback">
      <match url=".*" />
      <conditions logicalGrouping="MatchAll">
        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true"/>
        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true"/>
      </conditions>
      <action type="Rewrite" url="/index.html"/>
    </rule>
  </rules>
</rewrite>

对应 Nginx:

server {
    listen 80 default_server;
    server_name _;

    # 强制 HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/example.crt;   # PEM
    ssl_certificate_key /etc/nginx/ssl/example.key;

    # 代理到 .NET Kestrel
    location /api/ {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://127.0.0.1:5000;
        proxy_read_timeout 60s;
    }

    # SPA fallback
    location / {
        try_files $uri $uri/ /index.html;
        root /data/www/spa;
    }
}

上线顺序:

  • 先在 RHEL 上以 80/443 以外的端口启动 Nginx 做自检;
  • 内部 VIP 切到新 Nginx 做小流量;
  • 验证重写/缓存/跨域/压缩等细节;
  • 再对外切换。

4. .NET 与 Java 服务迁移

.NET 8 Kestrel + systemd:

# /etc/systemd/system/bff.service
[Unit]
Description=BFF service (.NET 8)
Wants=network-online.target
After=network-online.target

[Service]
WorkingDirectory=/opt/bff
ExecStart=/usr/bin/dotnet /opt/bff/Bff.dll --urls=http://127.0.0.1:5000
Restart=always
RestartSec=3
User=svcapp
Group=svcapp
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1
# 打开文件数
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target

useradd -r -s /sbin/nologin svcapp
mkdir -p /opt/bff && chown -R svcapp:svcapp /opt/bff
systemctl daemon-reload
systemctl enable --now bff

Java 17 Spring Boot + systemd:

# /etc/systemd/system/inventory.service
[Unit]
Description=Inventory Service (Java 17)
After=network-online.target
Wants=network-online.target

[Service]
User=svcapp
Group=svcapp
WorkingDirectory=/opt/inventory
ExecStart=/usr/bin/java -XX:+UseG1GC -Xms1g -Xmx4g \
  -Dspring.profiles.active=prod \
  -jar /opt/inventory/inventory.jar
Restart=always
RestartSec=5
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target

5. PHP 迁移与 Windows-only 扩展的替代

将 PHP 7.4 升至 PHP 8.2(RHEL 9 AppStream 提供)。

FastCGI → PHP-FPM:

sed -i 's/^user = apache/user = nginx/' /etc/php-fpm.d/www.conf
sed -i 's/^group = apache/group = nginx/' /etc/php-fpm.d/www.conf
systemctl enable --now php-fpm

Nginx 对接:

location ~ \.php$ {
    root /data/www/admin;
    fastcgi_pass   unix:/run/php-fpm/www.sock;
    fastcgi_index  index.php;
    include        fastcgi_params;
    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Windows-only 扩展:

  • 如果有 COM、ODBC 驱动仅 Windows 可用,优先寻找 等价 Linux 扩展;
  • 确实无替代 → 拆分该功能为独立的“Windows 边车服务”,通过 HTTP/RPC 提供能力,等后续重构;
  • 验收标准:核心路径无该扩展依赖,功能退化但可闭环。

6. 证书从 PFX 到 PEM/Keystore

导出 PEM(Nginx 用):

# 拿到 .pfx (含私钥),转换为 .pem
openssl pkcs12 -in cert.pfx -out example.pem -nodes
# 分离出证书与私钥
awk 'BEGIN{c=0}/BEGIN CERTIFICATE/{c++} { print > "cert" c ".crt"}' example.pem
grep -A100 "PRIVATE KEY" example.pem > example.key
chmod 600 example.key

转为 Java Keystore(如需):

# PFX -> PKCS12 -> JKS
keytool -importkeystore -srckeystore cert.pfx -srcstoretype pkcs12 \
        -destkeystore keystore.p12 -deststoretype pkcs12
keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 \
        -destkeystore keystore.jks -deststoretype jks

7. 身份与访问控制(AD 加域)

dnf install -y realmd sssd adcli oddjob oddjob-mkhomedir samba-common-tools
realm discover example.local
realm join -U admin@example.local example.local
# 允许域用户 ssh
echo "account required pam_access.so" >> /etc/pam.d/sshd
echo "-:ALL EXCEPT (example\\local admins):ALL" >> /etc/security/access.conf
systemctl restart sssd sshd
id alice@example.local

8. 跨系统访问数据库与后续迁移策略

阶段 A(当前窗口):应用在 RHEL 9,数据库暂留 Windows,同机房,确保 RTT < 1ms。
Nginx/BFF/Java 指向原 SQL Server 地址,连通性:

# 测试 1433 端口连通
nc -vz db-win.example.local 1433

连接字符串注意:启用加密(Encrypt=True;TrustServerCertificate=False;)与连接池配置。

阶段 B(下一个窗口):SQL Server 迁 Linux(RHEL 9),建议 日志传送(log shipping) 或 可用性组(Pacemaker AG)。
以日志传送为例(T-SQL 摘要):

-- 源(Windows)上备份
BACKUP DATABASE MyDB TO DISK='\\backup\MyDB_full.bak' WITH INIT, COMPRESSION;
BACKUP LOG MyDB TO DISK='\\backup\MyDB_tlog1.trn' WITH COMPRESSION;

-- 目标(Linux)还原(NORECOVERY)
RESTORE DATABASE MyDB FROM DISK='/mnt/backup/MyDB_full.bak' WITH NORECOVERY, MOVE ...;
RESTORE LOG MyDB FROM DISK='/mnt/backup/MyDB_tlog1.trn' WITH NORECOVERY;

-- 定时传送日志,只到切换前一刻
-- 切换窗口内,源暂停写,做 tail-log 备份,目标最后一次还原后 RECOVERY

要点:在跨 OS 时,文件路径与排序规则、权限、作业调度器不同,提前做演练。

9. 定时任务:任务计划程序 → systemd timer

# /etc/systemd/system/report.service
[Unit]
Description=Nightly Report

[Service]
Type=oneshot
User=svcapp
ExecStart=/opt/scripts/gen_report.sh

# /etc/systemd/system/report.timer
[Unit]
Description=Nightly Report Timer

[Timer]
OnCalendar=*-*-* 02:10:00
Persistent=true

[Install]
WantedBy=timers.target

systemctl enable --now report.timer
systemctl list-timers | grep report

10. 灰度与蓝绿:健康检查与切流

Nginx 本地 upstream + 灰度:

upstream bff_pool {
    server 127.0.0.1:5000 max_fails=2 fail_timeout=10s;
    keepalive 128;
}

# 10% 灰度给新版本
split_clients "${remote_addr}" $split_var {
    10%  "canary";
    *    "stable";
}

location /api/ {
    if ($split_var = canary) {
        proxy_pass http://127.0.0.1:5001;  # 新版
    }
    if ($split_var = stable) {
        proxy_pass http://bff_pool;        # 旧版
    }
    proxy_set_header Connection "";
    proxy_http_version 1.1;
    proxy_set_header Host $host;
}

健康检查:应用提供 /healthz;Nginx 或 LB 做主动探测,不健康即摘除。

外部切换:先内网 VIP灰度,再 DNS 低 TTL(如 60s) 切换公网域名。

回滚:只需将权重切回旧池,恢复原 DNS/VIP。

11. 文件与编码迁移细节(这些最容易“阴沟翻船”)

  • 大小写敏感:Linux 文件系统区分大小写,Windows 不区分。部署前跑一遍静态检查/脚本统一命名。
  • 换行符:CRLF→LF,尤其是 shell 脚本与 env 文件。
  • 编码:GBK/Big5 遗留数据统一转 UTF-8;Java/.NET 接口要声明 charset。
  • 路径分隔符:C:\、\→/,环境变量与代码里统一抽象路径。
  • 证书库:Windows 证书库转移后,注意 中间证书链 完整性。
  • 时区与夏令时:统一 Asia/Hong_Kong,日志全部用 UTC + 时区 输出,避免跨系统时间漂移。
  • MTU:跨 VLAN 或跨交换机链路,1500 若丢包,先试 1492/1450;tracepath 可定位 PMTU。
ip link set dev bond0 mtu 9000   # 仅在端到端都支持巨帧时启用

12. SELinux/权限常见拦截(现场坑与处理)

Nginx 反代到本地端口被拦:

setsebool -P httpd_can_network_connect 1

读写特定目录:

semanage fcontext -a -t httpd_sys_rw_content_t "/data/www(/.*)?"
restorecon -Rv /data/www

监听非默认端口:

semanage port -a -t http_port_t -p tcp 5000

13. 验收清单(上线前逐项打勾)

类别 项目 验收点 结论
网络 RTT App↔DB RTT < 1ms
证书 链路 TLS1.2+,中间证书完整
Nginx 重写 301、fallback 行为与 IIS 一致
.NET 线程/GC 峰值 QPS 下 P99 < 200ms
Java JVM G1 GC 暂停 < 50ms(P95)
PHP 兼容 Windows-only 功能剥离不影响主流程
SELinux 策略 无拒绝日志(/var/log/audit/audit.log
定时 Timer 02:10 成功跑,失败告警触发
日志 统一 结构化 JSON,包含 traceId
回滚 预案 5 分钟内可切回旧池

14. 运行中的观测与告警

  • 系统层:node_exporter + Prometheus + Grafana;
  • 应用层:/metrics 暴露、RED(Rate/Errors/Duration)指标;
  • 日志:journald 持久化 + Vector/Fluent Bit 汇聚到 Loki/Elastic;
  • 告警:延迟、错误率、5xx、队列长度、数据库连接池耗尽、GC 时间、磁盘写放大。

15. 关键数据与时间线(这次窗口的真实记录)

类别 项目 验收点 结论
网络 RTT App↔DB RTT < 1ms
证书 链路 TLS1.2+,中间证书完整
Nginx 重写 301、fallback 行为与 IIS 一致
.NET 线程/GC 峰值 QPS 下 P99 < 200ms
Java JVM G1 GC 暂停 < 50ms(P95)
PHP 兼容 Windows-only 功能剥离不影响主流程
SELinux 策略 无拒绝日志(/var/log/audit/audit.log
定时 Timer 02:10 成功跑,失败告警触发
日志 统一 结构化 JSON,包含 traceId
回滚 预案 5 分钟内可切回旧池

16. 附录:常用命令速查

# Nginx 语法/重载
nginx -t && systemctl reload nginx

# SELinux 拒绝审计
ausearch -m AVC -ts recent

# TCP 端口监听
ss -lntp

# 压测
wrk -t8 -c256 -d60s https://example.com/api/healthz

# 文件同步(Win→Linux 可用 SMB 临时共享)
mount -t cifs //winhost/share /mnt/win -o username=...,password=...,vers=3.0
rsync -avh /mnt/win/www/ /data/www/

从 Windows 到 RHEL 9,服务稳定落地

凌晨四点,我把外套搭在机柜门上,屏幕里最后一条图表线缓缓回落。Nginx 的 200 数一页页刷过,Java 的 GC 曲线像一条被驯服的鱼线。.NET 的 P99 停在了我们压测时的预期值以下。最顽固的几个小坑——PFX 转 PEM、web.config 的细枝末节、SELinux 的权限,都在一条条命令里被抚平。

“把业务从 Windows 背到 RHEL 9”,这话说起来轻巧。真正让服务不断的,是事先那张表,是每一个“如果不行就回滚”的开关,是把兼容性问题拆开、替代、验证、再替代的耐心。
走出机房,天还没亮,我给白天班的同事留了封邮件:数据库的迁移,我们下一个窗口见。

如果你要把这套流程落地在自己的环境里,可以直接拿上面的清单与配置模板改造;遇到“Windows-only”的中间件,别硬拼,先拆分,保证主流程可跑,再用时间去消化历史债务。迁移的节奏对了,中断自然就消失在灯光之外。

目录结构
全文