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

如何将香港服务器中的 Windows Server 应用迁移至 Linux 环境,解决跨平台依赖与中间件兼容性问题?

发布人:Minchunlin 发布时间:2025-08-18 10:57 阅读量:845


香港机房的恒温空调在凌晨两点发出低频的嗡鸣。交换机面板一排排绿灯像心跳。我站在香港葵涌的机柜前,看着最后一台 Windows Server 2012 R2 上的 IIS,心里盘算着:这个承载了公司核心业务七年的老家伙,今晚要把它的‘灵魂’迁到 Linux 上去。

迁移动因不复杂:旧版 Windows 授权与补丁压力越来越大,.NET Framework 技术债堆积,配套中间件(MSMQ、IIS URL Rewrite、Windows 计划任务、DPAPI)的平台耦合严重。我们既要控制停机窗口,又要把未来的运维成本和弹性做上去。下面是我这次在香港机房落地迁移的完整过程、参数、踩坑与解法。文字偏“现场气”,代码和表格尽量到位,新手能看懂,老手也能挑刺。

1)现场环境与目标架构

1.1 当前硬件与网络(香港机房)

角色 型号/CPU 内存 磁盘/阵列 网络 系统
应用节点(旧) Dell R730 / 2×Xeon E5-2680 v4 128GB ECC 2×SATA SSD (RAID1) + 4×10K SAS (RAID10) 2×10GbE(LACP) Windows Server 2012 R2
DB 节点(旧) Dell R740 / Xeon Silver 4114 256GB ECC 4×NVMe(RAID10,HBA 直通) 2×10GbE SQL Server 2016 (CU 最新)
负载均衡 F5(L4)+ Nginx(L7) N/A N/A 2×10GbE CentOS 7.9(混合角色)

带宽落地是双 10G,BGP,核心交换机上做了几个 VLAN:前端、后端、管理网与备份网分离。

1.2 迁移目标(最小改动+可持续)

  • 宿主 OS: 仍使用 CentOS 7.9(用户历史环境约束),应用容器化以解耦 glibc 等依赖。
  • Web 层: Nginx (L7) + Kestrel(.NET) 取代 IIS。
  • 应用: .NET Framework 应用迁移到 .NET 8(容器内 Ubuntu 22.04 基础镜像)。
  • 消息: 从 MSMQ → RabbitMQ。
  • 数据库: 从 SQL Server 2016(Windows)→ SQL Server 2019(Linux 容器)(先平移,后续再评估 Aurora/Postgres)。
  • 计划任务: Windows Task Scheduler → systemd timer/cron。
  • 认证与权限: AD 集成由 Kerberos/SSSD 实现 Windows 身份单点。
  • 日志与观测: journald + logrotate + node_exporter + Prometheus + Grafana;应用内打点统一 OpenTelemetry。

关键策略:宿主保持 CentOS 7,业务尽可能跑在容器里(避免 CentOS 7 上 glibc 过老与 .NET 6/8 兼容性冲突)。数据库先容器化“平移”,等业务稳定后再考虑拆分或托管。

2)盘点与评估:把“依赖画像”画清楚

2.1 Windows 侧资产导出脚本(PowerShell)

# 导出 IIS 站点/应用池配置
Import-Module WebAdministration
Get-Website | Select Name, State, PhysicalPath, Bindings | Export-Csv C:\mig\iis_sites.csv -NoType
Get-WebAppPoolState -Name * | Export-Csv C:\mig\app_pools.csv -NoType
# 导出 URL Rewrite 规则
Copy-Item "C:\Windows\System32\inetsrv\config\ApplicationHost.config" C:\mig\

# 导出安装软件与服务
Get-WmiObject -Class Win32_Product | Select Name, Version | Export-Csv C:\mig\software.csv -NoType
Get-Service | Select Name,DisplayName,Status,StartType | Export-Csv C:\mig\services.csv -NoType

# 导出计划任务
schtasks /Query /V /FO CSV > C:\mig\tasks.csv

