后端架构演进:从单体到云原生

码上开花
2025-06-14 22:49
阅读 613

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

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

大家好,我是李明,在过去五年里一直在一家中型互联网公司做后端开发,从最开始的菜鸟慢慢成长为能主导系统架构设计的技术骨干。今天想和大家分享一下我们团队在过去几年中的一次架构演进经历——从传统的单体应用向云原生架构的转型之路

这不是一个理论上的“最佳实践”分析,而是真实发生在我们项目中的、有血有肉的技术演进历程。整个过程持续了一年多的时间,期间踩过很多坑,也收获了很多经验。希望通过这篇文章,能给正在面临类似技术选型或架构升级的你带来一些启发和参考。


背景介绍:一次业务增长带来的阵痛

时间回到2021年初,我们公司主营的是一个面向中小企业的 SaaS 管理平台,主要功能包括客户管理、订单处理、员工排班、报表统计等模块。起初我们的架构非常典型:一个用 Spring Boot 写的 Java 单体应用,前端是 Vue,数据库用的是 MySQL 主从结构,部署方式也比较传统,通过 Jenkins 打包部署在两台物理服务器上。

当时的系统已经运行了三年多,整体还算稳定。但随着用户数量的快速增长(特别是年底冲量),系统的稳定性逐渐暴露出严重的问题。尤其是在高峰期,系统响应明显变慢,甚至偶发崩溃,严重影响客户体验和公司口碑。

更糟糕的是,每次上线新功能都需要整包发布,动辄几个小时,且容易出错,导致运维压力剧增。与此同时,产品团队提出了一系列新的需求,比如要接入第三方支付网关、支持国际化版本、增加移动端接口等等。

很明显,原来的单体架构已经扛不住当前的增长节奏了。我们迫切需要对系统进行重构和架构升级。


演进目标:我们要解决什么问题?

我们定了三个主要目标:

  1. 提升系统的可扩展性和可用性,应对不断增长的业务负载;
  2. 实现服务解耦与独立部署,提高开发效率和交付速度;
  3. 为未来引入微服务和容器化打基础,具备更强的弹性能力;

说白了,我们需要从原来的单体架构走向微服务+云原生这一当下主流的技术栈组合。


第一阶段:拆分单体,引入模块化设计

首先做的并不是直接上微服务,而是模块化改造。我们在原有项目基础上进行了代码分层和功能模块划分,把各个核心业务模块从主工程中抽离成独立的 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);
    }
}

在这个过程中,我们也遭遇了一些典型的问题:

  1. 服务间调用链长,性能下降明显:我们后来引入了 Dubbo + Triple 协议来优化服务间通信效率;
  2. 数据一致性难保证:早期我们尝试使用本地事务+消息队列来做最终一致性,后面又进一步引入 Seata 框架做分布式事务支持;
  3. 日志追踪困难:通过接入 SkyWalking 实现全链路追踪,帮助快速定位问题;

值得一提的是,为了更好地做监控与告警,我们引入了 Prometheus + Grafana 的组合,将各服务的关键指标纳入统一大盘展示。这对我们后续做容量评估和性能优化提供了很大帮助。


第三阶段:拥抱 Kubernetes,迈向云原生

当我们完成了大部分服务的微服务拆分后,部署方式依然相对原始——还是基于 Jenkins 的脚本部署,手动操作居多,缺乏自动扩缩容和容灾机制。

这时候我们意识到,要想真正发挥云原生的优势,必须从底层架构彻底转向容器化。于是我们开始引入 Kubernetes,并使用 Helm 配合 Chart 文件来进行服务部署配置管理。

容器化部署的基本流程如下:

  1. 服务打包为 Docker 镜像;
  2. 镜像上传至 Harbor 私有仓库;
  3. 利用 Helm Chart 定义 Deployment、Service、Ingress 等资源;
  4. 通过 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

系统架构设计图-1

这一步带来的改变是非常巨大的:

  • 服务部署变得更加统一和可控,可以通过一键回滚快速修复问题;
  • 资源调度更加灵活,我们可以根据负载自动伸缩副本数;
  • 故障恢复更快,Pod 异常重启后不影响整体服务;

