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

如何将香港服务器上的遗留应用从 Windows Server 2008 迁移到 Ubuntu 22.04,避免依赖性冲突?

发布人:Minchunlin 发布时间:2025-08-20 10:31 阅读量:966


昨天夜里,值班工程师把我领到 12 楼冷得发抖的机房,指着一台贴着“2008 产线”的 2U 旧机器:“这台必须今晚退役,业务不能停。”

我摸了摸前面板上已经被擦得发白的服务标签,心里暗骂早该动刀——遗留应用还跑在 Windows Server 2008 + IIS 7.0 上,.NET Framework 3.5,连补丁都停在多年前。目标是把它搬到隔壁机柜里新装好的 Ubuntu 22.04 LTS 上,第二天早上 7 点前切流。避坑和“依赖冲突”就是今晚的主旋律。

场景与目标

现状(源端)

机房:葵涌 iDC,双路市电 + UPS,A/B 路供电

旧服务器:Dell R710(2U),2× Xeon X5670,96GB RAM,RAID10(SAS 10K × 8),双口 1GbE(接 ToR:Arista 7050)

系统:Windows Server 2008 Standard,IIS 7.0

应用构成:

LegacyWeb: ASP.NET WebForms(.NET 3.5)

FileDropSvc: Windows Service,定时把 CSV 转 Excel 并上传到共享

NightlyJob: Windows 计划任务,跑一段 ETL(ODBC 连 MS SQL 2008 R2,DB 在异地)

依赖:COM 组件(Excel Interop)、ODBC 驱动、IIS URL Rewrite、旧版中文字体(宋体/仿宋)

目标(目的端)

服务器:Supermicro 1U,2× Xeon Silver 4310,256GB RAM,系统盘 NVMe(RAID1),数据盘 U.2(RAID10),双口 10GbE(Bonding LACP)

系统:Ubuntu 22.04.4 LTS(GA 内核)

运行时:.NET 6(Web)、Python 3.10(ETL/报表)、Nginx(反代)、Docker(隔离/避免冲突)

约束:不得中断业务、快速回滚、可平滑演进(允许部分功能先以兼容形态运行)

1. 迁移策略(避免依赖冲突的总思路)

  • 分层隔离:把“运行时”和“系统”解耦,用容器/虚拟环境把各自的依赖钉死。
  • 最小可用:不在一夜之间“重写”功能;能替代就替代(Mono/.NET 兼容、Python 替代 COM)。
  • 蓝绿切换:在同一 L7 入口(HAProxy/Cloudflare/Nginx)上灰度路由,保持回滚路径。
  • 可观测:上线前就埋好 metrics/log/trace,遇到冲突/崩溃迅速定位。
  • 数据侧不动:数据库仍指向原 SQL Server(先通后优)。

2. 盘点与 SBOM(源端清单)

在旧机上生成可迁移清单(SBOM),别把“隐性依赖”带雷过来:

:: IIS 站点与应用池
%windir%\system32\inetsrv\appcmd list apppool /text:*
%windir%\system32\inetsrv\appcmd list site /text:*

:: 已安装组件
wmic product get name,version > C:\sbom\installed_programs.txt

:: .NET/全局程序集缓存
dir C:\Windows\assembly /s > C:\sbom\gac_list.txt

:: 计划任务
schtasks /query /v /fo LIST > C:\sbom\tasks.txt

:: ODBC DSN
reg export "HKLM\SOFTWARE\ODBC" C:\sbom\odbc.reg /y

:: COM 注册
reg query "HKLM\SOFTWARE\Classes" /s /f "Excel.Application" > C:\sbom\com_excel.txt

结果要点(真实案例归纳):

模块 依赖 风险点 Linux 替代
WebForms .NET 3.5 GDI+、System.Web、URL Rewrite 与 .NET 6 API 差异大 升级到 ASP.NET Core(Upgrade Assistant)或临时跑 Mono
FileDropSvc Excel Interop(COM) Linux 无法使用 Office COM Python openpyxl/pandas 生成 XLSX
NightlyJob ODBC Driver 11 + SQL 2008 字符集/时区/加密 msodbcsql18 + FreeTDS/ODBC,校准 tds version
字体 宋体/仿宋 PDF/图片排版差异 安装 fonts-noto-cjk + 拷贝企业字体
URL 重写 IIS Rewrite 语法差异 转 Nginx rewrite/map

3. 目的端硬件与网络落盘(Ubuntu 22.04)

3.1 RAID/分区/挂载

