后端架构演进:从单体到云原生,我这五年的成长之路

算法边缘人
2025-06-13 00:32
阅读 582

背景介绍:为什么我要谈这个话题?

背景介绍:为什么我要谈这个话题?

我是小李,一名有着五年后端开发经验的程序员。记得刚入行那会儿,公司还用的是传统的单体架构,所有代码都在一个项目里,部署在一个 Tomcat 上。那时候的我们每天就是写业务逻辑、打补丁、修 Bug,日子过得也算安逸。直到后来,业务越做越大,用户越来越多,系统开始频繁宕机、响应变慢、上线出错……

那阵子,我经常半夜被监控告警叫醒,心里苦不堪言。也是从那时起,我才真正意识到:架构不是为了炫技,而是为了解决实际问题。

于是我们开始尝试拆分服务、引入微服务、拥抱云原生。一路走来,踩过不少坑,也积累了不少经验。今天我想结合自己的真实经历,和大家聊聊:

“从单体应用,到云原生架构,我们是怎么一步步演进过来的?”


一、最初的起点 —— 单体架构带来的挑战

一、最初的起点 —— 单体架构带来的挑战

项目背景

我参与的第一个中大型项目是一个电商平台。前端是 Vue,后端是 Spring Boot,数据库用 MySQL + Redis,部署在一台 8C16G 的服务器上。

当时整个项目都是一个 Maven Module,包含了用户、订单、商品、支付等多个模块。接口设计也很简单粗暴,一个 Controller 包含多个方法,Service 层直接调用 DAO 操作数据库。

遇到的问题

随着用户量上涨,我们很快就遇到了以下几个棘手的问题:

  1. 部署困难:

    • 每次上线都要全量打包部署,时间长、风险大
    • 小改一个小功能也要重启整个服务,影响其他模块
  2. 性能瓶颈:

    • 所有请求都打到同一个 JVM,线程池不够用
    • 数据库连接池资源争抢严重,导致响应延迟高
  3. 维护成本高:

    • 代码臃肿复杂,新人上手困难
    • 接口之间耦合度高,一处修改,处处受影响
  4. 稳定性差:

    • 出现内存溢出、线程死锁等异常,整个服务就挂了
    • 监控和日志分散,定位问题困难

有一次因为一个 SQL 写错了 left join,把整个订单查询页面拖垮了,连带着首页都加载不出来。客户投诉电话接了一上午,那次教训真的刻骨铭心。


二、第一次升级 —— 模块化改造 + 垂直拆分

系统架构设计图-1

架构设计思路

为了避免动不动就全量发布,我们决定先做一个基础改进:按业务垂直拆分模块,但不彻底独立成服务。

也就是将原来的大工程,按业务切分成多个 Maven Module:

project/
├── user-service/       # 用户模块
├── product-service/    # 商品模块
├── order-service/      # 订单模块
├── payment-service/    # 支付模块
└── common/             # 公共类和工具包

每个模块独立封装 Service 和 Repository,通过 Spring 的 @Import 或依赖注入的方式进行集成。这样虽然还是单体服务,但结构更清晰,便于后续进一步拆解。

效果与局限性

这次改造让我们实现了两个目标:

  • 发布更安全了:每次只改动相关模块的代码,降低出错概率
  • 代码更好维护了:各团队分工明确,各自负责自己模块的功能开发

但本质还是一个服务,所以当访问量继续上升时,性能瓶颈依然存在,尤其是在促销期间,服务动不动就卡顿甚至挂掉。


三、第二阶段:迈入微服务时代

架构升级方案

我们决定正式迈出关键一步——将各个业务模块拆分为独立的微服务。

技术选型如下:

  • 注册中心:使用 Alibaba Nacos
  • 远程通信:Spring Cloud Feign + Ribbon
  • 配置中心:Nacos Config
  • 网关层:Spring Gateway
  • 日志收集:ELK(Elasticsearch + Logstash + Kibana)
  • 链路追踪:Sleuth + Zipkin
  • 数据库:按业务分别建库,部分做了读写分离

数据库设计优化

以前所有数据都放在一个库中,随着表数量增加,索引混乱,维护成本极高。我们在微服务阶段做了几点优化:

  • 每个业务使用独立数据库,保证数据隔离
  • 使用 MyCat 实现简单的水平分表(比如用户和订单)
  • 对热点数据引入 Redis 缓存(如商品信息、库存)

网络模型调整

原来的网关用的是 Nginx 反向代理,现在换成 Spring Gateway 做统一入口路由。它支持动态配置更新,也能配合 Nacos 做负载均衡。

项目效果

拆完之后,明显感觉几个好处:

  • 服务部署灵活了:哪个服务出问题,单独重启就行,不影响其他功能
  • 性能提升了:不同服务可以部署在不同机器上,压力分散
  • 团队协作效率提高了:每个团队专注自己的服务,不再互相干扰

但新的问题也随之而来。


四、微服务之痛:分布式带来的一系列挑战

主要遇到的问题

  1. 服务治理难:

    • 服务注册与发现容易失联,需要手动处理下线重连
    • Feign 接口调用超时、重试策略没处理好,偶尔会引发雪崩效应
  2. 事务一致性难以保证:

    • 下单操作涉及到用户余额扣减、库存减少、生成订单等多个服务操作
    • 最初我们用了本地事务+回调机制,结果失败率非常高,最后被迫引入了 TCC 分布式事务框架
  3. 部署和运维复杂:

    • 每个服务都需要启动 jar 包、配环境变量、管理日志路径……
    • 定位问题靠翻日志,效率低下,而且服务太多,记不清谁部署在哪台服务器上
  4. 性能波动不定:

    • 在大促高峰期,服务实例扩容慢,不能自动伸缩,还得人工加机器
    • 某些服务调用链太长,响应慢成了常态

