Administrator
Published on 2026-05-14 / 21 Visits
0

从单体 EC2 到 ECS:一次完整的生产架构迁移复盘

大家好,今天想分享一次生产环境架构迁移的完整经历。

表面上看,这次项目只是把前端服务迁移到 ECS,但真正实施后会发现,它远远不是一次简单的“前端切换”。

我们实际上对整套生产架构进行了一次系统性升级,涉及存储、数据库、后端、前端、网络、TLS 证书和 DNS 等多个层面。

整个迁移主要包括以下几个部分:

  • 数据存储从 EC2 本地文件系统迁移到 S3

  • MySQL 从原有 EC2 迁移到 ECS 中的 crm-db

  • PHP 单体后端拆分并整合为 crm-backend

  • 管理后台前端独立为 crm-web

  • Next.js 主站前端部署为独立服务 gp-web

  • 引入 ALB、Target Group 和 ACM 证书

  • 通过 Cloudflare 完成 DNS 灰度切换

  • 在新架构稳定后,逐步下线旧 EC2

这是一场真正意义上的分层迁移,而不是简单地替换某一个服务。

一、存储迁移:从 EC2 本地磁盘到 S3

首先处理的是文件存储。

旧系统中的静态资源和用户上传文件都保存在 EC2 的本地文件系统里。这种方式虽然简单,但会让应用与具体服务器强绑定。

一旦应用迁移到容器环境,问题就会变得明显:容器随时可能被替换或重新调度,本地文件不会天然保留,也无法在多个容器实例之间共享。

因此,这一步的目标非常明确:让文件存储与计算节点彻底解耦。

我们先梳理了 EC2 上的静态文件和上传文件,然后将它们同步到 S3 Bucket:

gp-fileserver

接下来,对后端的文件处理逻辑进行调整:

  • 不再保存服务器本地文件路径

  • 改为保存 S3 Object Key

  • 上传文件时直接写入 S3

  • 读取文件时根据 Object Key 获取资源

  • 完整验证文件上传、访问和历史文件兼容性

这一步背后的核心原则是:

存储必须是无状态的,应用容器不能依赖本地磁盘。

完成改造以后,无论 ECS 启动多少个容器实例,也无论容器是否被重新部署,文件都不会受到影响。

二、数据库迁移:从 EC2 MySQL 到 crm-db

第二步是数据库迁移。

原来的生产数据库 gp_sales_prod 运行在旧 EC2 上。为了让数据库与遗留服务器解耦,我们将它迁移到了 ECS 中运行的 MySQL 服务 crm-db

迁移流程本身比较传统。

首先导出生产数据库:

mysqldump gp_sales_prod > dump.sql

然后将数据导入新的数据库:

mysql -h <crm-db-host> < dump.sql

真正花费时间的部分,不是导出和导入,而是数据兼容性问题。

其中最典型的是排序规则冲突:

  • utf8mb4_0900_ai_ci

  • utf8mb4_general_ci

除此之外,还需要处理字符集差异、外键约束以及不同 MySQL 版本之间的兼容问题。

数据库完成迁移后,我们还更新了所有应用的数据库连接配置,并验证了数据完整性和业务读写是否正常。

这部分给我们的经验是:

数据库迁移失败,很多时候并不是基础设施本身出了问题,而是排序规则、字符集和外键约束没有对齐。

所以在正式迁移之前,应该尽早确认以下信息:

  • 源数据库和目标数据库的 MySQL 版本

  • 数据库、数据表和字段使用的字符集

  • 排序规则是否一致

  • 外键导入顺序是否正确

  • 应用账号是否具备所需权限

这些细节往往比创建一台新数据库更容易影响迁移进度。

三、后端重构:从 PHP 单体应用到 crm-backend

旧系统是一套 PHP 单体应用,同时承担了多种职责:

  • 主站后端

  • 管理后台后端

  • 汇率更新任务

  • 页面渲染

  • 文件读写

  • 数据库访问

这些功能集中在同一个项目中,不仅耦合严重,也让后续迁移和维护变得困难。

重构后,核心后端能力被统一整合到 ECS 服务:

crm-backend

这次重构主要有四个目标:

  1. 将业务逻辑与页面展示层分离

  2. 移除对本地文件系统的依赖

  3. 集中管理数据库访问

  4. 为未来迁移到 Java 技术栈做准备

这一步的意义不只是“把 PHP 放进容器”。

如果只是原封不动地把单体应用打包进容器,原有的耦合关系和维护问题仍然存在。真正有价值的工作,是在迁移过程中重新划分服务职责,让后端成为一个相对独立、稳定的业务服务。

四、管理后台简化:拆分为 crm-web

旧版管理后台的前端页面与 PHP 后端混合在一起。

