从单体到云原生:一个后端架构演进的真实故事

云端小木屋
2025-06-27 21:48
阅读 245

引言

我是李工,一名在互联网公司摸爬滚打了7年的后端开发工程师。过去这些年,我亲身经历了多个项目从最初几十行代码的“玩具系统”发展为支撑千万级用户访问的复杂系统。其中一次最深刻、也最有挑战性的经历,是我们团队推动将公司主平台从传统单体架构微服务 + 容器化云原生架构迁移的过程。

这次重构不是拍脑袋的决定,而是在现实业务中不断遇到瓶颈后的无奈之举。今天我想分享这段经历,包括为什么我们要迁移到云原生、过程中踩了哪些坑、如何一步步解决问题,以及最终获得的实际收益。如果你正面临系统性能瓶颈、部署效率低下或者维护成本陡增的问题,也许这篇文章能给你带来一些启发。


一、最初的单体架构与暴露出的问题

我们的核心产品是一个面向企业用户的SaaS平台,早期采用的是传统的单体架构:

  • 后端使用Spring Boot框架开发
  • 数据库使用MySQL+Redis组合
  • 部署方式是Nginx反向代理+Tomcat
  • 所有业务模块打包成一个WAR包进行部署

初期的“甜蜜时光”

刚开始那一年一切都很顺利:需求变更快,上线流程简单,本地开发调试也很方便。我们甚至可以一天上线好几个版本,整个系统像一把瑞士军刀,小巧又实用。

瓶颈初现

随着用户数增长(从几万到百万),问题开始逐渐显现:

  1. 部署时间过长:每次发布新版本都要重启整个服务,动辄需要几分钟。
  2. 功能耦合严重:订单模块改一个小bug可能影响权限模块。
  3. 资源利用率失衡:日志和搜索等非核心模块吃掉大量CPU内存。
  4. 无法独立伸缩:高峰期并发量上来时只能整体扩容,浪费严重。
  5. 测试困难:本地启动环境依赖多,新人上手慢,测试覆盖率低。

有一次我们上线了一个支付模块的小改动,结果由于数据库连接池配置错误,直接导致整个应用挂掉。那次故障持续两个小时,损失了好几个大客户,教训十分惨痛。


二、决定重构:为什么选择云原生?

这个时候我们意识到不能再继续死守单体架构了。我们需要的是高可用性弹性扩展能力快速迭代机制,以及更高效的运维能力——而这正是云原生架构所擅长的地方。

我们在技术选型上做了深入调研和对比:

技术维度 单体架构 微服务+K8s
部署速度 慢(全量部署) 快(按服务部署)
故障隔离
资源利用率
扩展性 弱(横向) 强(可单独扩展)
开发协作 易于协作但易冲突 分布开发,需统一规范
运维复杂度

虽然云原生的学习曲线陡峭,但它的优势太明显了。结合当时我们公司正好也在往阿里云迁移,我们最终决定采用以下技术栈:

  • Spring Cloud Alibaba + Nacos 作为注册中心
  • Kubernetes 作为容器编排
  • Docker 容器化部署
  • Prometheus + Grafana 监控方案
  • ELK 日志收集体系
  • SkyWalking 链路追踪

三、重构过程:从拆分到集成

这个项目由我负责整体架构设计和技术选型,前后历时6个月,分为三个阶段:

阶段一:服务拆分与边界定义

我们首先对原有系统进行了详细的业务划分,按照领域驱动设计(DDD)原则,把系统拆分为以下几个关键服务:

  • 用户服务(User)
  • 权限服务(Auth)
  • 订单服务(Order)
  • 支付服务(Payment)
  • 搜索服务(Search)
  • 日志服务(Log)

每个服务都拥有独立的数据库实例(部分读场景共享)。为了保证接口稳定性,我们采用了OpenAPI标准,并使用Swagger生成文档供前端调用。

举个例子,原来的权限检查逻辑是写在各服务中的公共方法,现在统一抽离到Auth Service:

// Auth Client 示例
@FeignClient(name = "auth-service")
public interface AuthServiceClient {

    @PostMapping("/check-permission")
    boolean checkPermission(@RequestParam("userId") Long userId, 
                            @RequestParam("resource") String resource);
}

阶段二:基础设施建设

有了服务划分之后,下一步就是搭建基础架构。我们选择了Kubernetes + Helm的部署方式,并结合CI/CD流水线自动化构建镜像并部署。

K8s的核心YAML如下(以User Service为例):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: registry.example.com/user-service:latest
        ports:
        - containerPort: 8080
        resources:
          limits:
            memory: "1Gi"
            cpu: "1"
      imagePullSecrets:
      - name: regcred

此外,我们还配置了自动扩缩容策略(HPA)来应对流量波动:

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: user-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: user-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

