application.yml 示例
架构不是设计出来的,是“长”出来的

五年前,我刚入行做后端开发的时候,公司还是一套部署在本地服务器的单体应用。那时我们每天的工作就是在同一个 Git 分支上改代码、打包发布、偶尔出点问题就重启一下 Tomcat。整个项目结构清晰,功能模块都在一个工程里,看起来很稳。
但随着业务快速扩展,用户量激增,这个曾经“能用”的架构开始频频暴露出各种问题:发版慢、故障频发、数据库压力大、系统响应延迟严重……最头疼的一次,因为一次数据库表字段变更导致服务挂掉,整整花了半天才恢复。那时候我才真正意识到——技术架构不是一开始就设计完美的,而是随着业务的成长一步步演进出来的。
今天我想和你聊聊这些年我们团队经历的架构演进过程:从单体到微服务,再到如今拥抱云原生。这期间遇到的问题、踩过的坑,以及一些实际的技术方案和思考,都拿出来分享一下。
一、问题:当单体遇上增长,崩溃只是时间问题
我参与过一个电商平台的重构项目。最开始,这个平台只有一个部署包,包括商品管理、订单系统、用户中心等几十个模块,全部跑在一个 Java 应用里,依赖同一个 MySQL 数据库实例。
几个显著的问题很快暴露出来:
- 发版风险大:改一个小 bug 都得整体打包发布,一旦新版本有兼容性问题就得回滚,影响范围非常广。
- 性能瓶颈明显:特别是高峰期,数据库连接数经常爆满,订单模块的慢 SQL 拖垮整个系统。
- 横向扩容困难:虽然我们可以把服务部署多个实例,但由于状态耦合严重(比如本地缓存、共享 session),并没有起到很好的效果。
- 研发效率低:多个人同时在一个项目上开发容易冲突,新人进来也很难快速定位模块位置。
这些问题在业务量不大的时候还可以靠加班维持,但一旦日均 UV 超过百万,这套架构就成了系统的“定时炸弹”。
二、第一步尝试:拆分 —— 从单体走向粗粒度解耦
既然整体太庞大了,那我们就试着把它“掰开”。一开始我们选择的是按业务领域划分的方式,将原来的单体应用拆分成:
- 用户中心
- 商品中心
- 订单中心
- 支付中心
- 内容中心
每个服务独立部署,各自有自己的数据库,服务间通过 HTTP 接口通信。当然,为了控制成本,一开始我们还没引入注册中心,服务地址是写死在配置文件里的。
关键思路:
- 数据库分库:原来的大库拆成多个业务数据库,缓解单点压力;
- 接口抽象化:定义统一的 RESTful API 协议;
- 异步解耦:部分场景下使用 RabbitMQ 做消息队列处理通知类操作;
- 缓存下沉:将公共读操作下沉到 Redis 缓存中,提升响应速度;
举个例子,订单生成时需要调用商品库存扣减接口。这时候我们会先通过 HTTP 请求远程调用商品中心的服务,如果失败会记录日志并重试。
// 示例:服务间调用伪代码
OrderService {
createOrder(userId, productId) {
// 远程调用商品中心接口
boolean success = productService.decreaseStock(productId);
if (!success) {
log.warn("库存不足或服务调用失败");
return "库存扣减失败";
}
// 创建订单
orderRepo.create(order);
}
}
这种简单拆分带来了一些好处:比如某个服务出现异常不会影响其他服务,发版也可以单独进行。但也暴露了不少问题:
- 服务发现和治理缺失,调用链不可控;
- 数据一致性难以保障,例如订单扣库存失败后需补偿机制;
- 日志分散,问题排查变得困难;
- 性能没有根本提升,反而新增了网络开销。
三、第二阶段:微服务基础设施搭建
为了解决上述问题,我们决定引入更完整的微服务架构,搭建了一系列基础组件:
1. 注册中心 + 客户端负载均衡
我们选择了 Spring Cloud 生态下的 Eureka 作为注册中心,并基于 Ribbon 实现客户端负载均衡。这样,订单服务可以自动感知商品服务的不同实例,避免了 IP 硬编码。
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: order-service
2. 配置中心 & 监控告警
我们引入了 Spring Cloud Config 来统一管理各个服务的配置,并结合 Prometheus 和 Grafana 做指标采集与监控报警。
3. API 网关统一入口
Zuul 被我们用作网关,集中处理权限验证、路由转发、限流熔断等功能:
// ZuulFilter 示例,用于鉴权
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() { return "pre"; }
@Override
public int filterOrder() { return 1; }
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
if (validateToken(token)) {
ctx.setSendZuulResponse(true);
} else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("Unauthorized");
}
return null;
}
}
这些改进让我们的架构具备了基本的微服务能力。但真正的挑战还在后面。
四、第三阶段:向云原生转型
2021年开始,我们开始尝试从传统架构向云原生体系迁移。这次的动机很简单:想用更少的人力维护更多的服务,同时支持弹性扩缩容。
技术选型上,我们做了如下升级:
| 旧 | 新 |
|---|---|
| 物理机部署 | Kubernetes + Docker |
| Zookeeper/Eureka | Consul + Envoy 或 Istio |
| 自建消息中间件 | Kafka / RabbitMQ on K8s |
| Prometheus + AlertManager | Prometheus Stack + Loki + Tempo |
核心变化:
- 容器化部署:所有服务打成 Docker 镜像,并部署在 Kubernetes 集群中;
- 声明式配置管理:借助 Helm Charts 管理服务部署模板;
- 服务网格加持:Istio 提供更强大的流量治理能力;
- 统一日志与追踪:ELK Stack + OpenTelemetry 实现全链路追踪。
其中一个典型优化是在高并发下单场景引入了分布式锁+幂等设计。以创建订单为例:
@PostMapping("/order/create")
public ResponseEntity<?> createOrder(@RequestParam String userId,
@RequestParam String productId,
@RequestParam String requestId) {
String lockKey = "lock:order:" + userId;
try {
// 获取分布式锁(Redis)
boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 5, TimeUnit.SECONDS);
if (!acquired) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请勿重复提交");
}
// 幂等校验
String existingOrderId = redisTemplate.opsForValue().get("request:" + requestId);
if (existingOrderId != null) {
return ResponseEntity.ok(existingOrderId);
}
// 扣库存、生成订单逻辑...
String orderId = generateOrderId();
redisTemplate.opsForValue().set("request:" + requestId, orderId, 24, TimeUnit.HOURS);
return ResponseEntity.ok(orderId);
} finally {
redisTemplate.delete(lockKey);
}
}
这一段代码背后其实隐藏了很多考量:
- 高并发场景下必须加锁,否则会出现超卖;
- 使用 Redis 锁要小心设置过期时间和释放逻辑;
- 引入 requestId 可防止前端重复请求造成的多次创建;
- 最终数据一致性的保障往往需要配合补偿任务或事件驱动架构。
五、踩过的坑 & 实战经验
在这几年的架构演进过程中,有几个坑让我记忆犹新:
1. 跨服务事务没做好,最终对账系统背锅
有一次我们在重构支付流程时,订单状态更新和支付流水记录这两个操作分别属于不同服务,原本以为可以依靠 MQ 的顺序性和消费重试来保证数据一致性。但实际上由于网络波动和消费幂等问题未处理好,导致部分订单状态已变更为“已支付”,但支付流水却迟迟没到账。
教训:跨服务事务不能依赖外部消息队列,必须设计本地事务表 + 状态核对任务,必要时手动干预。
2. 微服务间的循环依赖
刚开始拆服务时,商品中心需要查用户信息,用户中心反过来也要查用户收藏的商品信息。于是两个服务互相调用对方接口,结果上线后经常因为某一方服务异常导致雪崩效应。
解决方式:重新梳理数据归属边界,引入 CQRS 模式,把某些只读数据冗余到本地缓存表中。
3. Kubernetes 上线初期资源配额不合理
有一次灰度上线新服务时,Pod 启动特别慢,甚至出现 CrashLoopBackoff。后来才发现是因为内存限制设得太小,JVM 启动失败。另外还有一个服务因为 CPU 限制太低,在高峰期被系统频繁驱逐。
建议:生产环境一定要设置合理的 limits/request 值,并结合 HPA 做自动扩缩容。
六、效果对比与收益总结
从最初那个“能跑就行”的单体应用到现在基于 Kubernetes 的多集群部署架构,我们取得了以下成效:
| 指标 | 初期 | 当前 |
|---|---|---|
| 发版频率 | 每周 1~2 次 | 每天多次 |
| 故障隔离能力 | 差 | 强 |
| 系统可扩展性 | 弱 | 强 |
| 排查效率 | 低 | 高 |
| 运维复杂度 | 简单 | 中高 |
| 成本投入 | 低 | 高但可控 |
更重要的是,现在我们可以通过 DevOps 工具链实现持续集成/持续交付(CI/CD),新功能的上线周期大幅缩短,故障也能快速定位和修复。
七、几点建议送给正在走这条路的你
不要一开始就追求“完美架构”
架构是随着业务发展慢慢迭代出来的。一开始不必纠结于微服务还是事件驱动,先解决最痛的问题。重视数据模型设计
拆服务之前就要明确数据边界,避免后期反向调用泛滥,引发更多复杂度。监控和日志是运维的生命线
不管是单体还是微服务,如果没有完善的可观测体系,出了问题就像大海捞针。别迷信任何框架或工具
我们曾过度信任 Spring Cloud Feign 的重试机制,结果在一次服务雪崩中加重了问题。技术方案一定要结合自身业务场景评估。学会用最小代价验证想法
很多功能其实不需要上线就能验证可行性。我们做过很多 A/B 测试和压测实验,帮助决策是否值得投入改造。
架构不是纸上画出来的蓝图,而是在代码和日志中慢慢长出来的生命力。回顾这几年的经历,我最大的感悟就是:“好的架构不是设计出来的,是踩着坑走过来的。”希望这篇文章能让你少走些弯路,走得更快、更稳。
如果你也在经历架构演进的过程,欢迎留言交流,我们一起成长 💪。

评论 0