后端架构演进:从单体到云原生的实战思考

唐智
2025-06-17 00:31
阅读 578

引言:一次系统崩溃带来的反思

引言:一次系统崩溃带来的反思

事情发生在三年前的一个平常工作日,我们负责的一套核心业务系统突然挂掉了。当时的情况非常紧急:用户投诉、客服电话被打爆、值班运维人员手忙脚乱地重启服务,但问题依旧频发。更糟的是,数据库连接池在高峰期直接被打满,后端服务像多米诺骨牌一样接连倒下。

那是一套传统的单体架构,代码库超过百万行,模块之间高度耦合,部署方式是典型的JAR包+Tomcat。每次上线都需要全量更新,测试环境和生产环境差异大,灰度发布基本靠“玄学”。这个项目在我加入团队的时候就存在了,一路走来,虽然不断进行微调,但架构上的根本性问题始终没有解决。

这次事故之后,我们开始着手重构整个后端架构,逐步从单体走向微服务、再到容器化,最终落地于云原生体系。这篇文章想和大家分享的就是这段真实经历中的所思所感。


一、单体架构下的痛点

一、单体架构下的痛点

1.1 初期的美好与后期的噩梦

最初采用单体架构是有其合理性的:

  • 开发速度快,适合初期快速试错
  • 部署简单,一台服务器跑一个JAR包就能上线
  • 不用考虑网络通信、分布式事务等问题

但随着业务发展,几个关键问题浮出水面:

  • 迭代效率低:一个小功能改动可能要牵连多个模块,提测流程动辄两天起步。
  • 资源利用率差:所有接口共享同一个线程池,高并发接口拖垮低频接口。
  • 稳定性差:一个模块内存泄漏或CPU占用飙升,会波及整个系统。
  • 扩缩容困难:扩容只能整体复制,不能按需弹性伸缩。

有一次,我们一个导出报表的功能由于设计不当,占用了大量数据库连接和计算资源,导致下单功能也跟着受影响——这在微服务架构中本不该发生的问题。

1.2 数据库设计的隐痛

数据层面的问题也很突出:

  • 所有表都放在同一张MySQL实例上,表关联严重,SQL语句复杂。
  • 读写分离做得很表面,很多查询依然走主库。
  • 缓存使用不规范,有的接口缓存过期策略混乱,导致缓存击穿。
  • 没有做数据库分片,数据量上来以后查询效率明显下降。

这些因素叠加在一起,形成了一个典型的“技术债黑洞”——你越想改进它,就越发现处处是雷区;而你不改它,又天天被它折磨。


二、第一次拆分:引入微服务架构

缓存策略对比-1

2.1 技术选型与规划

我们决定将单体应用拆分成若干个服务。拆分的原则是按照业务边界来划分(这也是后来踩坑的地方之一)。选型方面:

  • 使用Spring Cloud全家桶作为微服务框架
  • 用Consul做服务注册与发现(后续换成Nacos)
  • Feign做远程调用
  • Zuul做网关(后面换成了Spring Cloud Gateway)
  • Apollo做配置中心

当时还在本地搭建了一整套Kubernetes集群用于预研,但在初期并没有投入实际使用。

2.2 拆分实践中的经验教训

第一次拆分选择了订单、用户、商品三个核心模块,拆分过程其实比想象中复杂得多:

数据一致性问题突显

比如一个新建订单的接口,需要同时操作用户、库存、优惠券等多个服务的数据。一开始我们尝试使用分布式事务(如Seata),结果性能极其糟糕,TPS直接跌了一半不说,还出现了大量事务日志堆积。

最终采用了本地事务+消息队列补偿机制

// 伪代码示例:订单服务处理订单创建
@Transactional
public void createOrder(OrderDTO dto) {
    orderRepository.save(dto);
    
    // 发送事件到消息队列
    messageQueue.send("order.created", new OrderEvent(dto));
}

其他服务订阅该事件,并根据事件异步更新自身数据,通过重试机制保证最终一致性。这种方式虽然牺牲了强一致性,但大大提升了系统的可用性。

接口设计的挑战