# 搜索 DPAPI/注册表中的连接串等敏感信息线索
reg query HKLM\Software /s | findstr /i "ConnectionString Password" > C:\mig\secrets_hint.txt

2.2 代码级依赖扫描(.NET)

使用 dotnet upgrade-assistant(先在开发机上运行)评估可迁移性。

重点标注:System.Drawing.GDI+、COM 互操作、WCF、WindowsIdentity.RunImpersonated 之类 平台特性。

3)迁移策略矩阵(Windows → Linux)

类别 现状(Windows) 目标(Linux) 说明
Web 宿主 IIS + URL Rewrite Nginx + Kestrel + ngx_http_rewrite_module URL Rewrite 规则需转写;Web.config 中的 rewrite 节需抽离
.NET .NET Framework 4.6.x .NET 8(容器) 宿主 CentOS 7 不直接跑 .NET 8,用容器
消息队列 MSMQ RabbitMQ(容器) 代码改造为 AMQP 客户端(如 RabbitMQ.Client
计划任务 Task Scheduler systemd timer / cron 脚本逻辑迁移并改环境变量与路径
数据库 SQL Server 2016 (Win) SQL Server 2019 (Linux 容器) 先备份还原;连接串与排序规则注意
认证集成 AD 域 + Windows Auth SSSD + Kerberos/SPNEGO Nginx 支持 auth_gss 或应用侧 Kerberos
文件共享 SMB 本地盘/共享 挂载 CIFS 或迁到对象存储/NFS 权限与大小写敏感要先对齐
加密密钥 DPAPI 保护 外部密管(Vault/Keystore)或容器 Secret 读写调用逻辑需改造

4)目标平台准备(CentOS 7 宿主 + 容器化)

提醒:CentOS 7 已到生命周期尾声,但本次按现网约束操作。容器镜像选择 Ubuntu 22.04 或 Debian 12 以获得新 glibc 与 .NET 支持。

4.1 基础系统与联网

# 基础包与 Docker
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce
sudo systemctl enable --now docker

# docker-compose(用独立二进制)
sudo curl -L "https://github.com/docker/compose/releases/download/2.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 内核/网络调优(容器网络与高并发)
cat <<'SYS' | sudo tee /etc/sysctl.d/99-tuning.conf
net.core.somaxconn = 10240
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
fs.file-max = 1048576
SYS
sudo sysctl --system

# 打开文件句柄限制
echo "* soft nofile 1048576" | sudo tee -a /etc/security/limits.conf
echo "* hard nofile 1048576" | sudo tee -a /etc/security/limits.conf

4.2 Nginx(反向代理到容器内 Kestrel)

sudo yum install -y epel-release
sudo yum install -y nginx
sudo systemctl enable --now nginx

Nginx 核心反代配置(/etc/nginx/conf.d/app.conf):

upstream app_pool {
    server 127.0.0.1:5000;  # Kestrel 映射到宿主
    keepalive 64;
}

server {
    listen 80;
    server_name example.hk.company;

    # URL 重写示例(IIS 迁移)
    location /oldpath/ {
        rewrite ^/oldpath/(.*)$ /newpath/$1 permanent;
    }

    location / {
        proxy_pass         http://app_pool;
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_read_timeout 120s;
    }
}

5)应用改造与容器化

5.1 .NET Framework → .NET 8:最小可行改造要点

项目迁移:先用 try-convert/upgrade-assistant 生成 SDK 风格项目,替换 packages.config 为 PackageReference。

平台特性替换:

  • System.Drawing → SkiaSharp(跨平台);
  • WindowsIdentity/SSPI → Kerberos/SPNEGO 或应用层 JWT;
  • WCF → gRPC/REST;
  • MSMQ → AMQP(RabbitMQ.Client)。

静态资源 & 路径:修复大小写、斜杠差异(Linux 区分大小写,路径用 /)。

文件编码:统一 UTF-8,换行 LF(避免文本处理类 Bug)。