# 以 mdadm 举例(供应商 RAID 卡可跳过)
apt-get update && apt-get install -y mdadm lvm2

# 数据盘 /dev/nvme1n1...nvme4n1 组 RAID10
mdadm --create /dev/md0 --level=10 --raid-devices=4 /dev/nvme[1-4]n1
pvcreate /dev/md0 && vgcreate vgdata /dev/md0
lvcreate -L 2T -n appdata vgdata
mkfs.xfs /dev/vgdata/appdata

mkdir -p /data && echo "/dev/vgdata/appdata /data xfs defaults,noatime 0 0" >> /etc/fstab
mount -a

3.2 网络(Bond + VLAN)

# /etc/netplan/01-lacp.yaml
network:
  version: 2
  ethernets:
    eno1: {}
    eno2: {}
  bonds:
    bond0:
      interfaces: [eno1, eno2]
      parameters:
        mode: 802.3ad
        mii-monitor-interval: 100
      mtu: 9000
  vlans:
    bond0.120:
      id: 120
      link: bond0
      addresses: [10.20.120.23/24]
      gateway4: 10.20.120.1
      nameservers:
        addresses: [8.8.8.8, 1.1.1.1]

3.3 基础安全与内核/FD 限制

timedatectl set-timezone Asia/Hong_Kong
ufw allow 22,80,443/tcp

cat >/etc/security/limits.d/nofile.conf <<'EOF'
* soft nofile 1048576
* hard nofile 1048576
EOF

cat >/etc/sysctl.d/99-tune.conf <<'EOF'
net.core.somaxconn=1024
fs.inotify.max_user_watches=1048576
vm.swappiness=10
EOF
sysctl --system

4. “避免依赖冲突”的落地:分容器钉版本

核心做法:所有运行时进容器,把版本钉在 Dockerfile,宿主机只维持 Docker + Nginx + 监控代理。

4.1 Nginx 反向代理(宿主机)