为了减少迁移复杂度,我们没有在第一阶段追求完整重做,而是先收缩管理后台的功能范围,再将它拆分为独立的 ECS 服务:

crm-web

迁移后的管理后台只保留最核心的 CRUD 功能:

  • 新闻内容管理

  • 汇率数据管理

  • 基础管理界面

这里采用的原则是:

在迁移之前,先缩小系统的功能边界。

迁移本身已经包含很多不确定因素。如果同时重写大量低频功能,会让验证范围迅速扩大,也会增加上线风险。

先保留最重要的能力,等新架构稳定之后再逐步扩展,整体上会更加可控。

五、Next.js 主站迁移:部署独立服务 gp-web

这次迁移中最复杂的部分,是将 Next.js 主站作为独立服务部署到 ECS。

新服务命名为:

gp-web

它不仅需要运行应用本身,还涉及一整套外围基础设施:

  • 新建 ECS Task Definition

  • 新建 ECS Service

  • 创建 IP 类型的 Target Group

  • 配置 ALB Listener

  • 申请和验证 ACM 证书

  • 更新 Cloudflare DNS

  • 调整安全组访问规则

  • 配置健康检查与滚动部署策略

最终的请求链路如下:

用户
  ↓
Cloudflare DNS
  ↓
公网 ALB
  ↓
Target Group(IP 模式,HTTP 3000)
  ↓
ECS gp-web

由于使用的是 Fargate,Target Group 必须选择 IP 类型,不能使用传统的 Instance 类型。

Next.js 容器监听 3000 端口,同时必须绑定:

0.0.0.0:3000

如果应用只监听 localhost,即使容器内部运行正常,ALB 也无法从容器外部访问它。

六、ALB、Target Group 与 ECS 配置

ALB 使用 Internet-facing 模式,对公网提供服务。

Listener 配置为:

  • 协议:HTTPS

  • 端口:443

  • 证书:ACM 签发的正式证书

Target Group 的配置为:

  • Target Type:IP

  • 协议:HTTP

  • 端口:3000

  • 健康检查路径:/

  • 成功状态码:200-399

ECS Task Definition 中,容器暴露 3000 端口。

ECS Service 初始配置为:

  • Desired Count:1

  • Minimum Healthy Percent:100

  • Maximum Percent:200

这套部署参数意味着:发布新版本时,ECS 会先尝试启动新的任务,在新任务健康后再停止旧任务,从而降低部署期间服务中断的风险。

不过,当 Desired Count 只有 1 时,仍然需要重点关注新任务的启动时间、健康检查宽限期以及集群资源是否足够。

七、迁移过程中遇到的四类典型故障

这次迁移最有价值的部分之一,是我们经历了几种非常典型的云上故障。

它们看起来都是“网站打不开”,但实际上分别发生在完全不同的架构层。

第一阶段:连接超时

最开始访问站点时,请求直接超时。

最终发现,ALB 的安全组没有开放 HTTPS 端口。

修复方式是在 ALB Security Group 中添加入站规则:

HTTPS 443
来源:0.0.0.0/0

这个问题属于最外层的网络访问问题。

请求甚至还没有进入 ALB 的业务转发流程,就已经被安全组拦截。

第二阶段:证书不匹配

解决网络访问后,浏览器开始提示 TLS 证书与域名不匹配。

排查发现有两个原因:

  • ACM 证书中没有包含 www 子域名

  • ALB Listener 绑定了错误的证书

修复时,我们通过 Cloudflare 添加 DNS 验证记录,重新完成 ACM 证书验证,并将正确的证书绑定到 HTTPS Listener。

这类问题通常需要同时检查:

  • 当前访问的完整域名

  • ACM 证书中的主域名和 SAN

  • ALB Listener 实际绑定的证书

  • Cloudflare 中的 DNS 验证记录

  • Cloudflare SSL 模式

第三阶段:504 Gateway Timeout

证书问题修复后,ALB 可以接收到请求,但开始返回 504 Gateway Timeout

这意味着外部请求已经到达 ALB,但 ALB 无法在规定时间内连接到后端服务。

最终原因是:ECS 服务的安全组没有允许来自 ALB 的 3000 端口流量。

修复方式是在 ECS Security Group 中添加规则:

TCP 3000
来源:ALB Security Group

这里不建议直接向公网开放容器的 3000 端口。更合理的方式,是只允许 ALB 所在的安全组访问 ECS 服务。

这样,请求只能通过 ALB 进入应用,容器本身不会直接暴露在公网。

第四阶段:503 Service Unavailable

网络打通后,部署过程中偶尔又出现了 503 Service Unavailable

这次的原因不是端口不通,而是 Target Group 中暂时没有健康的目标。