配置:从 web.config 转为 appsettings.json(环境分层)。

最小 Program.cs(Kestrel + Nginx):

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseKestrel(o => {
    o.AddServerHeader = false;
    o.Limits.MaxConcurrentConnections = 10000;
});
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseForwardedHeaders();
app.UseRouting();
app.MapControllers();
app.Run();

5.2 应用容器镜像(Dockerfile)

我们用 Ubuntu 22.04 以满足 .NET 8 需求,即便宿主是 CentOS 7 也可运行容器。

FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy AS base
WORKDIR /app
EXPOSE 5000
ENV ASPNETCORE_URLS=http://0.0.0.0:5000

FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
WORKDIR /src
COPY ./src/App/App.csproj ./src/App/
RUN dotnet restore ./src/App/App.csproj
COPY . .
RUN dotnet publish ./src/App/App.csproj -c Release -o /out /p:PublishTrimmed=true

FROM base AS final
WORKDIR /app
COPY --from=build /out .
ENTRYPOINT ["dotnet", "App.dll"]

5.3 docker-compose(应用 + RabbitMQ)

/opt/stack/docker-compose.yml

version: "3.8"
services:
  app:
    build: ./app
    image: hk-registry.local/app:1.0.0
    restart: always
    ports:
      - "5000:5000"    # 宿主给 Nginx 反代
    environment:
      - ConnectionStrings__Default=Server=db,1433;Database=Prod;User Id=sa;Password=${SA_PWD};
      - Rabbit__Host=rabbit
    depends_on:
      - rabbit
      - db

  rabbit:
    image: rabbitmq:3.13-management
    restart: always
    ports:
      - "15672:15672"
      - "5672:5672"
    environment:
      - RABBITMQ_DEFAULT_USER=app
      - RABBITMQ_DEFAULT_PASS=${RABBIT_PWD}

  db:
    image: mcr.microsoft.com/mssql/server:2019-latest
    restart: always
    ports:
      - "1433:1433"
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=${SA_PWD}
    volumes:
      - mssql:/var/opt/mssql

volumes:
  mssql:

宿主通过 systemd 管理 docker-compose,保证开机自启与统一审计。

/etc/systemd/system/stack.service:

[Unit]
Description=Business Stack Compose
After=docker.service
Requires=docker.service

[Service]
WorkingDirectory=/opt/stack
ExecStart=/usr/local/bin/docker-compose up -d
ExecStop=/usr/local/bin/docker-compose down
TimeoutStartSec=0
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

sudo systemctl daemon-reload
sudo systemctl enable --now stack

6)数据库平移:SQL Server 2016(Win)→ 2019(Linux 容器)

6.1 备份与还原

旧库执行:

-- 收紧活动,踢出会话,生成 FULL 备份
ALTER DATABASE Prod SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
BACKUP DATABASE Prod TO DISK='\\backup\share\Prod_full.bak' WITH COMPRESSION, STATS=10;
ALTER DATABASE Prod SET MULTI_USER;

将 Prod_full.bak 拷贝到新库容器节点后,还原:

docker cp Prod_full.bak db:/tmp/Prod_full.bak
docker exec -it db /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PWD" -Q \
"RESTORE DATABASE Prod FROM DISK='/tmp/Prod_full.bak' WITH MOVE 'Prod' TO '/var/opt/mssql/data/Prod.mdf', MOVE 'Prod_log' TO '/var/opt/mssql/data/Prod_log.ldf'"

6.2 常见注意

排序规则(collation)一致性,避免中文/英文排序差异引发查询计划变化。

时区:容器内统一 Asia/Hong_Kong;应用层慎用 DateTime.Now,尽量 UTC + 显示层转换。

7)认证与 AD 集成(SSSD/Kerberos)

宿主加入域(示例,需与域管协作):

sudo yum install -y realmd sssd oddjob oddjob-mkhomedir adcli samba-common-tools
sudo realm discover corp.local
sudo realm join corp.local -U "CORP\\admin"
sudo authconfig --enablemkhomedir --update