有一年双十一,我们临时加了十几台服务器才扛住流量,事后复盘才发现很多服务其实是空跑状态,根本没必要那么多资源。


五、迈向云原生:容器化 + K8s 自动化编排

为什么要上 Kubernetes?

我们意识到必须让服务具备弹性扩缩容能力,才能应对未来的高并发场景。

于是我们开启了新一轮的技术迭代:服务容器化 + Kubernetes 自动编排部署。

关键技术点:

  • 使用 Docker 将每个微服务打包成镜像
  • 利用 Helm Chart 统一管理部署配置
  • 部署到 K8s 集群,实现 Pod 自动调度
  • 使用 Ingress 控制对外访问入口
  • 配合 Prometheus + Grafana 监控系统指标
  • ELK 改造为 EFK(加入 Fluentd)用于日志采集分析

示例:Dockerfile 配置片段

FROM openjdk:8-jdk-alpine
COPY *.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

示例:Kubernetes Deployment 配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
        - name: product-service
          image: registry.example.com/product-service:latest
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: product-service-config

数据库设计模型-2

配置中心迁移

我们将配置文件全部迁移到 ConfigMap 中,并通过 Nacos 做运行时热更新,实现了无需重新部署即可变更配置。


六、实践中的坑和解决方法

坑一:服务注册不上,或者反复上下线

刚开始用 Nacos 的时候,有些服务总是注册不上去,或者心跳丢失导致被踢除。

解决方案:

  • 加大心跳间隔,默认 5 秒,改为 10 秒
  • 提升健康检查频率,防止误删实例
  • 给每个节点加上探针检测 /health 接口,提高可用性判断准确率

坑二:Feign 调用不稳定,出现 Timeout

有时候某个服务响应时间突然飙升,导致整个调用链崩溃。

解决方案:

  • 给 Feign 添加熔断器(Hystrix),设置 fallback 方法
  • 使用 Resilience4j 增加限流和降级策略
  • 设置合适的超时时间,不要一刀切默认值

坑三:日志收集不完整,查不到报错日志

EFK 搭建完成后,部分日志迟迟采集不到,或者格式混乱。

解决方案:

  • 统一日志输出格式(采用 JSON 格式)
  • Fluentd 增加字段解析插件
  • 给每个服务指定不同的日志路径,避免冲突

坑四:Prometheus 监控指标不准

刚开始部署监控的时候,很多指标看起来都是 0,或者数值不准确。

解决方案:

  • 给服务添加 Actuator 监控接口
  • 检查 metrics 是否暴露正确端口和路径
  • 适当调整 Scrape 配置的时间间隔

七、最终成果和收获

从最开始的单体架构,到现在的云原生部署体系,我们团队实现了几个重大转变:

  1. 系统更稳定了:

    • 服务自我恢复能力强,即使个别 Pod 挂掉也不影响整体可用性
    • 异常告警响应迅速,问题可快速定位
  2. 部署效率提升了:

    • 新服务上线只需提交一次 Git,CI/CD 流水线自动构建部署
    • 支持灰度发布、回滚等高级操作
  3. 资源利用率更高:

    • HPA 自动扩缩容,节省了大量服务器资源
    • 资源分配更合理,避免了浪费
  4. 团队协作更高效:

    • 每个服务都有对应负责人,责任明确
    • 新人更容易理解架构,快速上手开发

八、我的一些经验和建议

作为过来人,我觉得对正在做架构演进的朋友,有几个重要的建议:

1. 不要一开始就追求完美架构

很多人上来就想搞中台、搞服务网格、搞 DDD,其实大可不必。

架构是为了解决当前的问题。先从小处做起,循序渐进才是正道。

我们当年就是太急于求成,想着一次搞定所有问题,结果各种组件没搭好反而拖慢进度。

2. 多关注性能和稳定性细节

很多时候问题不是出在架构设计本身,而是在细节上。

比如:

  • 线程池配置不合理导致线程阻塞
  • 数据库索引缺失导致查询缓慢
  • 日志级别没控制好刷爆磁盘

这些看似不起眼的小事,往往才是压垮系统的最后一根稻草。

3. 线上环境要有完善的监控和报警机制

我们吃过很大的亏,很多线上问题是用户反馈之后才知道的。

后来我们建立了完整的监控体系:

  • 应用层面的接口调用量、耗时
  • 数据库慢查询、CPU 负载
  • Redis、RabbitMQ 连接数、队列积压情况

这些监控项帮助我们提前预警了很多潜在问题。

4. 文档一定要跟上

每次架构变动,别忘了及时更新文档!

否则下次再接手的人,面对一堆 Kubernetes 配置和服务定义,根本不知道怎么下手。

我们现在的做法是:

  • 每个服务都有 README.md 说明用途和启动方式
  • 使用 Confluence 维护架构图、调用关系、部署流程
  • CI/CD 脚本也同步保存在 GitLab 中,方便查看历史变更

结语:架构的演化,是一条不断试错、持续打磨的路

这五年,从单体架构到如今的云原生体系,我们的系统越来越强壮,我也从中学到了很多宝贵的经验。

有时候回头看,那些深夜加班调试、排查问题的日子虽然辛苦,但也正是这些点滴积累,让我真正成长为一名成熟的后端工程师。

如果你也在架构演进的路上,希望这篇文章能带给你一点启发和帮助。记住:

不要害怕改变,也不要急着追求时髦。一切技术方案的核心,始终是要解决你当下遇到的实际问题。

与君共勉 🙏


如有兴趣交流更多实战经验,欢迎留言或私信探讨!

评论 0

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