后端架构演进:从单体到云原生 —— 一位老码农的实战经验分享
背景介绍

我在一家中型互联网公司做了近十年的后端开发,最早的时候我们用的是Spring Boot搭建的单体应用,部署在一台物理服务器上。那时候系统规模小,功能不多,代码量大概也就几万行,数据库也没怎么优化,接口响应时间都在毫秒级,一切都显得刚刚好。
但随着业务的发展,用户量、订单量、商品数量都在增长,系统的压力也在不断增加。最初我们尝试横向扩容,加了几台服务器做负载均衡,但发现效果并不理想,因为很多服务模块之间高度耦合,共享数据库,根本无法独立部署和伸缩。
后来,我被安排负责整个技术架构的改造工作,目标是将单体应用逐步拆分为微服务,并最终迁移到Kubernetes(简称K8s)平台实现云原生化。这个过程持续了将近两年,踩了很多坑,也收获了不少经验。今天我想以第一人称的角度,结合真实项目中的场景,和大家分享这次架构升级的全过程。
遇到的问题与挑战

1. 单体应用带来的瓶颈
我们的核心系统是一个电商后台服务,包括用户管理、商品管理、订单中心、支付模块、库存管理等多个子系统,都集成在一个Spring Boot工程中:
/src
┣ main/
┃ ┣ java/
┃ ┃ ┣ controller/
┃ ┃ ┣ service/
┃ ┃ ┣ entity/
┃ ┃ ┣ config/
┃ ┣ resources/
┃ ┃ ┣ application.yml
这种结构的好处是开发简单,本地运行快,部署容易,但它很快暴露出一系列问题:
- 发布风险大:一个小功能上线都要重新部署整个应用,出问题影响面广。
- 代码臃肿:各个模块之间没有边界,常常一个类引用另一个模块的服务,甚至直接访问其他表。
- 扩展性差:想对某个模块单独扩容基本不可能,数据库成了瓶颈。
- 维护成本高:团队人数增多后,不同人改同一段代码经常冲突。
2. 运维自动化程度低
之前我们使用Shell脚本 + Jenkins手动打包部署。每次上线都需要专人执行命令,一旦出错就得回滚,效率低且容错率差。
3. 没有统一的服务治理能力
服务之间的调用全靠硬编码配置或者内网IP直连,缺乏统一注册发现机制,也没有限流、熔断这些高级能力。
架构改造思路与实施路径

为了解决这些问题,我们决定走一条“先拆分、再微服务、最后上云”的渐进式路线:
第一阶段:单体应用拆分(Monolith to Microservices)
我们选择从业务逻辑相对清晰的几个模块开始拆分:订单中心、支付服务、用户服务、商品服务。
服务拆分策略
我们采取了以下策略进行拆分:
- 按领域划分:每个服务专注于自己的业务领域,比如订单只处理订单相关的创建、查询、状态变更等。
- 数据库分离:为每个服务分配独立数据库实例,避免数据层耦合。
- 对外提供REST API:服务间通过HTTP API通信(后续替换为gRPC),同时保留内部调用SDK(如Feign Client封装)。
拆分遇到的技术难题
- 服务间的依赖管理:早期服务之间存在大量互相调用,如果不合理设计可能会导致循环依赖。
- 数据一致性问题:订单服务需要同步用户信息,我们引入了基于Event Driven的设计,借助消息队列(我们用了Kafka)实现异步更新。
举个例子:用户修改手机号时会发布一个UserUpdatedEvent,订单服务消费该事件并更新对应的数据缓存。
@Component
public class UserEventListener {
private final OrderService orderService;
public UserEventListener(OrderService orderService) {
this.orderService = orderService;
}
@KafkaListener(topics = "user.updated")
public void handleUserUpdate(UserDTO userDTO) {
orderService.updateUserDetails(userDTO.getUserId(), userDTO.getPhone());
}
}
这段代码虽然看起来简单,但我们初期没考虑到的是消息重复消费的问题,后来加上了幂等控制字段(例如用户ID + eventID 的Redis布隆过滤器)才解决。
接口设计注意事项
我们在定义接口时遵循了以下几个原则:
- 所有接口必须使用统一格式的返回对象,比如
ResponseEntity<ApiResponse<T>> - 错误码标准化,前端可识别处理
- 分页、排序、过滤等通用参数抽象成PageRequest/SortRequest对象
这为后续服务治理打下了良好的基础。
第二阶段:引入服务治理体系
为了更好地管理服务间通信和监控,我们引入了以下组件:
| 工具 | 作用 |
|---|---|
| Spring Cloud Alibaba Nacos | 注册中心+配置中心 |
| Sentinel | 限流、熔断 |
| OpenFeign + LoadBalancer | 服务间通信 |
| Sleuth + Zipkin | 分布式链路追踪 |
其中,我们最受益的其实是 Sentinel + Nacos的组合。它可以动态配置规则,在控制台上实时调整限流策略。
示例:限流配置在Nacos中定义
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: 127.0.0.1:8848
data-id: order-service-sentinel-rules
group: DEFAULT_GROUP
rule-type: flow
然后,我们在Nacos上维护类似如下内容:
[
{
"resource": "/api/order/create",
"count": 100,
"grade": 1,
"limitApp": "default"
}
]
这就表示,针对创建订单接口设置每秒QPS不能超过100,超过后拒绝请求,保护下游系统。
第三阶段:向云原生迁移(Cloud Native with Kubernetes)
前面两个阶段完成后,我们已经完成了微服务的基础建设。这时候,我们决定进一步拥抱云原生理念,将所有服务容器化,并使用Kubernetes进行编排管理。
容器化改造
我们首先将所有服务打包为Docker镜像:
FROM openjdk:8-jdk-alpine
ADD *.jar app.jar
ENTRYPOINT ["java", "-jar", "./app.jar"]
然后编写Deployment和Service YAML文件进行部署:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.example.com/order-service:latest
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: common-config
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- protocol: TCP
port: 8080
targetPort: 8080
Kubernetes给我们带来的收益
- 弹性伸缩:根据CPU使用情况自动扩缩Pod
- 健康检查:Liveness / Readiness Probe保障服务稳定性
- 滚动更新:支持灰度发布、失败自动回滚
我们还接入了Prometheus + Grafana做监控告警体系,大大提升了可观测性。
开发与运维过程中踩过的坑

