从单体到云原生:一个准打工人的真实架构演进血泪史

曹桂英○
2026-01-05 04:23
阅读 792

去年秋招拿到 offer 后,我本以为能躺平到入职。结果导师一句“提前熟悉下公司技术栈”,直接把我推进了后端架构的深水区。作为普通一本 CS 大四狗,平时在学校写写 Spring Boot 单体应用、跑个 MySQL 就觉得自己很牛了,直到真正接触生产环境——才知道什么叫“理想很丰满,线上很骨感”。

现在远程在家办公,每天对着 MacBook Pro 写代码,偶尔切到 Windows 虚拟机测兼容性(没错,我就是那个“Mac 开发,Windows 只用来测试”的典型)。最近帮实习团队重构一个老项目,从单体一路折腾到云原生,中间踩的坑比我大学四年 debug 的总和还多。今天就来聊聊这段“血与火”的实战经历,顺便回应一下最近被问爆的面试题挑战——“你们项目是怎么从单体演进到微服务的?”


起点:那个“能跑就行”的单体应用

故事要从我们组的老古董项目说起。这是个内部运营系统,2018 年用 Spring Boot + MyBatis + MySQL 搞的,代码全塞在一个 repo 里,启动一次要 45 秒,打包 jar 文件 80MB+。功能倒是不复杂:用户管理、订单处理、报表导出。但问题在于——所有逻辑耦合在一起

比如,用户注册接口里居然要调用风控服务(硬编码 IP)、发邮件(同步阻塞)、写审计日志(直连另一个 DB)。更离谱的是,报表导出功能一跑,整个 JVM 内存飙升,GC 频繁,其他接口直接超时。

产品经理上周五晚上 9 点在群里 @ 我:“明天上线新活动,用户量预估翻三倍,系统撑得住吗?”
我当时看着监控面板上那根快触顶的 CPU 曲线,心里只有一个念头:这玩意儿根本扛不住


第一步:拆!但别瞎拆

很多人一听到“微服务”就热血沸腾,恨不得把每个函数都拆成独立服务。但现实是——拆错了比不拆更惨

我们一开始也犯了这个错误。有个实习生直接按 Controller 拆,搞出 10+ 个小服务,结果发现:

  • 用户服务要调订单服务查历史记录
  • 订单服务又要回调用户服务更新状态
  • 两个服务互相依赖,形成死锁式循环调用

上线当天下午,系统雪崩。运维大哥在 Slack 里咆哮:“又是你们后端搞的鬼?!”
我默默打开日志,看到满屏的 FeignException: Read timed out,真的想砸电脑。

后来痛定思痛,我们决定按业务边界 + 数据一致性来拆:

  • 用户中心:负责注册、登录、权限
  • 订单引擎:处理下单、支付、状态流转
  • 报表服务:异步生成、缓存结果

关键是——每个服务拥有自己的数据库,彻底杜绝跨库 join。虽然初期数据同步有点麻烦(后面会讲),但至少避免了“一个 SQL 拖垮整个系统”的惨剧。

💡 面试题挑战小贴士:当面试官问“怎么划分微服务边界”,别只说 DDD(领域驱动设计)这种高大上的词。结合你项目的真实业务场景,比如“我们发现用户信息变更和订单创建没有强事务依赖,所以拆开”。落地细节才是加分项


中间件选型:稳定 vs 新潮?我选稳定!

作为一个喜欢折腾新技术的人(本地 Docker Desktop 跑着十几个实验性项目),我一度想上 Service Mesh + gRPC + etcd 全家桶。但带我的 senior 直接泼冷水:“生产环境不是 playground。”

最后我们选择了保守但可靠的组合:

  • 服务通信:Spring Cloud OpenFeign(基于 HTTP/JSON)
  • 服务发现:Nacos(比 Eureka 更轻量,支持配置中心)
  • 熔断限流:Sentinel(阿里开源,规则动态可配)
  • 消息队列:RabbitMQ(团队熟悉,运维有经验)

举个例子,报表导出原来同步执行,现在改成:

// 用户点击“导出”按钮
@PostMapping("/export")
public ResponseEntity<Void> triggerExport(@RequestBody ExportRequest request) {
    // 1. 校验参数
    // 2. 发送 MQ 消息
    rabbitTemplate.convertAndSend("report.queue", request);
    // 3. 立即返回,前端轮询结果
    return ResponseEntity.accepted().build();
}

报表服务监听队列,异步处理,结果存 Redis。用户刷新页面就能看到下载链接。响应时间从 30s+ 降到 200ms 以内,而且再也不怕大查询拖垮主线程。


