微服务时代
2015 年,Martin Fowler 发表了一篇文章《Microservices》,系统性地总结了这一架构风格的核心特征。这篇文章迅速在技术圈传播,成为后续几年无数公司「拆微服务」的理论依据。
但讽刺的是,很多人读完文章后,只记住了「微服务」三个字,忘记了文章副标题——「企业级应用开发者应该在认真考虑利弊后,再决定是否采用」。
这篇文章的正文只有 3000 单词,但「背景和动机」就占了 1000 单词。Martin Fowler 用大量篇幅描述了微服务诞生的背景、对 SOA 失败的反思、以及它适用的场景。但大多数人只看了摘要和核心特征。
微服务不是银弹。它是 SOA 失败的教训 + 云计算的普及 + 敏捷开发的兴起,共同推动的产物。
微服务诞生的背景
SOA 的失败教训
上一节我们讲了 SOA 的失败案例。SOA 失败的核心原因:
- ESB 中心化:所有流量都经过 ESB,成为新的瓶颈
- 服务粒度太粗:拆成了几个大服务,和单体没本质区别
- 过度设计:为了抽象而抽象,导致系统复杂度爆炸
微服务在设计上刻意避开了这些坑:去中心化、细粒度服务、轻量级通信。
云计算的普及
2010 年代,AWS、Azure、阿里云等云平台成熟。容器技术(Docker)和容器编排(Kubernetes)的出现,让「独立部署」变得简单。
云之前的部署:
1. 申请物理服务器(2 周)
2. 安装操作系统(1 天)
3. 配置中间件(1 天)
4. 部署应用(1 天)
总耗时:约 2 周
云 + 容器之后的部署:
1. 编写 Dockerfile(1 小时)
2. push 镜像(10 分钟)
3. kubectl apply(1 分钟)
总耗时:约 1 小时
部署成本从「天」变成「分钟」,让独立部署每个服务变得可行。
敏捷开发的兴起
DevOps 运动让「独立团队负责独立服务」成为可能。每个团队可以自主决定技术栈、发布节奏,不需要跨团队协调。
微服务的核心特征
独立部署
每个微服务是独立的部署单元,可以单独构建、单独测试、单独发布。一个服务的变更不需要影响其他服务。
单体部署:
┌──────────────────────────────────────────────┐
│ 单体应用 │
│ [用户模块] [商品模块] [订单模块] [支付模块] │
│ │
│ 一次构建 → 一次测试 → 一次发布 │
│ 任何一个模块改动了,都要发布整个应用 │
└──────────────────────────────────────────────┘
微服务部署:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户服务 │ │ 商品服务 │ │ 订单服务 │ │ 支付服务 │
│ 独立构建 │ 独立构建 │ 独立构建 │ 独立构建 │
│ 独立测试 │ 独立测试 │ 独立测试 │ 独立测试 │
│ 独立发布 │ 独立发布 │ 独立发布 │ 独立发布 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
独立数据库
每个微服务管理自己的数据库,服务之间不共享数据存储。这被称为「Database per Service」模式。
// 用户服务:MySQL
@Service
public class UserService {
@Autowired private UserRepository userRepository; // MySQL
}
// 订单服务:PostgreSQL
@Service
public class OrderService {
@Autowired private OrderRepository orderRepository; // PostgreSQL
}
// 商品服务:MongoDB
@Service
public class ProductService {
@Autowired private ProductDocumentRepository productRepository; // MongoDB
}
这样做的好处是:每个服务可以选择最适合自己的数据库,数据模型可以独立演进。代价是跨服务的数据一致性需要额外的机制来保证。
API 通信
微服务之间通过轻量级协议通信,REST 和 gRPC 是最常见的选择。
// REST 调用示例
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public OrderDetail getOrderWithUser(Long orderId) {
// 从用户服务获取用户信息
// 走 HTTP REST API,而非进程内调用
User user = restTemplate.getForObject(
"http://user-service/api/users/{userId}",
User.class,
order.getUserId()
);
return new OrderDetail(order, user);
}
}
// gRPC 调用示例(更高性能)
@Service
public class OrderService {
@Autowired private UserServiceGrpcClient userClient;
public OrderDetail getOrderWithUser(Long orderId) {
// gRPC 基于 HTTP/2,支持双向流,性能比 REST 更好
User user = userClient.getUser(order.getUserId());
return new OrderDetail(order, user);
}
}
去中心化数据管理
微服务强调「每个服务管理自己的数据」,不共享数据库。服务之间通过 API 传递数据,而不是直接查询对方的数据库。
错误做法(微服务中不应该出现):
┌─────────────┐ ┌─────────────┐
│ 用户服务 │ │ 订单服务 │
│ MySQL │←────→│ PostgreSQL │
└──────┬──────┘ └─────────────┘
↓
直接查询对方数据库(耦合)
正确做法:
┌─────────────┐ ┌─────────────┐
│ 用户服务 │ │ 订单服务 │
│ MySQL │ │ PostgreSQL │
└─────────────┘ └──────┬──────┘
↑ ↓
│ API 调用 │
└────────────────────┘
康威定律
Mel Conway 在 1967 年提出:「设计系统的组织,其产生的设计等价于组织内部的沟通结构。」这就是著名的康威定律(Conway's Law)。
┌─────────────────────────────────────────────────────────┐
│ 组织结构决定系统结构 │
│ │
│ 组织: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 用户团队 │ │ 商品团队 │ │ 订单团队 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ↓ ↓ ↓ │
│ 系统: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 用户服务 │ │ 商品服务 │ │ 订单服务 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 如果用户团队和商品团队经常开会讨论问题, │
│ 说明它们应该合并成一个团队,对应的服务也应该合并 │
└─────────────────────────────────────────────────────────┘
微服务架构强调「团队自治」:每个微服务由一个团队独立负责,团队可以自主选择技术栈、决定发布节奏。这就是康威定律的应用:如果要拆分微服务,先调整组织结构。
微服务的优势与代价
优势
独立扩展:每个服务可以独立扩容,不需要复制整个应用。
扩展场景:商品服务是瓶颈
单体架构:
需要复制整个应用,包括不需要扩容的用户、订单模块
成本:4x 资源
微服务架构:
只需要扩容商品服务
成本:1.5x 资源
独立部署:服务之间解耦,可以独立发布。某个服务的变更不需要全量测试,不需要协调其他团队。
技术多样性:不同服务可以用不同语言、不同框架、不同数据库。机器学习服务可以用 Python,实时通讯服务可以用 Go,高并发服务可以用 Rust。
故障隔离:一个服务的问题不会级联扩散到其他服务。整个系统的稳定性更高。
代价
运维复杂度:每个服务需要独立的监控、告警、部署、回滚。10 个服务意味着 10 倍的运维工作。
分布式系统复杂性:服务间通信、网络延迟、超时处理、熔断降级、分布式事务——这些都是单体架构中不存在的问题。
数据一致性:跨服务的数据一致性是微服务最复杂的部分。本地事务变成分布式事务,需要引入 Saga、TCC 等补偿机制。
测试复杂性:端到端测试需要启动多个服务,测试环境的搭建和维护成本高。
微服务 vs 单体的复杂度对比:
| 维度 | 单体 | 微服务 |
| --- | --- | --- |
| 部署 | 1 个应用 | 10~100 个服务 |
| 监控 | 1 套指标 | 10~100 套指标 |
| 日志 | 1 个日志文件 | 10~100 个日志聚合 |
| 数据库 | 1 个实例 | 10~100 个数据库 |
| 事务 | 本地事务 | 分布式事务 |
| 测试 | 单元测试 → 集成测试 | 单元 → 集成 → 端到端 |
| 故障排查 | 单进程调试 | 全链路追踪 |
微服务适合的场景
适合的场景
- 大规模互联网应用:用户量超过 100 万,日活超过 10 万
- 快速迭代需求:需要频繁发布,每天甚至每小时一次
- 多团队协作:团队规模超过 20 人,需要并行开发
- 高并发需求:需要独立扩容特定模块
- 技术多样性需求:不同业务需要不同技术栈
不适合的场景
- 小��队(5 人以下):运维成本超过收益
- 业务边界不清晰:拆分错了边界,后期合并代价更大
- 资金和人力有限:基础设施投入太高
- 简单 CRUD 应用:业务逻辑简单,单体完全够用
微服务 12 要素(The Twelve-Factor App)
Heroku 在 2011 年提出了 12 要素方法论,成为微服务开发的最佳实践指南。
核心要素
为什么要素很重要
12 要素方法论解决的核心问题是:「如何让应用在云环境中稳定运行」。它的每一条规定都是经验总结:
- 无状态:云环境中实例随时可能被销毁重建,状态必须存储到外部
- 端口绑定:云环境中没有固定的服务器端口,服务必须暴露端口
- 日志流:云环境中日志分散在多个实例,需要集中收集
违反 12 要素的常见问题:
# 问题 1:配置写在代码里
public class Config {
private static final String DB_URL = "jdbc:mysql://localhost:3306";
// 问题:换环境需要改代码
# 问题 2:使用本地文件系统存储
File file = new File("/data/uploads/" + filename);
// 问题:云环境中实例没有持久化存储
# 问题 3:使用本地内存缓存
Map<String, Object> cache = new HashMap<>();
// 问题:重启后缓存丢失,多实例之间缓存不同步
配套基础设施
微服务需要配套的基础设施,否则运维成本会高到无法承受。
服务发现
服务实例的 IP 和端口是动态的(容器重启后可能变化),需要服务发现机制来定位服务。
# Kubernetes 服务发现示例
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 8080
targetPort: 8080
# 服务发现:通过服务名访问
# user-service:8080 → 自动解析到对应的 Pod IP
配置中心
每个服务的配置需要统一管理,修改配置不需要重新部署服务。
# Apollo 配置中心示例
app:
id: user-service
config:
# 数据库配置
- key: datasource.url
value: ${DATABASE_URL}
- key: datasource.pool-size
value: 20
# Redis 配置
- key: redis.timeout
value: 3000
链路追踪
分布式系统中,一次请求可能跨越多个服务,需要链路追踪来定位问题。
// OpenTelemetry 链路追踪示例
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{userId}")
public User getUser(@PathVariable Long userId) {
// 自动生成 traceId 和 spanId
// 所有日志和 HTTP 请求会自动关联
return userService.getUser(userId);
}
}
日志聚合
服务日志分散在多个实例,需要集中收集和分析。
# Filebeat + Elasticsearch 日志收集
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
processors:
- add_kubernetes_metadata:
output.elasticsearch:
hosts: ["elasticsearch:9200"]
CI/CD
每个服务需要独立的构建和部署流水线。
# GitHub Actions CI/CD 示例
name: Deploy User Service
on:
push:
branches: [main]
paths: [user-service/**]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Docker Image
run: docker build -t user-service:${{ github.sha }} ./user-service
- name: Push to Registry
run: docker push registry.example.com/user-service:${{ github.sha }}
- name: Deploy to K8s
run: |
kubectl set image deployment/user-service \
user-service=registry.example.com/user-service:${{ github.sha }}
总结
微服务是互联网时代的产物,它继承了 SOA 的服务化理念,吸收了云计算的部署能力,结合了敏捷开发的团队自治思想。
微服务的核心价值:
- 独立部署:让服务可以快速迭代、独立发布
- 独立扩展:让每个模块可以按需扩容
- 团队自治:让团队可以自主决策、自主负责
但微服务也有明确的代价:
- 运维复杂度:需要完善的基础设施支撑
- 分布式复杂性:网络、事务、一致性都有额外成本
- 学习曲线:团队需要掌握 DevOps、容器、K8s 等技能
微服务不是银弹,它是特定场景下的最优解。如果你的团队规模小、业务简单、资金有限,单体架构可能是更好的选择。下一节,我们来看微服务治理的进阶形态:服务网格。
思考题
问题 1:某团队有 15 个人,正在开发一个电商系统,代码量 30 万行。他们应该立即拆成微服务吗?
参考答案
不一定是立即拆。这支团队的规模处于「单体有压力、微服务有收益」的边界线上。建议的路径是:
-
先做模块化:在代码层面做清晰的模块划分(用户模块、商品模块、订单模块),明确模块间的接口。虽然物理上还是单体,但逻辑上已经解耦。
-
建立基础设施:微服务的前提是 DevOps 能力。先把 CI/CD、监控、日志、服务发现建立好,这些能力不是微服务的结果,而是微服务的前提。
-
试点拆分:选择一个风险低、依赖少的模块(如用户模块)做试点,验证微服务拆分的能力和流程。
-
渐进式拆分:根据试点经验,逐步拆分其他模块。6 个月到 1 年完成拆分是合理的节奏。
问题 2:微服务中,订单服务创建订单时需要查询用户信息。用户服务变慢导致订单创建超时,应该怎么处理?
参考答案
这是一个典型的级联失败问题,有几种处理方式:
-
熔断降级:当用户服务连续失败超过阈值时,打开熔断器,直接返回降级结果(如「获取用户信息失败,但订单已创建」)。Resilience4j、Sentinel 等工具支持熔断器。
-
超时设置:为用户服务调用设置合理的超时时间(如 500ms),超时不等待,直接继续处理。
-
异步化:订单创建后,异步获取用户信息并补充到订单详情中。用户在查看订单详情时可能稍晚看到用户名,但订单创建本身不受影响。
-
本地缓存:把用户信息缓存到订单服务,减少对用户服务的依赖。缓存策略需要考虑数据一致性。
问题 3:微服务 12 要素中,「进程无状态」和「后端服务作为附件资源」是什么关系?为什么这样设计?
参考答案
这两个要素是相互关联的,共同解决云环境中的两个问题:
「后端服务作为附件资源」的核心是:数据库、消息队列、缓存等后台服务不应该「内置」在应用中,而应该通过配置注入。环境变化时,只需要改配置,不需要改代码。
「进程无状态」的核心是:应用不依赖本地内存或本地文件系统存储状态。云环境中实例随时可能被销毁重建,如果状态存在内存里,重启就丢失了。
它们的关系是:无状态意味着状态必须存到外部(后端服务)。如果把状态存到 Redis 而不是本地内存,那么容器重启后状态不丢失;如果把状态存到本地文件,那么容器销毁后状态丢失。
这样做的好处是:应用可以随时水平扩展,任意实例都可以处理任意请求,因为它们共享同一个后端状态存储。