从单体到微服务:一个考公程序员的架构血泪史
上周五晚上十点半,我戴着耳机听着《加州旅馆》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
但新问题来了:跨服务查询怎么办?比如“我的订单”页面要显示商品图片和用户昵称。
我们尝试过三种方案:
- 前端聚合:Javascript分别调三个接口再拼数据 → 首屏加载慢,用户体验差
- BFF层:新增一个
web-api服务专门做聚合 → 增加维护成本 - CQRS + 事件溯源:订单创建后发Kafka消息,用户服务消费并更新本地冗余表 → 最终一致,但实现复杂
最后折中:高频场景用缓存预热。用户进入订单页前,先异步拉取关联数据存Redis。虽然有短暂不一致,但99%的场景够用了。
那些年被面试官问烂的微服务面试题
自从开始搞微服务,我参加的每场面试必被问:
“你们怎么保证分布式事务一致性?”
我以前背八股文答“Seata AT模式”,现在会反问:“业务能接受最终一致吗?如果能,用消息队队列+幂等就行。”
另一个高频题:
“服务雪崩怎么防?”
除了常规的熔断降级,我们还做了依赖分级:核心链路(如下单)走高配集群,非核心(如日志上报)走低配。Sentinel里配置了不同的QPS阈值,大促时自动丢弃边缘流量。
其实,最好的容灾是压根不让故障发生。所以我们上了全链路压测,每周日凌晨两点自动跑脚本模拟双11流量。虽然被吵醒很多次,但至少不用在半夜接电话救火。
Javascript前端也得跟上节奏
别以为微服务只是后端的事。前端如果不调整,照样会拖后腿。
以前单体时代,一个页面一个API搞定。现在要调五六个服务,HTTP请求爆炸。我们做了两件事:
- GraphQL网关:前端声明需要哪些字段,后端按需聚合。虽然增加了网关复杂度,但减少了70%的无效数据传输。
- 微前端:用Module Federation把用户中心、订单中心拆成独立子应用。开发时互不影响,发布时各自上线。
不过说实话,微前端这玩意儿水太深。Webpack5的配置文档看得我头秃,最后还是靠抄同事的webpack.config.js才跑起来。
上岸路上的技术沉淀
如今,系统稳定运行半年多,QPS从800提升到5000+,部署频率从月更变成天更。虽然偶尔还会被运维吐槽“你们服务太多,Prometheus指标爆炸”,但至少我不用再通宵修那个该死的单体应用了。
回看这段经历,最大的收获不是技术本身,而是对复杂性的敬畏。微服务不是炫技,而是为了解耦、提速、容错。如果你的团队连CI/CD都没搞好,就别急着上K8s——先把基础打牢。
写这篇文章时,窗外上海的雨下个不停。耳机里换成了《夜曲》,手边是刚泡好的枸杞茶。明天还要早起去线下班刷题,但今晚,让我再为这个亲手拆解又重建的系统点个赞。
毕竟,在代码与申论之间反复横跳的日子,也是另一种“上岸”吧。

评论 0