单体 vs 微服务权衡
2016 年,某创业公司在拿到 A 轮融资后决定「all in 微服务」。他们把一个 3 人团队维护的电商系统拆成了 20 个微服务,每个服务 2-3 个人负责。一年后,他们花了大价钱招聘 DevOps 工程师,购买了 K8s 集群,却因为服务间调用链路复杂、分布式事务问题频出,迭代速度反而比拆分前还慢。
2019 年,Instagram 在增长到亿级用户时仍然跑在单体架构上。他们没有把 Instagram 拆成几十个微服务,而是通过单体 + 良好的模块化来支撑业务高速增长。
这两个案例告诉我们:微服务不是银弹,单体也不是原罪。关键是理解两者的适用场景。
单体架构:简单即力量
什么是单体架构
单体架构(Monolithic Architecture)将所有功能模块打包在一个部署单元中。
flowchart TD
subgraph 单体应用
API["API 层"]
Service["业务逻辑层"]
DAO["数据访问层"]
DB[("数据库")]
API --> Service
Service --> DAO
DAO --> DB
end
subgraph 业务模块(都在一个进程内)
User["用户模块"]
Order["订单模块"]
Product["商品模块"]
Payment["支付模块"]
end
Service --> User
Service --> Order
Service --> Product
Service --> Payment
单体架构的优势
1. 开发效率高
一个代码仓库,一套 CI/CD 流水线,一次部署所有功能。新入职的工程师两周内可以熟悉完整代码。
// 单体项目:直接调用,不涉及网络
@Service
public class OrderService {
@Autowired
private UserService userService;
@Autowired
private ProductService productService;
@Autowired
private PaymentService paymentService;
public void createOrder(OrderDTO dto) {
// 直接调用,毫秒级响应
User user = userService.getUserById(dto.getUserId());
Product product = productService.getProductById(dto.getProductId());
// 事务在同一个数据库连接中
Order order = new Order(user, product, dto.getQuantity());
orderRepository.save(order);
// 调用支付服务,直接方法调用
paymentService.processPayment(order);
}
}
2. 测试简单
端到端测试不需要启动多个服务;本地开发不需要启动 Docker Compose;测试覆盖率统计一套工具就够了。
3. 部署简单
一个 JAR 包,一个进程,一行命令启动。没有服务发现、没有网关路由、没有分布式配置中心。
# 单体部署:简单到极致
java -jar app.jar --spring.profiles.active=prod
# 微服务部署:需要考虑很多东西
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml
4. 事务简单
本地事务直接回滚,不需要考虑分布式事务。代码里的 @Transactional 注解就能解决 99% 的并发问题。
5. 性能更好
没有网络开销,方法调用比 RPC 快 1000 倍。对于中小规模系统,单体可以少买 50% 的服务器。
单体架构的局限
1. 技术栈锁定
选定了 Spring Boot,就很难在部分模块使用其他技术栈。想在某个模块用 Python 深度学习?抱歉,进了单体就出不来了。
2. 团队耦合
50 个工程师在一个代码仓库里,改代码时「牵一发动全身」。一个团队部署,影响其他团队的代码。
3. 扩展性受限
某个模块 CPU 密集(推荐算法),某个模块 IO 密集(文件处理),但只能统一扩展整应用。推荐算法拖垮了文件处理模块。
4. 部署僵化
修改一行代码,需要重新部署整个系统。「用户模块加个字段,订单模块被迫停机」。
微服务架构:分而治之
什么是微服务架构
微服务架构(Microservices Architecture)将系统拆分为多个独立部署、独立运行的服务,每个服务负责特定的业务能力。
flowchart TD
subgraph API 网关
GW["API Gateway"]
end
subgraph 微服务集群
US["用户服务\n:8081"]
OS["订单服务\n:8082"]
PS["商品服务\n:8083"]
PYS["支付服务\n:8084"]
end
subgraph 数据存储
UDB[("用户库")]
ODB[("订单库")]
PDB[("商品库")]
PYDB[("支付库")]
end
GW --> US
GW --> OS
GW --> PS
GW --> PYS
OS --> US
OS --> PS
OS --> PYS
US --> UDB
OS --> ODB
PS --> PDB
PYS --> PYDB
style US fill:#e3f2fd
style OS fill:#e8f5e9
style PS fill:#fff3e0
style PYS fill:#fce4ec
微服务架构的优势
1. 独立部署
每个服务可以独立部署、回滚、扩缩容。支付服务出了 bug,不会影响用户浏览商品。
2. 技术多样性
可以用 Java 写订单服务,用 Go 写文件处理,用 Python 写推荐算法,按需选择最合适的技术栈。
3. 团队自治
每个团队负责自己的服务,端到端负责(开发、测试、运维)。团队之间通过 API 约定接口,不再需要协调部署。
4. 弹性扩展
CPU 密集的服务多部署几台,IO 密集的服务部署在更高配的机器上。各服务按需扩展,资源利用率更高。
微服务架构的代价
1. 分布式复杂性
// 微服务调用:到处都是坑
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public void createOrder(OrderDTO dto) {
// 问题1:服务发现,这个地址从哪来?
String userServiceUrl = "http://user-service/api/users/" + dto.getUserId();
// 问题2:网络超时,怎么处理?
// 问题3:服务挂了,怎么降级?
// 问题4:返回的数据格式对不对?
UserDTO user = restTemplate.getForObject(userServiceUrl, UserDTO.class);
// 问题5:本地事务不管用了,分布式事务怎么搞?
orderRepository.save(order);
// ... 调用支付服务
}
}
分布式系统需要解决的问题清单:
- 服务发现(Consul / Eureka / Nacos)
- 负载均衡(Ribbon / Spring Cloud LoadBalancer)
- 熔断降级(Hystrix / Sentinel / Resilience4j)
- 分布式事务(Seata / Saga)
- 链路追踪(Zipkin / Jaeger)
- 统一配置(Nacos / Apollo)
- 日志聚合(ELK)
- 监控告警(Prometheus + Grafana)
2. 运维复杂度
Kubernetes、Docker、Helm、Istio——微服务的运维栈比单体复杂 10 倍不止。
3. 调试困难
一个用户请求经过 5 个服务,哪个环节出了问题?需要链路追踪系统、日志聚合系统来排查。
4. 数据一致性
跨服务的分布式事务,是微服务架构最大的痛点之一。
何时该拆分微服务?
微服务不是想拆就拆,而是需要满足一定的前提条件:
flowchart TD
A["微服务拆分判断"] --> B{团队规模 `>=` 20 人?}
B -->|否| C["单体更合适\n(小团队驾驭不了微服务复杂度)"]
B -->|是| D{业务复杂度高?}
D -->|否| C
D -->|是| E{需要独立扩展?}
E -->|否| C
E -->|是| F{需要技术多样性?}
F -->|否| G["考虑模块化单体"]
F -->|是| H["微服务是合理选择"]
style C fill:#fff3e0
style G fill:#e3f2fd
style H fill:#e8f5e9
适合微服务的信号
不适合微服务的信号
从单体到微服务:演进路径
路径一:绞杀者模式
在保留单体系统的基础上,逐步将功能迁移到微服务。新功能用微服务,老功能继续跑在单体里。
flowchart LR
subgraph 演进阶段1
M1["单体应用"] -->|"逐步拆分"| M2["部分微服务 + 单体残余"]
M2 -->|"继续拆分"| M3["大部分微服务"]
M3 -->|"最终"| MS["纯微服务架构"]
end
style M1 fill:#bbdefb
style M2 fill:#90caf9
style M3 fill:#64b5f6
style MS fill:#42a5f5
路径二:领域驱动设计(DDD)
用 DDD 的思想划分限界上下文,每个上下文对应一个微服务。这种拆分方式更符合业务边界,比按技术层拆分更合理。
flowchart TD
subgraph 限界上下文
UM["用户上下文\n- 注册/登录\n- 个人信息\n- 权限管理"]
OM["订单上下文\n- 创建订单\n- 订单查询\n- 订单取消"]
IM["库存上下文\n- 库存查询\n- 库存扣减\n- 库存归还"]
PM["支付上下文\n- 支付发起\n- 支付回调\n- 退款处理"]
end
subgraph 领域事件
E1["UserRegistered"]
E2["OrderCreated"]
E3["InventoryDeducted"]
end
E1 --> OM
E2 --> IM
E3 --> PM
路径三:模块化单体
在拆分微服务之前,先把单体做成「模块化的单体」。每个模块有清晰的接口,模块之间通过接口通信,但不涉及网络调用。等业务稳定、团队成熟后再考虑拆分。
// 模块化单体:清晰的模块边界
package com.company.user; // 用户模块
public interface UserService {
User getUserById(Long id);
void updateUser(User user);
}
package com.company.order; // 订单模块
public interface OrderService {
Order createOrder(Long userId, List<Long> productIds);
}
// 订单模块依赖用户模块,但不直接调用数据库
public class OrderServiceImpl implements OrderService {
private final UserService userService; // 通过接口注入
@Override
public Order createOrder(Long userId, List<Long> productIds) {
// 调用用户服务,获取用户信息
User user = userService.getUserById(userId); // 本地方法调用,非网络调用
// ...
}
}
权衡矩阵
常见误区
「别人都在用微服务,我们也要用」
微服务不是追潮流的工具。小团队(< 10 人)强行上微服务,运维成本会拖垮整个研发效率。
「微服务就是 RPC 调用」
把单体拆成多个 JAR 包,用 RPC 调用,这不叫微服务,叫分布式单体。真正的微服务需要独立部署、独立数据、独立团队。
「拆分粒度越细越好」
服务粒度太细会导致调用链路过长、事务难以处理、运维成本暴增。Amazon 的「两个 pizza 原则」:一个团队吃两个 pizza 能吃饱,说明这个团队规模刚好,服务规模也差不多。
「微服务不需要考虑耦合」
微服务之间仍然需要考虑耦合问题。核心原则是:同步调用要尽量少,异步消息通信优先。
思考题
问题 1:一个 5 人团队的创业公司,要做电商平台,应该选单体还是微服务?为什么?
参考答案
建议选单体,原因:
- 团队规模太小:5 人驾驭微服务的分布式复杂度,运维成本会拖垮开发效率
- 业务边界未清晰:创业初期业务模式还在探索,拆了可能白拆
- 快速迭代优先:创业公司要快速验证业务,单体的开发速度优势明显
推荐方案:
- 模块化单体:按业务模块划分代码结构,但不拆分服务
- 加 Redis 缓存 + MySQL 主从,解决性能问题
- 等团队扩大到 15 人以上、业务稳定后,再考虑拆分微服务
核心原则:小团队做大事,先活下来再考虑架构优雅。
问题 2:微服务架构中,用户下单需要调用用户服务、商品服务、库存服务、积分服务。如果某个服务挂了,应该如何处理?
参考答案
多级降级策略:
1. 熔断器(Circuit Breaker)
@FeignClient(name = "inventory-service", fallback = InventoryServiceFallback.class)
public interface InventoryService {
void deduct(Long productId, Integer quantity);
}
// 熔断降级实现
@Component
public class InventoryServiceFallback implements InventoryService {
@Override
public void deduct(Long productId, Integer quantity) {
// 库存服务挂了,降级:标记订单为「待确认库存」
log.warn("库存服务不可用,订单库存待确认");
order.setStatus(OrderStatus.PENDING_INVENTORY);
}
}
2. 重试 + 超时
- 设置合理的超时时间(如 500ms)
- 网络抖动时自动重试
- 超时后降级处理
3. 幂等设计
- 使用幂等键(订单 ID)防止重复扣减
- 下游服务重启时,消费消息不会导致数据问题
4. 最终一致性
- 库存服务恢复后,通过消息队列补偿未处理的扣减
- 或使用 Saga 模式进行补偿事务
问题 3:如果让你把一个 50 万行代码的单体系统拆分为微服务,你会如何规划这个过程?需要关注哪些风险?
参考答案
拆分步骤:
- 领域建模:用 DDD 思想划分限界上下文
- 识别粘性代码:先拆耦合度低的模块
- API 设计:定义服务间接口,保证向后兼容
- 数据迁移:每个服务最终要有自己的数据库
- 逐步切换:用 Strangler Fig 模式,新旧系统并存,逐步切流量
关键风险:
核心建议:先拆分数据,再拆分代码。没有独立数据库的微服务,等于没有拆分。