后端架构演进:从单体到云原生
从单体到云原生:一次后端架构进化的实战之旅

大家好,我是李明,在过去五年里一直在一家中型互联网公司做后端开发,从最开始的菜鸟慢慢成长为能主导系统架构设计的技术骨干。今天想和大家分享一下我们团队在过去几年中的一次架构演进经历——从传统的单体应用向云原生架构的转型之路。
这不是一个理论上的“最佳实践”分析,而是真实发生在我们项目中的、有血有肉的技术演进历程。整个过程持续了一年多的时间,期间踩过很多坑,也收获了很多经验。希望通过这篇文章,能给正在面临类似技术选型或架构升级的你带来一些启发和参考。
背景介绍:一次业务增长带来的阵痛
时间回到2021年初,我们公司主营的是一个面向中小企业的 SaaS 管理平台,主要功能包括客户管理、订单处理、员工排班、报表统计等模块。起初我们的架构非常典型:一个用 Spring Boot 写的 Java 单体应用,前端是 Vue,数据库用的是 MySQL 主从结构,部署方式也比较传统,通过 Jenkins 打包部署在两台物理服务器上。
当时的系统已经运行了三年多,整体还算稳定。但随着用户数量的快速增长(特别是年底冲量),系统的稳定性逐渐暴露出严重的问题。尤其是在高峰期,系统响应明显变慢,甚至偶发崩溃,严重影响客户体验和公司口碑。
更糟糕的是,每次上线新功能都需要整包发布,动辄几个小时,且容易出错,导致运维压力剧增。与此同时,产品团队提出了一系列新的需求,比如要接入第三方支付网关、支持国际化版本、增加移动端接口等等。
很明显,原来的单体架构已经扛不住当前的增长节奏了。我们迫切需要对系统进行重构和架构升级。
演进目标:我们要解决什么问题?
我们定了三个主要目标:
- 提升系统的可扩展性和可用性,应对不断增长的业务负载;
- 实现服务解耦与独立部署,提高开发效率和交付速度;
- 为未来引入微服务和容器化打基础,具备更强的弹性能力;
说白了,我们需要从原来的单体架构走向微服务+云原生这一当下主流的技术栈组合。
第一阶段:拆分单体,引入模块化设计
首先做的并不是直接上微服务,而是模块化改造。我们在原有项目基础上进行了代码分层和功能模块划分,把各个核心业务模块从主工程中抽离成独立的 Maven 子模块,并通过接口抽象隔离。
例如:
// 原始的单体结构:
- src/
- main/
- java/
- com.xxx.order.OrderController.java
- com.xxx.customer.CustomerService.java
...
// 改造后的结构:
project-root/
pom.xml
module-order/
src/main/java
module-customer/
src/main/java
common-utils/
...
这个阶段虽然看起来只是逻辑层面的结构调整,但实际上带来了几个关键好处:
- 各个模块职责清晰,便于后期拆分为独立服务;
- 可以在编译构建时控制依赖,降低耦合度;
- 接口标准化为后续引入 RPC 或 RESTful API 打下基础;
同时我们也借助这个机会,对系统中一些老的代码进行了重构。举个例子,订单模块原来所有的状态判断写在 Controller 层,根本不可维护。我们把它提取成了一个专门的 OrderStateHandler 类,并使用策略模式统一处理不同状态流转逻辑。
当然,这一步也不是一帆风顺。比如有些旧代码依赖过于复杂,拆分过程中一度导致功能异常,还引发了一起生产环境的服务中断事故。那次教训让我们意识到,重构必须配合完善的自动化测试和灰度发布机制,才能真正落地见效。
第二阶段:引入微服务框架,搭建初步服务化体系
经过几个月的内部模块沉淀后,我们决定正式迈入微服务时代。我们选择了当时社区活跃度较高的 Spring Cloud Alibaba 框架,结合 Nacos 作为注册中心,Degrade 和 Sentinel 处理熔断降级。
这里简单说一下我们初期微服务划分的方式:
- 将原来的核心业务模块拆分为独立服务,如订单服务 order-service、客户管理 customer-service、任务调度 job-service 等;
- 公共部分抽取为 gateway-api、common-utils、auth-center 等基础设施模块;
- 数据库方面采用按业务域垂直拆分,每个服务拥有独立数据库,保证数据边界;
比如在订单服务中,我们将原本复杂的业务逻辑逐步迁移过去,通过 Feign 进行服务间通信,如下所示:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/{id}")
public Order getOrderById(@PathVariable String id) {
return orderService.getOrderById(id);
}
// 通过Feign调用customer-service获取客户信息
@Autowired
private CustomerClient customerClient;
@GetMapping("/detail/{id}")
public OrderDetail getOrderDetail(@PathVariable String id) {
Order order = orderService.getOrderById(id);
Customer customer = customerClient.getCustomerById(order.getCustomerId());
return new OrderDetail(order, customer);
}
}
在这个过程中,我们也遭遇了一些典型的问题:
- 服务间调用链长,性能下降明显:我们后来引入了 Dubbo + Triple 协议来优化服务间通信效率;
- 数据一致性难保证:早期我们尝试使用本地事务+消息队列来做最终一致性,后面又进一步引入 Seata 框架做分布式事务支持;
- 日志追踪困难:通过接入 SkyWalking 实现全链路追踪,帮助快速定位问题;
值得一提的是,为了更好地做监控与告警,我们引入了 Prometheus + Grafana 的组合,将各服务的关键指标纳入统一大盘展示。这对我们后续做容量评估和性能优化提供了很大帮助。
第三阶段:拥抱 Kubernetes,迈向云原生
当我们完成了大部分服务的微服务拆分后,部署方式依然相对原始——还是基于 Jenkins 的脚本部署,手动操作居多,缺乏自动扩缩容和容灾机制。
这时候我们意识到,要想真正发挥云原生的优势,必须从底层架构彻底转向容器化。于是我们开始引入 Kubernetes,并使用 Helm 配合 Chart 文件来进行服务部署配置管理。
容器化部署的基本流程如下:
- 服务打包为 Docker 镜像;
- 镜像上传至 Harbor 私有仓库;
- 利用 Helm Chart 定义 Deployment、Service、Ingress 等资源;
- 通过 CI/CD 流水线触发部署操作;
例如,我们定义了一个简单的 Helm Chart 结构如下:
charts/
order-service/
values.yaml # 可配参数
Chart.yaml # chart元信息
templates/
deployment.yaml # 部署文件
service.yaml # 服务暴露配置
ingress.yaml # 外部访问入口
一个 deployment 的示例模板内容大概是这样:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "order-service.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: 8080