微服务之间的调用不像本地方法调用那么随意,必须重视接口契约:

  • 统一返回结构
  • 明确异常定义
  • 版本控制机制(如API路径带版本 /api/v1/user

我们在早期忽视了接口变更的影响范围,导致一次升级引发下游多个服务异常,最终不得不用A/B部署+流量切换的方式临时修复。


三、容器化与编排的引入

3.1 痛点催生改变

微服务拆分完成后,我们迎来了新的挑战:

  • 服务数量暴涨,手工部署和监控难度陡增
  • 环境差异依然存在(开发、测试、生产)配置不一致
  • 扩缩容依然不够灵活,尤其是突发流量场景

这些问题促使我们开始引入Docker和Kubernetes。

3.2 实践中的关键技术点

容器镜像标准化

我们制定了严格的Dockerfile编写规范:

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

并建立了CI/CD流水线,使用Jenkins自动构建镜像、推送至Harbor仓库。

Kubernetes部署优化

起初我们只是简单把服务跑在K8s上,但随着使用深入,陆续做了以下几件事:

  • 水平伸缩:基于HPA自动扩缩副本数,设置合适的资源Limit和Request
  • 滚动更新:设置maxUnavailable、maxSurge参数避免服务中断
  • 健康检查:为每个服务实现合理的Liveness和Readiness探针

比如针对一个敏感的服务,我们这样设置探针:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 15
  periodSeconds: 5

这样的探针逻辑帮助我们减少了因偶发抖动导致的误杀问题。


四、迈向云原生:拥抱基础设施现代化

4.1 架构再进化

当我们完成K8s迁移到阿里云ACK后,才真正体会到什么是“云原生”。

我们做了几个关键调整:

  • 使用SLB代替Zuul做负载均衡
  • 使用OSS替代本地文件存储
  • 使用SLS统一日志收集(之前是ELK)
  • 使用Arms做全链路监控
  • 将MySQL迁移至PolarDB,Redis迁移至云数据库

在这个过程中,最深的感受就是:不要重复造轮子,云厂商提供的产品已经足够强大。

比如我们之前自己搭了一个Prometheus+AlertManager做监控告警,但迁移到云原生之后,直接对接SLS+Arms,省时省力不说,报警准确性、响应及时性都远超自建方案。

4.2 数据库与缓存的再次演化

为了应对更大的数据量,我们做了如下优化:

  • 分库分表:使用ShardingSphere对订单表进行水平拆分
  • 索引优化:通过慢查询日志分析,重建不合理索引
  • 写分离:读取请求走只读实例,减轻主库压力
  • 缓存穿透防护:布隆过滤器 + Redis空值缓存策略

特别值得一提的是,我们曾经遇到过一个热点Key问题:某个爆款商品详情页频繁刷新,导致Redis QPS飙到几十万。解决方案是:

  1. 对热Key做二级缓存:Nginx层做LocalCache
  2. 设置随机TTL:避免大量缓存同时失效
  3. 前置限流:通过Sentinel对特定接口做流量控制

五、开发体验提升:从工具链到协作模式

5.1 全链路压测与观测

我们实现了真正的全链路压测:

  • 用JMeter模拟真实场景
  • 使用Jaeger做链路追踪
  • 压测数据注入线上链路(加特殊标识)

例如,在某次双十一压测中,我们发现了订单支付链路中的瓶颈环节:优惠券验证服务存在同步阻塞行为,最终将其改为异步批量校验,提升了整体吞吐量。

5.2 敏捷协作的转变

微服务带来了更细粒度的职责划分,但也增加了沟通成本。我们采取了两个措施:

  • 接口文档自动化生成:使用Swagger + SpringDoc,配合GitBook生成完整文档
  • 跨服务Mock测试:使用WireMock模拟外部依赖,方便本地联调

另外,我们开始推动DevOps理念,让开发同学更多参与部署与监控环节,打破了“研发-运维”的壁垒。


六、那些年踩过的坑与教训

服务器部署方案-2

6.1 微服务不是银弹

  • 拆分初期我们过度追求“微”,反而造成服务间调用关系混乱
  • 忘记服务治理,导致雪崩效应频发
  • 没有建立良好的服务治理平台,排查问题耗时极长

6.2 容器与虚拟机的区别

  • 最初把容器当虚拟机用,随便装各种组件,镜像臃肿
  • 忽略Pod生命周期管理,出现InitContainer反复失败却不知晓
  • 日志输出不规范,无法集中采集分析

6.3 云原生认知误区

  • 认为上了K8s就是云原生,忽略了Operator、Service Mesh等进阶能力
  • 盲目使用Serverless,结果运行时延迟不达标
  • 过度依赖云平台功能,造成供应商绑定严重

七、成果与收获

如今我们的系统已经稳定运行两年多,经历过几次大促考验。回顾这段旅程,取得了以下成果:

方向 改进前 改进后
部署效率 每次上线需30分钟+ CI/CD全流程约5分钟
服务可用性 平均每月1次故障 99.9% SLA达成
接口响应时间 P99>500ms P99<200ms
运维复杂度 多人专职维护 半个人兼职即可
新业务上线周期 4~6周 1~2周

最重要的是,整个团队的技术视野拓宽了,大家不再局限于编码本身,而是开始思考如何构建一个高可用、可扩展、易运维的系统。


八、给读者的建议

如果你正在面对类似的架构转型或升级挑战,下面几点或许能帮你少走弯路:

  1. 先明确目标再做架构:不是为微服务而微服务,一切以解决现有问题为导向
  2. 循序渐进,小步快跑:可以从领域模型入手,逐步拆分,而不是一刀切
  3. 关注可观测性:日志、监控、链路跟踪尽早接入,后期补救代价巨大
  4. 做好容量评估与演练:压测不仅是测试接口性能,更是检验整个链路的能力
  5. 重视团队赋能:新技术落地的背后是人,培训、知识沉淀、文化适应缺一不可

最后想说一句:好的架构不是设计出来的,是在一次次试错、总结中生长出来的。


作者简介
我在互联网行业从业9年,目前担任某电商平台后端架构负责人。经历过从单机到微服务、从物理机到云原生的完整技术演进,主导过多个亿级流量系统的改造与优化。平时喜欢写代码、研究开源项目,也热爱分享。欢迎交流技术问题或探讨架构思路。

评论 0

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