不过在这个过程中我们也是踩了不少坑的:

  • 镜像构建不规范:初期没有统一镜像命名规范和 tag 管理,导致多个环境混杂不清;
  • Chart 参数混乱:同一个 Chart 在不同环境使用时修改太多,维护成本高;
  • 资源限制不合理:Kubernetes 中没有设置合适的 CPU/Mem 限制,导致部分 Pod OOM/Kill;

这些问题后来我们通过建立统一的 CI/CD 规范、使用 Helmfile 统一管理 Chart 配置、引入 ResourceQuota 机制得以解决。


第四阶段:全面引入云原生生态组件

到了这个时候,整个系统基本上已经完成从单体到微服务再到容器化的完整迁移。但真正的“云原生”不仅仅是部署方式的变化,更重要的是围绕云的能力做深度整合与优化

我们在这一阶段做了几件重要的事情:

  1. 引入 OpenTelemetry 进行统一监控埋点
  2. 将日志收集从 ELK 替换为 Loki+Promtail+Grafana 架构
  3. 将 Kafka 作为异步消息中间件全面替代原有的 RabbitMQ
  4. 使用 KEDA 实现基于事件驱动的自动扩缩容
  5. 引入 Istio 提供流量治理、安全管控能力

特别值得说的是 Istio 的服务治理能力,它极大简化了我们对服务间通信的管理。比如我们可以非常方便地配置流量权重、灰度发布、熔断策略等,而无需在每一个服务内自行实现。

此外,我们还将原本的一些定时任务迁移到了 CronJob,利用 Kubernetes 自带的调度能力进行管理,大大减少了运维负担。


整体效果与收益总结

经过一年多的努力,我们成功将一个濒临失控的老旧单体系统,打造成了一个稳定、可扩展、易运维的新一代云原生系统。具体收益如下:

  • 系统稳定性显著提升,线上故障率下降约 60%;
  • 部署频率加快,每日可以支持多次灰度发布;
  • 资源利用率更高,Kubernetes 自动扩缩容节省了至少 30% 的计算资源;
  • 新成员入职效率提高,清晰的模块划分和文档降低了学习曲线;
  • 业务迭代速度加快,新功能开发不再受制于臃肿的单体结构;

最重要的是,我们现在有了更高的技术掌控力。无论是突发流量的应对,还是未来的架构升级(比如 Serverless 方向探索),我们都有了更强的技术储备。


经验分享:几点建议送给同行们

如果你也在经历类似的架构演进,我想给你几点切身体会的建议:

  1. 不要一开始就追求“大而全”:先从最小可行性架构做起,比如从模块化入手,再逐步拆服务;
  2. 关注技术债和自动化建设:良好的 CI/CD、监控告警、日志跟踪体系,远比一时的功能上线更重要;
  3. 重视团队沟通和文档沉淀:技术架构变化快,只有文档和知识库跟得上,团队才能持续发展;
  4. 选择适合自己的技术栈:Spring Cloud、Dubbo、Kubernetes、Istio 这些都是很好的工具,但要用对地方;
  5. 保持开放心态,持续学习:云原生生态日新月异,及时跟进新技术才能始终保持竞争力;

还记得我刚开始接手这次架构升级时,内心是非常焦虑的,毕竟这是个牵一发动全身的大工程。但正是在这次挑战中,我不仅提升了技术能力,也学会了如何带领团队一起完成技术变革。这段经历对我来说,是一次真正的成长。


结语:技术是手段,不是终点

最后想送给大家一句话:“架构服务于业务,技术服务于人”。我们做架构演进,不是为了追赶潮流,而是为了支撑公司的发展和用户的体验。技术本身没有高低之分,适合业务场景的才是最好的。

希望这篇分享能够对你有所启发。如果你也有类似的架构演进经历,欢迎留言交流。我们一起,把更好的软件交付给这个世界。


本文作者:李明 | 资深后端工程师 | 专注企业级系统架构与云原生方向

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