单体架构撑不住了,我们是怎么一步步“云原生”起来的
上周五晚上十点半,我还在工位上盯着屏幕上疯狂滚动的kubectl logs输出。系统又崩了——不是代码逻辑问题,而是单体应用扛不住流量峰值。当时真想把键盘砸了,但想到下个月要交房租,还是默默泡了杯速溶咖啡,继续 debug。
我是谁?一个在杭州某大厂(你懂的)摸爬滚打五年的后端工程师,日常调参炼丹,Vim 是我的 IDE,.vimrc 比我的简历还长。最近被 Rust 的零成本抽象和内存安全迷得不行,晚上回家总忍不住写几行 tokio::spawn。但白天,我还是得面对现实:用 Java 写业务,用 JavaScript 和前端撕接口文档,然后在 deadline 前修锅。
今天这篇文章,不讲高大上的理论,就说说我们团队从去年双 11 以来,怎么从一个跑在单台 ECS 上的“巨石”应用,一步步演进到如今跑在 Kubernetes 上的云原生架构。过程里踩的坑、熬的夜、和产品吵的架,全给你摊开讲。
起点:那个“能跑就行”的单体应用
三年前刚接手这个项目时,它还是个典型的 Spring Boot 单体:一个 JAR 包,连数据库都用的 H2 内存版(别笑,线上初期真这么干过)。前端用 Vue 写,通过 RESTful API 调后端,一切看起来岁月静好。
但问题很快就来了。随着业务增长,代码库膨胀到 15 万行,每次改个小功能都要全量回归测试。最恐怖的是部署——发一次版,整个服务停机 3 分钟。产品经理每次看到“系统维护中”的页面,眼神都能把我烧穿。
更别说性能瓶颈。所有模块共享同一个 JVM 堆内存,一个慢 SQL 就能让整个服务雪崩。记得有一次促销活动,用户注册接口因为没加索引,CPU 直接飙到 100%,连登录都进不去。运维小哥在群里@我:“兄弟,你这 Java 进程是不是在挖矿?”
那会儿我翻遍了《Spring 实战》《Java 并发编程实战》,甚至把 Martin Fowler 的《重构》当睡前读物,但书里教的是“如何写好单体”,而不是“如何拆掉单体”。
第一步:垂直拆分——微服务不是银弹,但能救命
去年年初,技术总监拍板:必须拆!理由很简单——招聘难。新来的应届生看到 15 万行的单体代码,简历都没投完就跑了。
我们没直接上 Service Mesh,也没搞什么 fancy 的 DDD,而是采用最朴素的垂直拆分:按业务域切。用户中心、订单系统、商品管理……每个模块独立成服务,各自有数据库。
// 用户服务 UserService.java(简化版)
@RestController
public class UserController {
@Autowired
private UserRepository userRepo;
@PostMapping("/register")
public ResponseEntity<User> register(@RequestBody UserRegisterDTO dto) {
// 校验、加密、存 DB
User user = new User(dto.getUsername(), bcrypt.encode(dto.getPassword()));
return ResponseEntity.ok(userRepo.save(user));
}
}
前端那边也不轻松。以前一个 /api/* 代理搞定,现在得配 N 个 proxy target。前端小哥抱怨:“你们后端拆得爽,我 .env 文件都快成配置中心了!” 最后我们统一接入了 API Gateway(用 Spring Cloud Gateway),前端只对接网关,内部路由由网关处理。
但拆完就完事了?天真。
第一个坑:分布式事务。用户注册成功后要发欢迎邮件,但邮件服务挂了怎么办?我们试过 2PC,性能直接砍半;后来改用 本地消息表 + 定时补偿,虽然土,但稳。
第二个坑:服务发现与调用。一开始用 Eureka,结果网络抖动时服务列表不同步,调用方疯狂报 UnknownHostException。后来换成 Nacos,配合 Feign + Ribbon 做负载均衡,总算稳住。
第三个坑:日志追踪。以前 grep 一个 traceId 就能串起全流程,现在 10 个服务,日志散落在各处。紧急引入 SkyWalking,靠 TraceContext 把链路串起来。那天凌晨三点,我看着完整的调用链路图,感动得差点哭出来。
第二步:容器化——让部署不再靠“玄学”
微服务拆完,部署又成了噩梦。每个服务依赖不同版本的 JDK、MySQL 驱动,运维小哥每天在服务器上 apt-get install,活脱脱一个“人肉 Ansible”。
领导一句话:“全部容器化!”
于是我们开始写 Dockerfile:
# 典型的 Java 服务 Dockerfile
FROM openjdk:17-slim
COPY target/user-service.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
但很快发现:JAR 包动辄 100MB+,每次构建镜像都慢得像蜗牛。后来学乖了,用 多阶段构建:
# 多阶段构建优化
FROM maven:3.8-openjdk-17 AS builder
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests
FROM openjdk:17-slim
COPY --from=builder /target/user-service.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
镜像体积从 120MB 降到 60MB,CI/CD 流水线速度提升一倍。
但真正的转折点是引入 Kubernetes。以前运维要手动扩容,现在一个 kubectl scale deployment user-service --replicas=5,秒级生效。HPA(Horizontal Pod Autoscaler)更是神器——根据 CPU 或自定义指标自动扩缩容。双 11 那天,订单服务从 3 个 Pod 自动扩到 50 个,流量高峰一过又缩回去,省下的钱够我买半年咖啡。
不过 K8s 的学习曲线是真的陡。第一次写 Deployment YAML 时,我把 livenessProbe 的 initialDelaySeconds 设成 5 秒,结果 Pod 刚启动就被 kill,无限重启。查了一晚上日志,才发现应用初始化要 15 秒。从此以后,我写 probe 都留足 buffer。
第三步:云原生——不止是上云,更是思维方式的转变
很多人以为“上云”就是把 VM 换成 ECS,把应用打包成镜像。但真正的云原生,是拥抱不可变基础设施、声明式 API、弹性伸缩、可观测性。
我们做了几件事:
1. 配置外置化
以前配置写在 application.yml 里,改个数据库密码要重新打包。现在用 Nacos Config,动态刷新:
@RefreshScope
@RestController
public class ConfigController {
@Value("${db.timeout:30}")
private int dbTimeout;
}
2. 服务网格探索
虽然还没上 Istio,但我们用 OpenTelemetry 统一收集 metrics、logs、traces,推送到 Prometheus + Grafana + Loki。现在看板上一眼就能看出哪个服务延迟高、错误率高。
3. Serverless 尝鲜
一些非核心任务,比如图片缩略图生成、日志清洗,我们迁到了 阿里云函数计算(FC)。不用管服务器,按调用量付费,成本降了 70%。
4. GitOps 实践
所有 K8s 配置存 Git,通过 Argo CD 自动同步。再也不用手动 kubectl apply,避免“配置漂移”。上周有个同事误删了 Service,Argo CD 两分钟内自动恢复,救了大命。
技术栈变迁:从 Java 单打独斗到多元融合
早期全是 Java,但现在我们的技术栈丰富多了:
| 模块 | 技术栈 | 理由 |
|---|---|---|
| 核心业务 | Java 17 + Spring Boot 3 | 稳定、生态成熟 |
| 高并发实时处理 | Rust (Actix Web) | 性能高、内存安全 |
| 脚本工具 | Node.js + TypeScript | 快速开发、JSON 处理方便 |
| 数据分析 | Python + Pandas | 生态强大 |
没错,我最近就在用 Rust 重写一个高频交易接口。虽然 borrow checker 初期折磨得我想放弃,但一旦跑通,QPS 直接翻倍,内存占用只有 Java 版的 1/3。关键是——没有 GC 停顿!这在金融场景太重要了。
至于 JavaScript?别误会,我们后端不用它写主逻辑。但它在 自动化测试脚本、Mock Server(用 Express 快速搭)、运维工具(比如用 Puppeteer 截图监控)里出镜率极高。前端甩过来的 Swagger 文档经常缺字段,我就写个 JS 脚本自动校验,省下无数扯皮时间。
血泪教训:那些年我们踩过的坑
不要为了微服务而微服务
一开始我们把“用户积分”拆成独立服务,结果发现它和用户中心强耦合,调用延迟反而增加。最后又合并回去。微服务不是目的,解耦和自治才是。数据库拆分比代码拆分更痛
单体时代一张 user 表搞定,拆完后用户基本信息、积分、地址分散在三个库。跨库查询?不存在的。最后靠 CQRS + 事件驱动 解决——用户更新时发 Kafka 消息,其他服务消费后更新自己的视图表。监控不是可选项,是生命线
有次 Redis 缓存穿透,没告警,等用户投诉才发现。现在我们对核心接口都设了 SLO(Service Level Objective),延迟 > 500ms 或错误率 > 0.1% 就钉钉告警。文档和自动化同样重要
新人入职第一天,给他一份《本地开发环境搭建指南》,里面连~/.kube/config怎么配都写了。不然光解释“为什么你连不上测试集群”就得花半天。
写在最后:架构演进没有终点
从单体到云原生,我们花了 18 个月。中间熬过无数个通宵,和产品吵过无数次“这个需求能不能下个版本做”,也经历过线上 P0 事故被叫回公司。
但回头看,每一步都值得。现在的系统,扩容只需改个数字,故障能自动隔离,新功能上线从周级缩短到小时级。最重要的是——我不再害怕改代码了。
如果你也在经历类似转型,我的建议是:
- 别迷信新技术,先解决最痛的点
- 小步快跑,别指望一口吃成胖子
- 多写自动化脚本,少做重复劳动
- 保持学习,但别被 hype 带偏(是的,我说的就是某些天天吹 Web3 的)
对了,最近在刷《Cloud Native Patterns》这本书,比那些纯理论的好懂多了。如果你也在杭州,阿里网易这边机会不少,简历可以往“云原生+高并发”方向包装——毕竟,谁不想招一个能搞定双 11 流量的人呢?
好了,咖啡凉了,该去 review 下一个 PR 了。希望下次写博客,是在 Rust 服务稳定运行一年之后。
(完)

评论 0