阶段三:可观测性体系建设

微服务架构示意图-2

重构后最大的痛点之一是监控和排查难度上升。所以我们重点投入了可观测性体系建设:

  • 接入Prometheus做指标采集,通过ServiceMonitor自动发现微服务
  • 使用Grafana建立大盘看板,展示QPS、响应时间、错误率
  • 接入ELK集中管理日志,提升故障定位效率
  • 使用SkyWalking接入链路追踪,定位跨服务调用问题

比如我们通过SkyWalking可以看到某次请求经过的完整链路,清晰地识别出瓶颈所在:

SkyWalking截图示意


四、实战踩坑经验分享

这6个月的重构并非一帆风顺,遇到了很多预料之外的坑,我也愿意把这些真实的教训和你分享:

❗坑1:跨服务事务一致性难题

一开始我们以为只要把数据拆开就能万事大吉,结果很快就在订单支付流程里遇到了问题:支付成功了,但库存没扣减。

解决办法:

  • 对核心业务引入TCC分布式事务框架(如Seata)
  • 对最终一致性要求高的业务采用异步补偿机制
  • 所有写操作都加上幂等处理

建议:能不跨服务就别跨服务,跨服务接口尽量做成幂等且支持重试。

❗坑2:网关压力过大

微服务初期我们把所有外部请求都打到Gateway,结果不到几天,网关就成了瓶颈。

分析发现,某些服务没有限流策略,加上存在恶意扫描攻击,造成网关负载暴涨。

解决方案:

  • 在Kong网关层加了基于IP和接口粒度的限流策略
  • 结合Sentinel实现熔断降级机制
  • 针对高频查询接口增加缓存层(Redis)

❗坑3:K8s调度不当引发雪崩效应

有一次半夜突发流量高峰,集群自动扩了几十个Pod,但由于节点资源不足,部分Pod长时间处于Pending状态,进而触发级联失败。

我们紧急优化了几个点:

  • 设置Pod优先级和服务容忍度,合理分配资源
  • 调整Horizontal Pod Autoscaler的阈值,避免盲目扩容
  • 启用了Cluster Autoscaler自动调整节点池大小

五、重构后的成果与收益

经过三个月的稳定运行,我们取得了显著的改善效果:

指标 重构前 重构后
上线频率 1周/次 1天/次(核心服务)
平均部署耗时 5分钟 <1分钟
故障影响范围 全局瘫痪 局部服务异常
故障恢复时间 30分钟 <5分钟
CPU利用率 30%~90%波动 稳定在60%左右
成本节省 节省约30%服务器成本

更重要的是,开发体验明显提升:

  • 新人可以在半天内完成本地环境搭建
  • 模块之间互不影响,开发效率大幅提升
  • 可以根据业务特性灵活定制不同服务的伸缩策略

有一次我们在国庆期间临时扩容某个促销相关的服务,仅用了半小时就准备就绪,这种灵活性是以前不敢想象的。


六、我的一些建议与思考

数据库设计模型-1

如果你正在考虑是否要进行云原生转型,这里是我的几点建议:

✅ 什么时候适合做微服务?

  • 当你的单体应用已经难以支撑频繁的迭代节奏
  • 多人协作出现明显阻碍(比如多人修改同一份代码冲突频发)
  • 需要做精细化扩缩容控制
  • 需要高可用性和灾备能力

⚠️ 什么情况下应该谨慎?

  • 小团队小项目,前期不必过度设计
  • 技术团队缺乏分布式系统经验
  • 缺乏良好的DevOps和CI/CD流程支撑
  • 没有专业的运维或SRE团队

💡 一些实用技巧

  1. 保持接口简洁,提前做版本管理

    @RequestMapping("/v1/users")
    public class UserControllerV1 {
        ...
    }
    
  2. 所有服务强制接入健康检查接口

    @GetMapping("/actuator/health")
    public String health() {
        return "OK";
    }
    
  3. 合理设置超时和熔断机制

    feign:
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 10000
    sentinel:
      transport:
        dashboard: localhost:8080
    
  4. 日志必须包含traceId和requestId 我们自研了一个Trace Filter,用于串联整个调用链路


结语

回过头来看,这次架构升级对我们团队来说不仅仅是技术层面的跃迁,更是组织能力和工程文化的全面提升。它迫使我们建立起更完善的文档体系、更强的自动化流程,以及更严谨的服务治理意识。

云原生不是一个终点,而是一次思维方式的转变。它教会我在设计之初就要考虑解耦容错可观测这些关键词。

希望这篇真实的技术演进记录能对你有所帮助。如果你也有类似的经历或困惑,欢迎留言交流。毕竟,作为一个开发者,成长之路总是伴随着不断的重构与反思。

评论 0

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