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

香港机房的恒温空调在凌晨两点发出低频的嗡鸣。交换机面板一排排绿灯像心跳。我站在香港葵涌的机柜前,看着最后一台 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)
- 资产导出:IIS/服务/任务/注册表/配置
- 代码评估:平台 API、第三方依赖、重写点
- 宿主准备:Docker、内核与 limits、Nginx、SELinux/防火墙
- 容器编排:应用、RabbitMQ、SQL Server
- 配置迁移:URL Rewrite、连接串、密钥管理
- AD/SSO:SSSD/Kerberos 或改 JWT
- 日志与观测:节点与应用指标、日志汇聚
- 灰度切换:10%→50%→100%,压测达标
- 回滚预案:旧栈只读、DNS/F5 快速回切
- 文档与交接:运行手册、CI/CD、变更流程
如果你的场景与我类似(香港机房、CentOS 7 遗留、.NET Framework 技术债、中间件异构),上面的步骤与参数基本可以“照抄+微调”。真正落地时,记住一个原则:**把与内核/系统强相关的东西尽量收进容器,把易变的业务逻辑做成可回滚、可观测、可灰度的流水线。**祝顺利