从单体到微服务:一个考公程序员的架构血泪史

一行代码半杯茶
2025-12-21 15:27
阅读 1666

上周五晚上十点半,我戴着耳机听着《加州旅馆》remix版,一边改着线上bug,一边刷着粉笔APP的行测题——这就是我在上海某电商公司做后端开发的日常。白天写Java,晚上刷申论,中间还得抽空啃几页《Spring Cloud实战》,毕竟“上岸”才是终极KPI。

而说到Spring Cloud,就不得不提我们组去年那个史诗级项目:把一个跑了五年的单体应用拆成微服务。产品经理嘴上说“只是小改”,结果上线前一周才甩过来三百多页PRD;测试同学天天在群里@我说“这个接口又500了”;运维大哥则一脸冷漠:“你们自己搞定注册中心吧,我还要去救火双11的数据库”。

但正是这场“拆弹行动”,让我真正理解了什么叫微服务不是银弹,而是责任

为什么非得拆?

我们的老系统是个典型的Java单体应用:Spring Boot + MyBatis + MySQL,前端是Vue + Javascript。代码量超过30万行,启动时间4分半,打包一次Jenkins要跑20分钟。最要命的是,每次改个优惠券逻辑,都得全量回归——因为没人敢保证没动到订单模块。

去年618大促前夕,系统直接崩了。原因?用户登录模块的一个N+1查询,拖垮了整个DB连接池。那一刻,我盯着监控面板上飙升的CPU和满屏的Too many connections错误,心里只有一个念头:这玩意儿再不拆,我就要陪它一起进ICU了

领导拍板:微服务化,三个月内上线。于是,我和两个同事开启了“白天写业务、晚上学架构”的地狱模式。

拆!但怎么拆?

很多人以为微服务就是把模块切成一个个jar包,部署到不同服务器上。Too young。真正的难点在于边界划分

我们一开始按技术分层切:用户服务、商品服务、订单服务……听起来很合理,对吧?结果第一周就翻车了。比如“下单”这个动作,涉及库存扣减、优惠计算、积分发放、消息通知……五个服务来回调用,一个失败就得全回滚。分布式事务搞到头皮发麻,Saga模式写得我怀疑人生。

后来我们改用领域驱动设计(DDD) 的思路,以业务能力为单位划分。比如把“营销”作为一个独立上下文,包含优惠券、满减、秒杀等子域。这样,下单时只需要调用营销服务的一个接口,内部逻辑由它自己协调。

教训:微服务的边界不是技术边界,而是业务语义边界。别让“服务”变成“分布式单体”。

Java生态下的微服务工具链

作为Java程序员,我们自然选择了Spring Cloud全家桶。但版本兼容问题差点让我放弃考公直接出家。

组件 初期选型 后来换掉的原因
Eureka 2.0.x 官方已停更,社区支持弱
Zuul 1.x 性能瓶颈明显,不支持异步
Hystrix - Netflix已归档,转向Resilience4j

最终我们定型为:

  • 注册中心:Nacos(国产之光,支持配置中心)
  • 网关:Spring Cloud Gateway(基于WebFlux,性能吊打Zuul)
  • 熔断限流:Sentinel(阿里开源,控制台超友好)
  • 链路追踪:SkyWalking(无侵入,比Zipkin好用太多)

举个Gateway的路由配置例子:

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service  # 基于Ribbon的负载均衡
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1  # 去掉/api前缀
            - RequestRateLimiter=redis-rate-limiter  # 限流

这里踩了个坑:lb://协议必须配合spring-cloud-starter-loadbalancer,否则会报No instances available。文档没写清楚,查了一天才发现是依赖漏了。

数据库怎么办?别让SQL成为瓶颈

微服务化后,每个服务都有自己的数据库,这是基本原则。但我们一开始图省事,让用户服务和订单服务共用同一个MySQL实例的不同库。结果大促时,订单写入高峰直接把用户库的IO打满,登录接口全线超时。

后来彻底隔离:

  • 用户服务 → user_db
  • 订单服务 → order_db
  • 商品服务 → product_db

但新问题来了:跨服务查询怎么办?比如“我的订单”页面要显示商品图片和用户昵称。

我们尝试过三种方案:

  1. 前端聚合:Javascript分别调三个接口再拼数据 → 首屏加载慢,用户体验差
  2. BFF层:新增一个web-api服务专门做聚合 → 增加维护成本
  3. CQRS + 事件溯源:订单创建后发Kafka消息,用户服务消费并更新本地冗余表 → 最终一致,但实现复杂

最后折中:高频场景用缓存预热。用户进入订单页前,先异步拉取关联数据存Redis。虽然有短暂不一致,但99%的场景够用了。

那些年被面试官问烂的微服务面试题

自从开始搞微服务,我参加的每场面试必被问:

“你们怎么保证分布式事务一致性?”

我以前背八股文答“Seata AT模式”,现在会反问:“业务能接受最终一致吗?如果能,用消息队队列+幂等就行。”

另一个高频题:

“服务雪崩怎么防?”

除了常规的熔断降级,我们还做了依赖分级:核心链路(如下单)走高配集群,非核心(如日志上报)走低配。Sentinel里配置了不同的QPS阈值,大促时自动丢弃边缘流量。

其实,最好的容灾是压根不让故障发生。所以我们上了全链路压测,每周日凌晨两点自动跑脚本模拟双11流量。虽然被吵醒很多次,但至少不用在半夜接电话救火。

Javascript前端也得跟上节奏

别以为微服务只是后端的事。前端如果不调整,照样会拖后腿。

以前单体时代,一个页面一个API搞定。现在要调五六个服务,HTTP请求爆炸。我们做了两件事:

  1. GraphQL网关:前端声明需要哪些字段,后端按需聚合。虽然增加了网关复杂度,但减少了70%的无效数据传输。
  2. 微前端:用Module Federation把用户中心、订单中心拆成独立子应用。开发时互不影响,发布时各自上线。

不过说实话,微前端这玩意儿水太深。Webpack5的配置文档看得我头秃,最后还是靠抄同事的webpack.config.js才跑起来。

上岸路上的技术沉淀

如今,系统稳定运行半年多,QPS从800提升到5000+,部署频率从月更变成天更。虽然偶尔还会被运维吐槽“你们服务太多,Prometheus指标爆炸”,但至少我不用再通宵修那个该死的单体应用了。

回看这段经历,最大的收获不是技术本身,而是对复杂性的敬畏。微服务不是炫技,而是为了解耦、提速、容错。如果你的团队连CI/CD都没搞好,就别急着上K8s——先把基础打牢。

写这篇文章时,窗外上海的雨下个不停。耳机里换成了《夜曲》,手边是刚泡好的枸杞茶。明天还要早起去线下班刷题,但今晚,让我再为这个亲手拆解又重建的系统点个赞。

毕竟,在代码与申论之间反复横跳的日子,也是另一种“上岸”吧。

评论 0

最热最新
暂无评论
一行代码半杯茶Lv.1
0
影响力
0
文章
0
粉丝