apt-get install -y nginx
cat >/etc/nginx/sites-available/prod.conf <<'EOF'
upstream app_web { server 127.0.0.1:5000; }
server {
  listen 80;
  server_name legacy.example.com;
  client_max_body_size 20m;
  location / {
    proxy_pass http://app_web;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
  location /healthz { return 200 "ok\n"; }
}
EOF
ln -s /etc/nginx/sites-available/prod.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

4.2 Web:.NET 迁移两段式

A 档(理想):用官方 upgrade-assistant 升级到 ASP.NET Core 6(最小改动保活)。

dotnet tool install -g upgrade-assistant
upgrade-assistant analyze ./LegacyWeb.sln
# 根据报告逐项替换,先从包升级、移除 System.Web 绑定开始

B 档(兜底):短期用 Mono 跑 .NET Framework 4.x(只建议做过渡)。

# Dockerfile.web (A 档)
FROM mcr.microsoft.com/dotnet/sdk:6.0-jammy AS build
WORKDIR /src
COPY . .
RUN dotnet restore && dotnet publish -c Release -o /out

FROM mcr.microsoft.com/dotnet/aspnet:6.0-jammy
WORKDIR /app
COPY --from=build /out .
ENV ASPNETCORE_URLS=http://0.0.0.0:5000
EXPOSE 5000
ENTRYPOINT ["dotnet","LegacyWeb.dll"]

这样做的意义:把 .NET 版本钉死在镜像标签上(6.0-jammy),避免系统升级带来的 ABI 冲突或包回退困难。

4.3 ETL/报表:用 Python 替代 COM + ODBC

弃用 Excel Interop(COM):Linux 上没有 Office COM,把 CSV→XLSX 用 pandas/openpyxl。

连接 MS SQL:msodbcsql18 + pyodbc(或 pymssql/FreeTDS)。

# Dockerfile.job
FROM ubuntu:22.04

# 钉住 OS 层依赖版本,避免后续 apt 自动升级导致 ABI 变更
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
    curl apt-transport-https gnupg ca-certificates locales && \
    locale-gen zh_CN.UTF-8

# SQL Server ODBC 驱动
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \
    echo "deb [arch=amd64] https://packages.microsoft.com/ubuntu/22.04/prod jammy main" > /etc/apt/sources.list.d/msprod.list && \
    apt-get update && apt-get install -y msodbcsql18 unixodbc-dev python3.10 python3-pip

# Python 依赖(全部钉版本)
COPY requirements.txt /tmp/requirements.txt
# 示例 requirements.txt:
# pandas==2.2.2
# openpyxl==3.1.3
# pyodbc==4.0.39
# SQLAlchemy==2.0.32
RUN pip3 install --no-cache-dir -r /tmp/requirements.txt

WORKDIR /app
COPY jobs/ /app
ENV LANG=zh_CN.UTF-8 TZ=Asia/Hong_Kong
ENTRYPOINT ["python3","run_jobs.py"]

示例:CSV → XLSX 并上传(SFTP/SMB 皆可,这里以 SFTP 为例)

# jobs/export_report.py
import pandas as pd
import sqlalchemy as sa

engine = sa.create_engine(
    "mssql+pyodbc://username:password@10.1.2.3/LegacyDB"
    "?driver=ODBC+Driver+18+for+SQL+Server&Encrypt=no"
)

df = pd.read_sql("EXEC dbo.usp_daily_report @date = CONVERT(date, GETDATE())", engine)
df.to_excel("/tmp/daily.xlsx", index=False)

# 上传
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect("10.1.9.9", username="uploader", password="***")
sftp = client.open_sftp()
sftp.put("/tmp/daily.xlsx", "/upload/daily.xlsx")
sftp.close(); client.close()

4.4 docker-compose 统一编排

# docker-compose.yml
version: "3.9"
services:
  web:
    build:
      context: ./web
      dockerfile: Dockerfile.web
    image: registry.local/legacyweb:6.0.1
    restart: always
    ports: ["127.0.0.1:5000:5000"]
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    depends_on: [job]
  job:
    build:
      context: .
      dockerfile: Dockerfile.job
    image: registry.local/legacyjob:1.4.0
    restart: always
    volumes:
      - /data/reports:/reports
    environment:
      - SQLSERVER=10.1.2.3
      - TZ=Asia/Hong_Kong

关键点:镜像名带版本号,组合 image:tag 成为可回退单位;容器之间不直接共享系统库,冲突在容器边界被“截断”。

5. 系统服务化与启动顺序(systemd)

# /etc/systemd/system/stack.service
[Unit]
Description=Legacy Stack via Docker Compose
After=network-online.target docker.service
Wants=network-online.target

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

[Install]
WantedBy=multi-user.target

systemctl daemon-reload
systemctl enable --now stack.service

6. 与 SQL Server 的“坑位”与规避

驱动/协议:SQL 2008/2012 常需要 tds version = 7.4 或 7.3;ODBC18 默认 Encrypt=yes,老库没 TLS 时需设置 Encrypt=no 或启用 TrustServerCertificate=yes(与安全策略评估后决定)。

字符集/排序规则:确认 nvarchar/varchar 映射;日志中看到 “编码错误” 时,优先检查 LANG/TZ 与连接字符串。

时区:生产库多用 UTC,报表用 HKT,一律以 ISO 8601 存储、前端展示换时区。

连接池:Python SQLAlchemy 配 pool_pre_ping=True,避免夜间短断导致“连接已关闭”。

7. URL 重写规则迁移(IIS → Nginx)

IIS 示例:

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

Nginx 等价:

server {
  listen 80;
  server_name example.com;
  return 301 https://www.example.com$request_uri;
}

复杂的 rewrite 建议用 map 拆分,并写用例回归(见第 10 节)。

8. 字体/报表与中文排版

apt-get install -y fonts-noto-cjk fonts-arphic-ukai fonts-arphic-uming
# 如需企业授权字体(SimSun/SimHei),放到 /usr/local/share/fonts/chs/ 并:
fc-cache -fv

常见现象:换行/字重不一致导致导出的 PDF 偏移;用 wkhtmltopdf/weasyprint 渲染前先在容器内装同款字体并固定 CSS 行高。

9. 观测与日志

  • Nginx:access/error 合理分卷到 /var/log/nginx/*.log
  • 应用:.NET Serilog 输出 JSON 到 STDOUT,再由宿主 journald 收集
  • Metrics:dotnet-counters/prometheus-net 暴露 /metrics,Nginx 透传
  • 健康检查:/healthz 返回 OK,同时检查 SQL 连通

10. 验收脚本与回归用例

接口冒烟(基础可用性测试):执行最小化脚本,验证服务能启动、接口能响应。例如:

set -e
host=https://legacy.example.com
curl -fS $host/healthz
curl -fS $host/api/v1/ping | grep pong

URL 重写回归(节选):

输入 期望输出
http://example.com/a https://www.example.com/a
/legacy.aspx?id=1 /legacy/1
/download?file=报告.csv 200 + 正确 Content-Type

数据一致性:

  • 对比新旧报表行数、汇总金额(±0 容忍)
  • 连续三日凌晨 2 点~3 点 ETL 成功率 100%

11. 蓝绿与切换

HAProxy(示例):按 Header 灰度 10%

frontend fe_http
  bind *:80
  acl canary hdr(X-Canary) -i 1
  use_backend be_new if canary
  default_backend be_old

backend be_old
  server s1 10.20.120.10:80 check
backend be_new
  server s2 10.20.120.23:80 check

回滚:移除 be_new 绑定或把 DNS CNAME 指回旧 VIP(保持旧机一周只读观察)。

12. 我遇到的几个典型坑 & 现场解法

  1. ODBC 崩溃/乱码:msodbcsql18 + 老库 TLS,连不上。解决:Encrypt=no;TrustServerCertificate=yes(临时),随后给 SQL 加证书并启用 TLS1.2。
  2. 报表中文变方块:容器没字库,装 fonts-noto-cjk,并在 CSS 指定 font-family。
  3. 上传 413:Nginx 默认 1MB,设置 client_max_body_size 20m。
  4. inotify 溢出:文件监控多,fs.inotify.max_user_watches=1048576。
  5. CRLF 引发脚本异常:从 Win 拷贝的脚本,dos2unix 一遍好。
  6. 文件名大小写:Linux 区分大小写,旧代码硬编码了 Report.xls/report.xls 混用,统一改名 + 单测兜底。
  7. 时区偏 8 小时:容器没设置 TZ,Excel 日期错位;镜像里添加 ENV TZ=Asia/Hong_Kong 并装 tzdata。
  8. Mono 暂用方案的边界:遇到 GDI+ 兼容差异导致图表异常,最终还是把该模块用 ASP.NET Core 重写,Mono 仅作为过桥方案,不留恋。

13. 最终交付与参数表(摘录)

宿主 OS Ubuntu 22.04.4 LTS(GA 内核)
反代 Nginx 1.18(APT)
Web 运行时 .NET 6.0.XX(容器镜像 mcr…aspnet:6.0-jammy)
作业框架 Python 3.10 + pandas 2.2.2 + pyodbc 4.0.39
SQL 驱动 msodbcsql18 + unixODBC 2.3.9
资源限制 nofile=1,048,576,Bond LACP,MTU 9000
部署 docker-compose v2,image:tag 固定
监控 /healthz + 应用指标 + Nginx 日志

14. 清单:一键检查(可放在 checklist.md)

  •  /healthz 正常,SQL 可连
  •  URL 重写全部过用例
  •  报表中文&数字格式正确
  •  Nginx/应用日志滚动策略就绪
  •  容器镜像 tag 落库记录(可回滚)
  •  回滚预案验证(把流量切回旧机 1 分钟)
  •  运维手册与故障指引更新

15. 尾声

早上 6:40,我站在机房走道尽头,看着新服务器前面板的蓝灯有节奏地闪。风声还在,但心里安定多了:入口切到了新集群,报表也按时生成,工单系统里“遗留应用迁移完成”的状态被点成了绿色。
走出机房时,保安大叔端着热水给我比了个大拇指。我知道这事儿还没完——接下来我们会把 Mono 过渡模块彻底下线,把数据侧做成只读缓存;但至少此刻,我们把 2008 的锚拔掉了,也把“依赖冲突”的炸弹拆在了容器边界。
这不是一篇完美的迁移剧本,却是一次真实的夜战:能跑、好管、可回滚。剩下的,等下一个无风的夜里,再慢慢打磨。

附:可复用的“避冲突”实践要点(摘取成卡片)

  • 容器化一切运行时:在 Dockerfile 中钉死基础镜像与库版本。
  • 替代 Office COM:用 pandas/openpyxl,彻底绕开 Windows-only 依赖。
  • IIS → Nginx:用回归用例驱动迁移,复杂规则拆 map。
  • ODBC 老库兼容:明确 Encrypt/TrustServerCertificate/tds version。
  • 字体与地区:安装 CJK 字体、设 LANG/TZ,保证 PDF/Excel 一致。
  • 蓝绿/回滚:DNS/HAProxy 双通道,镜像 tag 记录在案。
  • 观测优先:健康检查、结构化日志、基础指标先行。

如果你也正准备把 2008 上的历史债务搬到 22.04,记得先把“依赖冲突”挡在容器门外,再谈优雅重写。祝你夜战顺利。

目录结构
全文