这一步带来的改变是非常巨大的:
- 服务部署变得更加统一和可控,可以通过一键回滚快速修复问题;
- 资源调度更加灵活,我们可以根据负载自动伸缩副本数;
- 故障恢复更快,Pod 异常重启后不影响整体服务;
不过在这个过程中我们也是踩了不少坑的:
- 镜像构建不规范:初期没有统一镜像命名规范和 tag 管理,导致多个环境混杂不清;
- Chart 参数混乱:同一个 Chart 在不同环境使用时修改太多,维护成本高;
- 资源限制不合理:Kubernetes 中没有设置合适的 CPU/Mem 限制,导致部分 Pod OOM/Kill;
这些问题后来我们通过建立统一的 CI/CD 规范、使用 Helmfile 统一管理 Chart 配置、引入 ResourceQuota 机制得以解决。
第四阶段:全面引入云原生生态组件
到了这个时候,整个系统基本上已经完成从单体到微服务再到容器化的完整迁移。但真正的“云原生”不仅仅是部署方式的变化,更重要的是围绕云的能力做深度整合与优化。
我们在这一阶段做了几件重要的事情:
- 引入 OpenTelemetry 进行统一监控埋点;
- 将日志收集从 ELK 替换为 Loki+Promtail+Grafana 架构;
- 将 Kafka 作为异步消息中间件全面替代原有的 RabbitMQ;
- 使用 KEDA 实现基于事件驱动的自动扩缩容;
- 引入 Istio 提供流量治理、安全管控能力;
特别值得说的是 Istio 的服务治理能力,它极大简化了我们对服务间通信的管理。比如我们可以非常方便地配置流量权重、灰度发布、熔断策略等,而无需在每一个服务内自行实现。
此外,我们还将原本的一些定时任务迁移到了 CronJob,利用 Kubernetes 自带的调度能力进行管理,大大减少了运维负担。
整体效果与收益总结
经过一年多的努力,我们成功将一个濒临失控的老旧单体系统,打造成了一个稳定、可扩展、易运维的新一代云原生系统。具体收益如下:
- 系统稳定性显著提升,线上故障率下降约 60%;
- 部署频率加快,每日可以支持多次灰度发布;
- 资源利用率更高,Kubernetes 自动扩缩容节省了至少 30% 的计算资源;
- 新成员入职效率提高,清晰的模块划分和文档降低了学习曲线;
- 业务迭代速度加快,新功能开发不再受制于臃肿的单体结构;
最重要的是,我们现在有了更高的技术掌控力。无论是突发流量的应对,还是未来的架构升级(比如 Serverless 方向探索),我们都有了更强的技术储备。
经验分享:几点建议送给同行们
如果你也在经历类似的架构演进,我想给你几点切身体会的建议:
- 不要一开始就追求“大而全”:先从最小可行性架构做起,比如从模块化入手,再逐步拆服务;
- 关注技术债和自动化建设:良好的 CI/CD、监控告警、日志跟踪体系,远比一时的功能上线更重要;
- 重视团队沟通和文档沉淀:技术架构变化快,只有文档和知识库跟得上,团队才能持续发展;
- 选择适合自己的技术栈:Spring Cloud、Dubbo、Kubernetes、Istio 这些都是很好的工具,但要用对地方;
- 保持开放心态,持续学习:云原生生态日新月异,及时跟进新技术才能始终保持竞争力;
还记得我刚开始接手这次架构升级时,内心是非常焦虑的,毕竟这是个牵一发动全身的大工程。但正是在这次挑战中,我不仅提升了技术能力,也学会了如何带领团队一起完成技术变革。这段经历对我来说,是一次真正的成长。
结语:技术是手段,不是终点
最后想送给大家一句话:“架构服务于业务,技术服务于人”。我们做架构演进,不是为了追赶潮流,而是为了支撑公司的发展和用户的体验。技术本身没有高低之分,适合业务场景的才是最好的。
希望这篇分享能够对你有所启发。如果你也有类似的架构演进经历,欢迎留言交流。我们一起,把更好的软件交付给这个世界。
本文作者:李明 | 资深后端工程师 | 专注企业级系统架构与云原生方向

评论 0