微服务架构迁移与云原生服务编排 - 第1节
一、单体架构的黄金时代:什么是单体调度
说起单体架构,我脑子里第一个画面是十年前刚入行时做的一个电商项目。那时候整个团队就十几个人,一个应用包搞定所有功能——用户、商品、订单、支付,全在一个进程里跑。部署?一个 Tomcat 搞定。上线?半夜发个包就行。
说白了,单体调度就是所有业务逻辑都在同一个应用进程里,由应用自身来管理请求的流转和资源的分配。你想想看,一个请求进来,Controller 调 Service,Service 调 DAO,DAO 查数据库,整个过程都在一个 JVM 堆栈里完成。没有网络开销,没有序列化成本,也没有服务间调用的超时重试。
核心定义:单体调度是指应用内部通过函数调用或方法调用的方式,在单一进程内完成请求的处理、资源的分配和任务的编排。调度逻辑与业务代码紧密耦合,不存在跨进程的通信开销。
我记得当时我们团队有个老哥,特别喜欢在 Service 层里直接 new 线程池做异步任务。嗯,这在单体时代其实挺常见的——反正都在一个进程里,线程池共享内存,数据一致性靠数据库事务保证,简单粗暴但有效。
单体调度的典型特征
- 进程内调用:所有模块通过方法调用通信,没有网络 IO
- 共享内存:全局变量、缓存、线程池都在同一个 JVM 里
- 统一事务:一个 @Transactional 注解就能搞定跨表事务
- 集中式配置:数据库连接、缓存配置、MQ 配置全在一个 application.yml 里
我在项目中遇到过最夸张的一个单体应用,war 包解压后有 800 多兆。启动要 5 分钟,但跑起来确实稳。为什么?因为没有网络抖动,没有服务降级,没有熔断限流这些破事。
单体应用的优势与痛点
优势:为什么它能统治十年
别一提到单体就摇头。我做了这么多年架构,见过太多一上来就拆微服务,结果把自己拆死的团队。单体架构能活这么久,是有道理的。
| 优势 | 说明 | 我的体会 |
|---|---|---|
| 开发效率高 | IDE 里一个项目搞定所有代码,调试方便 | 我最快一次从零搭单体电商只用了一周 |
| 部署简单 | 一个 war 包扔到 Tomcat 就完事 | 运维同学最喜欢单体,不用配网关和注册中心 |
| 性能损耗小 | 进程内调用,没有网络开销 | 单机 QPS 轻松上千,延迟在毫秒级 |
| 事务一致性强 | 本地事务 + 数据库锁,ACID 天然支持 | 支付和库存扣减在一个事务里,不用操心分布式事务 |
| 调试方便 | 一个断点就能跟踪完整请求链路 | 出了问题,本地复现一下就能定位 |
我的建议:如果你的团队在 10 人以内,业务复杂度不高,别盲目上微服务。单体架构的简单性本身就是一种生产力。我见过太多 5 个人的团队,硬要拆 8 个微服务,结果每天光联调就花半天。
痛点:为什么它撑不住了
但是,当业务规模上来之后,单体的毛病就藏不住了。我亲身经历过一个电商系统,日活从 10 万涨到 100 万,单体架构开始各种「闹脾气」。
- 编译部署慢:800 兆的 war 包,编译 10 分钟,部署 5 分钟。改一行代码,整个应用都要重新发布。
- 团队协作困难:20 个人同时改一个代码库,合并冲突能让你崩溃。我记得有一次,两个同事同时改了订单模块的同一个文件,合并花了整整一下午。
- 扩展粒度太粗:订单模块扛不住了,你想加机器?对不起,整个应用都得一起扩。用户模块明明没压力,也得跟着吃资源。
- 技术栈锁定:整个项目用 Java,你就不能在某些模块用 Go 或 Python。想换数据库?所有模块都得跟着换。
- 故障隔离差:一个模块的内存泄漏,能把整个应用拖垮。我遇到过用户模块的一个死循环,直接把订单和支付模块一起搞挂了。
避坑指南:我曾经接手过一个单体电商项目,代码里到处都是 ThreadLocal 传参。一开始觉得方便,后来发现请求上下文经常串数据。排查了三天,最后发现是线程池复用导致 ThreadLocal 没清理。嗯,从那以后我对共享内存就特别警惕。
一个典型的单体电商系统拆解
光说不练假把式。咱们来看一个真实的单体电商系统长什么样。这是我几年前参与过的一个项目,虽然现在回头看有很多槽点,但当时确实撑起了千万级的 GMV。
系统架构总览
先上一张架构图,让大家对整体有个感觉。
模块职责与调度关系
这个系统里,每个模块的职责其实挺清晰的。我挑几个重点模块说说。
| 模块 | 核心职责 | 调度方式 | 典型问题 |
|---|---|---|---|
| 用户模块 | 注册、登录、权限校验、地址管理 | 直接调用 UserService 方法 | Session 共享问题 |
| 商品模块 | SPU/SKU 管理、库存维护、分类搜索 | 调用 ProductService + 本地缓存 | 库存超卖 |
| 订单模块 | 下单、状态流转、超时取消 | 调用 OrderService + 定时任务 | 状态不一致 |
| 支付模块 | 支付回调、退款、对账 | 异步回调 + 本地事务 | 掉单问题 |
| 营销模块 | 优惠券发放、满减活动、积分 | 硬编码在订单流程中 | 活动规则变更频繁 |
一次完整的下单流程
咱们来走一遍用户下单的完整流程,看看单体调度是怎么工作的。
// 伪代码:单体电商下单流程
@Transactional
public Order createOrder(Long userId, Long skuId, Integer quantity) {
// 1. 校验用户
User user = userService.getById(userId);
if (user == null || user.getStatus() == 0) {
throw new BusinessException("用户不存在或已禁用");
}
// 2. 查询商品
Sku sku = productService.getSkuById(skuId);
if (sku == null || sku.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 3. 计算价格(含营销优惠)
BigDecimal price = marketingService.calculatePrice(sku, quantity, userId);
// 4. 扣减库存
productService.deductStock(skuId, quantity);
// 5. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setSkuId(skuId);
order.setQuantity(quantity);
order.setAmount(price);
order.setStatus(OrderStatus.PENDING_PAYMENT);
orderDao.insert(order);
// 6. 发送消息(异步)
mqService.send("order.created", order.getId());
return order;
}
你看,整个过程都在一个事务里。从用户校验到库存扣减,再到订单创建,全部是本地方法调用。没有网络超时,没有分布式事务,没有服务降级。这就是单体调度的魅力——简单、可靠、高效。
一个小细节:注意第 6 步的 MQ 发送。我习惯把 MQ 放在事务的最后,而且用的是同一个数据库事务。如果 MQ 发送失败,整个事务回滚,订单和库存都不会被提交。这在单体里很容易做到,但到了微服务时代,这种「本地事务 + 消息」的模式就变成了分布式事务的难题。
单体调度的隐形成本
不过,这种「一个事务搞定一切」的爽感,随着业务增长会慢慢变成负担。我总结了几点单体调度的隐形成本:
- 长事务风险:一个下单流程涉及 5-6 个模块,事务时间可能超过 1 秒。高并发下,数据库连接池很容易被打满。
- 代码耦合严重:营销模块的代码直接写在订单 Service 里。改个优惠规则,订单模块也得重新部署。
- 资源争抢:用户模块的慢查询会拖慢整个应用的数据库连接池。我记得有一次,用户模块的一个全表扫描,直接把订单模块的查询也拖垮了。
- 扩展瓶颈:订单模块需要更多计算资源?对不起,整个应用都得一起扩。用户模块和商品模块也跟着吃资源,浪费严重。
我曾经踩过的坑:有一次线上大促,订单量暴涨。单体应用的数据库连接池被打满,所有请求都在等数据库连接。更惨的是,用户模块的登录请求也在等同一个连接池,导致新用户根本进不来。嗯,那次事故之后,我们才开始认真考虑拆分的事情。