在新版本部署时,旧任务已经进入 draining 状态,但新任务尚未完成注册和健康检查,因此出现了短暂的服务空窗期。

解决方法包括:

  • 等待新任务完成注册

  • 检查 Target Group 健康状态

  • 调整 ECS 部署参数

  • 设置合理的健康检查宽限期

  • 避免旧任务过早退出

  • 确保集群有足够资源同时运行新旧任务

这个问题提醒我们:滚动发布策略写在配置里,并不代表系统就一定能够做到零停机。

还必须同时满足资源容量、应用启动速度和健康检查设置等条件。

八、最终切换策略:先验证,再灰度切流

为了降低上线风险,我们没有直接关停旧 EC2,而是采用了灰度切换策略。

整个过程分为几个阶段:

  1. 保持旧 EC2 继续提供生产服务

  2. 部署新的 ECS 服务

  3. 通过 ALB 自带域名访问并测试新环境

  4. 验证文件上传和访问

  5. 验证数据库数据一致性

  6. 验证管理后台的 CRUD 操作

  7. 更新 Cloudflare DNS,将正式流量切换到 ALB

  8. 持续监控访问日志、错误率和 Target 健康状态

  9. 确认系统稳定后,再逐步下线旧 EC2

这种方式最大的价值,是让“部署新系统”和“正式切换流量”成为两个独立动作。

即使新环境出现问题,也不会立刻影响所有用户,还可以快速将流量切回旧环境。

九、迁移前后的架构变化

迁移前,所有核心能力都集中在一台 EC2 上:

单台 EC2
├── 本地文件系统
├── MySQL 数据库
├── PHP 后端
├── 管理后台
└── 主站前端

任何一个模块出现资源竞争或部署故障,都可能影响整套系统。

迁移后,架构变成:

S3
└── 文件存储

ECS crm-db
└── 数据库

ECS crm-backend
└── 业务后端与 API

ECS crm-web
└── 管理后台

ECS gp-web
└── Next.js 主站前端

ALB
└── 流量入口与服务转发

ACM
└── TLS 证书

Cloudflare
└── DNS 与公网流量入口

新的架构并不只是组件数量变多了,更重要的是每个组件的职责更加清晰。

存储不再依赖计算节点,前后端可以独立部署,外部流量通过统一入口进入,各个服务也可以单独扩展和维护。

十、这次迁移带来的工程能力提升

通过这次迁移,我们不仅完成了系统升级,也积累了一套更成熟的 DevOps 实践。

其中包括:

  • 分层设计基础设施

  • 将有状态应用改造成无状态服务

  • 使用安全组描述服务之间的访问关系

  • 通过健康检查定位后端状态

  • 理解 ECS 滚动部署和零停机条件

  • 处理 ACM 证书与 DNS 验证

  • 设计可回滚的灰度切换方案

  • 根据错误类型快速判断故障层级

这些能力比单纯掌握某个 AWS 服务更加重要。

因为生产环境中的问题通常不会只发生在一个组件里,而是发生在多个组件的连接处。

十一、最有价值的经验:根据错误判断故障层

这次迁移中,我认为最值得总结的经验是:

不同的错误类型,通常对应不同的架构层。

可以建立这样一套快速判断关系:

错误表现

优先排查的层级

请求直接超时

公网、路由、安全组

证书不匹配

TLS、ACM、域名配置

504 Gateway Timeout

ALB 到 Target 的连接

503 Service Unavailable

Target 健康状态或部署过程

请求超时,通常说明流量连入口都没有到达。

证书不匹配,说明网络已经连通,但 TLS 配置存在问题。

出现 504,说明请求已经到达 ALB,但 ALB 无法正常连接后端服务。

出现 503,则通常意味着 ALB 可以工作,但当前没有可用的健康目标。

掌握这种映射关系以后,排查问题时就不需要在所有组件之间反复猜测,而是可以根据错误现象快速缩小范围。

结语

回头来看,这次迁移真正困难的地方,并不是创建 ECS Service、配置 ALB,或者修改一条 DNS 记录。

真正的挑战,是理解每一层的职责,以及这些层之间是如何连接的。

从本地文件迁移到 S3,是在解决状态管理问题。

从 EC2 数据库迁移到独立服务,是在解决数据层耦合问题。

拆分后端和管理前端,是在重新划分应用边界。

引入 ALB、ACM 和 Cloudflare,则是在重新设计整个系统的流量入口。

当这些层被逐一拆开、验证并重新连接以后,系统才真正从一台服务器上的单体应用,演进成一套职责清晰、可独立部署、可持续扩展的云上架构。

而这也是这次迁移最重要的价值:我们完成的不只是一次服务搬迁,而是一次完整的架构升级。