坑1:服务注册慢或丢失
一开始我们用Zookeeper做服务注册,后来换成了Nacos。结果在压测环境下发现某些服务节点频繁注册失败,排查后发现是因为心跳检测超时设置过短,导致部分网络波动大的环境出现反复下线。
最终我们增加了重试次数、拉长了心跳间隔,并启用了服务健康检查,才稳定下来。
坑2:日志分散难查找
微服务多了以后,日志变得特别分散。最初我们只是把日志输出到各自Pod的目录里,查问题非常痛苦。后来我们引入ELK(Elasticsearch + Logstash + Kibana)统一收集日志,并设置了索引模板,按服务名、日期分类存储。
坑3:K8s内存溢出却无报错
有一次上线后突然有服务频繁OOM,但K8s并没有报错。排查了很久才发现是Java虚拟机默认堆大小没限制,导致JVM申请超过Pod配额,被K8s Kill掉但日志中没记录。
解决方案是在启动命令中显式指定:
CMD ["java", "-Xms256m", "-Xmx512m", "-jar", "./app.jar"]
并且在K8s资源限制中增加内存约束:
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1"
坑4:服务初始化慢导致探针失败
有些服务启动要加载几十张表元数据,耗时比较长,LivenessProbe 设置太短的话会导致Pod一直 restarting。
解决方案是给健康检查加上合理的初始化延迟:
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 15
改造后的效果与收益
经过近两年的努力,我们完成了一次成功的架构升级。系统在多个维度都有明显提升:
| 维度 | 原始状态 | 改造后状态 |
|---|---|---|
| 系统可用性 | 月故障多次 | 连续三个月零事故 |
| 发布效率 | 全量部署,需停机 | 滚动发布,无需停机 |
| 故障隔离能力 | 出问题影响全局 | 故障限于局部服务 |
| 弹性扩容能力 | 固定服务器数量 | 根据流量自动扩缩 |
| 日志与监控可视性 | 分散、难以定位 | 集中式、可视化、告警联动 |
| 技术债务清理 | 模块耦合严重 | 清晰边界、服务自治 |
团队协作也更加顺畅。大家各司其职,专注自己负责的服务模块,再也不用担心别人改代码会影响你的服务。
我的一些经验建议
如果你正在考虑或正在进行类似的架构升级,我有几点建议送给你们:
✅ 不要盲目追求“微服务”
微服务不是银弹。如果你的系统还没到一定复杂度,拆得太早反而带来管理和运维负担。先做好模块化设计,等真正需要时再拆也不迟。
✅ 数据库一定要尽早独立
很多人在拆服务的时候忽略了数据库拆分,这是大忌。数据层如果不解耦,服务之间依然会互相牵制,无法做到真正的“微”。
✅ 服务治理要跟上节奏
拆服务后如果没有配套的注册中心、限流、链路追踪等能力,你会发现服务更容易挂、更难排查问题。
✅ 自动化运维必不可少
CI/CD流水线、容器编排、监控告警这些工具链,都是提升研发效能的核心要素。不要想着“手动操作也可以”,越早自动化越好。
✅ 保留过渡方案,别一步到位
我们当初是边写新服务边兼容旧代码,中间用了不少Adapter模式做适配,直到完全替代为止。这样可以降低切换风险,也能随时回滚。
结语:写给后端开发者的一句话
技术演进从来都不是一蹴而就的事。我在这一路上经历了无数版本迭代、无数次深夜调试、也有过迷茫和怀疑。但我始终相信:一个好的架构,应该服务于业务,而不是反过来;它应该是让工程师更高效、让团队协作更顺畅的工具。
希望我的这段经历能帮你在面对技术选型、架构决策时多一份底气,少一点焦虑。如果你也有类似的实践故事,欢迎留言交流~我们一起成长!
—— 来自一线的老码农 🧱

评论 0