应用如需 Kerberos 代理登录,可在 Nginx 层使用相应模块(或应用层处理 SPNEGO)。多数场景我们改为 反向代理 + 应用内 JWT/Session,减少耦合。

8)计划任务迁移

8.1 Windows 计划任务脚本改 systemd timer

原有任务:C:\jobs\Cleanup.bat 每 5 分钟执行一次 → Linux:

/usr/local/bin/cleanup.sh:

#!/usr/bin/env bash
set -euo pipefail
/usr/bin/docker exec app dotnet App.dll --cleanup

/etc/systemd/system/cleanup.service:

[Unit]
Description=App Cleanup Job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup.sh

/etc/systemd/system/cleanup.timer:

[Unit]
Description=Run Cleanup every 5 minutes

[Timer]
OnBootSec=2m
OnUnitActiveSec=5m
Unit=cleanup.service

[Install]
WantedBy=timers.target

sudo systemctl daemon-reload
sudo systemctl enable --now cleanup.timer

9)URL Rewrite 迁移:IIS → Nginx

典型 IIS 规则:

<rule name="ForceWWW" stopProcessing="true">
  <match url="(.*)" />
  <conditions>
    <add input="{HTTP_HOST}" pattern="^example\.hk$" />
  </conditions>
  <action type="Redirect" url="https://www.example.hk/{R:1}" redirectType="Permanent" />
</rule>

Nginx 等价:

if ($host = example.hk) {
    return 301 https://www.example.hk$request_uri;
}

坑点:IIS 的 Rewrite Maps 与 Outbound Rules 在 Nginx 没有 1:1 对应,需要按场景拆解(可用 map 指令、子请求、Lua/OpenResty 等扩展实现)。

10)日志与观测

10.1 日志

宿主:journald + logrotate(Nginx、docker logs)

应用:结构化 JSON 输出到 stdout,由 docker logs 收集,再被 Promtail/Fluent Bit 推到 Loki/ES。

/etc/logrotate.d/nginx 示例:

/var/log/nginx/*.log {
    daily
    rotate 14
    missingok
    compress
    delaycompress
    notifempty
    sharedscripts
    postrotate
        /bin/systemctl reload nginx > /dev/null 2>/dev/null || true
    endscript
}

10.2 性能指标

宿主:node_exporter

应用:OpenTelemetry + Prometheus exporter

数据库:telegraf sqlserver 或 mssql_exporter

11)安全与合规要点

防火墙(firewalld):仅开放 80/443、15672、5672、1433(内网)。

SELinux:保持 Enforcing。Nginx 反代容器端口需要:

sudo setsebool -P httpd_can_network_connect 1

密钥:弃用 DPAPI。改为 K8s/Compose Secret 或 Vault。应用读取方式统一 IOptions。

12)灰度与切换(蓝绿)

新栈并行起,Nginx 加权 10% 流量到新栈,观察 30 分钟。

压测(k6 示例):

// loadtest.js
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = { vus: 200, duration: '5m' };
export default function () {
  const res = http.get('https://www.example.hk/api/health');
  check(res, { 'status is 200': (r) => r.status === 200 });
  sleep(0.5);
}

指标达标后 50% → 100%,再把 F5 L4 指向新 VIP。DNS TTL 事先降到 60s 以便回切。

观察 24 小时无严重告警后,冻结旧栈写入,确认备份,再下线。

13)实测数据(迁移前后对比)

指标 迁移前(IIS + .NET Fx) 迁移后(Nginx + .NET 8 容器) 变化
峰值 RPS(关键 API) 2.8k 5.2k +85%
P95 延迟 220 ms 110 ms -50%
Web 节点 CPU 占用(峰值) 78% 52% -26 pp
平均内存占用 9.2 GB 6.8 GB -26%
故障恢复(单节点) 120 s 20 s(容器重建) -83%

主要收益来自:Kestrel 吞吐、Nginx keepalive、容器冷启动速度与 GC 优化(Server GC+分配模式)。

14)常见“坑位”与现场解决

.NET 版本/GLIBC 冲突

症状:在 CentOS 7 直接运行 .NET 6/8 报错。

解法:容器化 + 现代基础镜像(Ubuntu 22.04);宿主只跑 Docker。

大小写与路径

症状:部分静态文件 404。

解法:CI 加 case-sensitive 检查;资产扫描修正 /Content/logo.png vs /content/Logo.png。

IIS URL Rewrite 复杂规则

症状:在 Nginx 行为不一致。

解法:将“巨规则”拆小,用 map + try_files;必要时引入 OpenResty/Lua。

MSMQ 语义差异

症状:幂等/事务消息行为差异。

解法:应用层补偿(去重键、幂等表),RabbitMQ 设置持久化、确认(publisher confirms)。

DPAPI 受保护配置

症状:连接串无法解密。

解法:与安全同事协作,统一迁到 Vault/Secrets,代码改从环境变量或 Vault 取。

时区与文化区设置

症状:报表日期错一天,千分位/小数点格式异常。

解法:容器设 TZ=Asia/Hong_Kong;业务层所有计算用 UTC,展示层本地化。

SQL Server 还原后性能回退

症状:卡在某些慢查询。

解法:更新统计信息、重建索引;检查排序规则与并行度;容器限制 mssql-conf set memory.memorylimitmb 避免过度吃内存。

SELinux 拦截反代

症状:Nginx 502,但容器服务正常。

解法:setsebool -P httpd_can_network_connect 1,并检查 audit.log。

15)回滚预案(必须有)

旧栈与数据库在切换后 保留 72 小时,只读模式;

Nginx/F5 保留一键回切脚本(改权重/改后端 IP);

DNS TTL 已降至 60s,随时回指向旧 VIP。

16)交付与文档化

运行手册:端口矩阵、拓扑、依赖项、启动/停止顺序、常见告警处理。

配置即代码:Nginx、Compose、Systemd 全部进 Git;对生产修改需 MR + 审批。

CI/CD:Build 镜像→安全扫描→预生产回归→灰度发布→全量。

17)结尾:清晨的机房与咖啡

“切到 100% 流量时是凌晨四点十二分。Grafana 的曲线稳稳地贴在一条更低的延迟线上。机柜门关好,冷风从地板下方滑过。回去的路上我把工单状态改成‘完成’,给团队群里发了张曲线图。有人回了一个咖啡杯的表情:‘Windows 的壳退下来了,业务的心脏跳得更稳了。’
这事没有神奇魔法,只有成吨的细节和一次次推演。下一步,我们会把数据库也做成多 AZ 高可用,把 RabbitMQ 的镜像队列拓扑优化掉,再谈谈架构的拆分和无状态化。可那已经是下一个凌晨的故事了。”

附:执行清单(Checklist)

  1.  资产导出:IIS/服务/任务/注册表/配置
  2.  代码评估:平台 API、第三方依赖、重写点
  3.  宿主准备:Docker、内核与 limits、Nginx、SELinux/防火墙
  4.  容器编排:应用、RabbitMQ、SQL Server
  5.  配置迁移:URL Rewrite、连接串、密钥管理
  6.  AD/SSO:SSSD/Kerberos 或改 JWT
  7.  日志与观测:节点与应用指标、日志汇聚
  8.  灰度切换:10%→50%→100%,压测达标
  9.  回滚预案:旧栈只读、DNS/F5 快速回切
  10.  文档与交接:运行手册、CI/CD、变更流程

如果你的场景与我类似(香港机房、CentOS 7 遗留、.NET Framework 技术债、中间件异构),上面的步骤与参数基本可以“照抄+微调”。真正落地时,记住一个原则:**把与内核/系统强相关的东西尽量收进容器,把易变的业务逻辑做成可回滚、可观测、可灰度的流水线。**祝顺利

目录结构
全文