数据库拆分:分库分表不是万能药

拆服务之后,数据库压力依然很大。订单表半年就涨到 2000w 行,SELECT * FROM orders WHERE user_id = ? 这种简单查询都要 2s+。

我们评估过分库分表(ShardingSphere),但发现业务还没到那个量级——日活才 5w,纯属过早优化。于是先做了三件事:

  1. 读写分离:主库写,从库读(报表类查询全走从库)
  2. 索引优化:给 user_idcreate_time 加复合索引
  3. 冷热分离:超过 6 个月的订单归档到 orders_archive

效果立竿见影:

优化前 优化后
P99 查询耗时 2100ms P99 查询耗时 80ms
主库 CPU 峰值 95% 主库 CPU 峰值 45%

📌 真实教训:不要为了用分库分表而分库分表。先做好 SQL 审计、索引优化、缓存策略,很多时候瓶颈不在架构,而在烂代码 + 烂 SQL


云原生上车:K8s + Helm + Prometheus

今年春招面试时,几乎每家公司都问:“有云原生经验吗?” 说实话,之前只在本地 Minikube 玩过。但既然要写简历,那就得真上。

我们把服务容器化,用 K8s 编排。初期踩了几个经典坑:

坑 1:健康检查配错

# 错误示范:livenessProbe 用 /health,但服务启动慢
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5  # 太短!服务还没初始化完就被 kill

结果 Pod 不停重启。后来改成:

  • startupProbe 先探活(容忍慢启动)
  • livenessProbe 只在 startup 成功后生效

坑 2:资源限制太紧

resources:
  limits:
    memory: "256Mi"  # JVM 应用直接 OOMKilled

JVM 应用至少留 512Mi,还得配 -XX:MaxRAMPercentage=75.0,否则容器内存和 JVM 内存对不上。

监控不能少

我们用 Prometheus + Grafana 搭了基础监控:

  • Pod CPU/Memory 使用率
  • HTTP 接口 P99 延迟
  • RabbitMQ 队列堆积数

有一次半夜报警:订单服务延迟突增。一看 Grafana,发现是 MySQL 从库同步延迟飙到 10 分钟。赶紧切流量到主库,避免了资损。没有监控的云原生,就像开车不看仪表盘


面试题挑战:我们到底考什么?

最近帮学弟模拟面试,发现很多人对“架构演进”理解太浅。常见的错误回答:

  • “单体不好,微服务好”
  • “用了 Kubernetes 就是云原生”

其实面试官想听的是:

  1. 你遇到了什么具体问题?(比如“大促期间单体应用无法水平扩展”)
  2. 为什么选这个方案?(比如“没选 gRPC 因为团队熟悉 HTTP,降低迁移成本”)
  3. 踩了什么坑,怎么解决的?(比如“服务拆分后分布式事务问题,最终用 Saga 模式+补偿机制”)

我们项目里就遇到分布式事务难题。用户下单要扣库存、创建订单、发优惠券。最初想用 Seata AT 模式,但发现回滚逻辑太复杂。最后采用事件驱动 + 最终一致性

  • 订单服务发布 OrderCreatedEvent
  • 库存服务消费事件,扣减库存
  • 如果失败,重试 + 告警人工介入

虽然不能 100% 实时一致,但业务可接受(用户看到“下单成功”,几秒后库存才扣,总比下单失败强)。


心得:架构演进不是目的,解决问题才是

折腾了三个月,系统终于稳了。双 11 当天,QPS 峰值 5000+,CPU 峰值不到 60%,零故障。运维大哥难得在群里夸了一句:“这次后端没掉链子。”

回顾这段经历,最大的感悟是:不要为了技术而技术。单体架构在小业务场景下完全够用;微服务增加了复杂度,必须有足够收益才值得上;云原生也不是银弹,配不好反而更难维护。

作为即将入职的新人,我深知自己还有很多要学。但至少现在面对“架构演进”类面试题,我能自信地说出:“我在真实项目中做过,知道坑在哪,也知道自己为什么这么选。”


给同路人的建议

如果你也在准备后端岗位,不妨:

  1. 动手搭个 mini 项目:从单体开始,逐步加入注册中心、配置中心、消息队列
  2. 关注运维视角:学会看日志、看监控、写健康检查
  3. 理解业务约束:技术方案永远服务于业务目标,不是炫技

最后自嘲一句:我现在写代码还是会写出 bug,但至少知道怎么快速定位、怎么优雅降级、怎么不让运维半夜打电话骂我了。

共勉,打工人!

评论 0

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