主题
界面
主题
《Dubbo 与 RPC 基础》讲了服务之间怎么通信;本节讲**"为什么要拆、拆到什么程度"**。
★ 微服务的收益众所周知,但它的代价很少被量化 ✓ ——本节补上这两笔账:
① 延迟账:一次业务从「1 次本地调用」变成「5 次远程调用」
② 可用性账:依赖 5 个 99.9% 的服务,整体变成多少先看一笔可用性账 ✓✓:
假设每个服务的可用性是 99.9%(一年停机 8.8 小时)
串联服务数 整体可用性 一年停机时长
1 99.9000% 8.8 小时
2 99.8001% 17.5 小时
3 99.7003% 26.2 小时 ← 已低于「3 个 9」
5 99.5001% 43.8 小时 ← 接近「2 个 9」,年停机近 2 天 ✗
10 99.0045% 87.2 小时 ← 不可接受 ✗✗★★ 这就是"微服务会降低可用性"的数学依据:串联的可靠性是乘法 ✓✓
★ 而且这还没算上网络故障与服务发现的开销 ⚠️
实测(5 步下单流程,每步真实工作 2 ms) ✓✓:
① 单体(5 次本地调用): 10030 微秒(10.03 ms)✓✓
② 微服务(5 次远程调用): 32406 微秒(32.41 ms)
→ 微服务版慢 **3.23 倍**,多花 22.38 ms(本机回环,最乐观的情况)✗★ 关键认识 ✓✓:
每多一次远程调用,就要多花约 4.5 ms(本机) ✗
跨机房时,每次远程调用的网络往返是 5~50 ms —— 5 步串行就可能多出几十到几百毫秒 ✗✗
(呼应 《Dubbo 与 RPC 基础》:RPC 的固定成本约 0.8 ms,跨机房主要花在网络往返上)
★ 三种降低代价的手段 ✓✓:
| 手段 | 做法 | 代价 |
|---|---|---|
| 能并行的并行 ★★★ | 5 步里 3 步无依赖 → 并发调用 | 代码复杂度 ⚠️ |
| 合并边界 ★★ | 把"总是一起调用"的服务合回去 | 放弃一部分独立性 |
| 异步化 ★★★ | 非核心步骤(通知)发消息异步做 | 一致性变最终一致 ⚠️ |
★★ 最重要的一条 ✓✓:
「跨服务调用次数」是微服务架构的第一性能指标 ✓
设计时数一数「一次用户操作要跨几次服务」,超过 3~5 次就该重新考虑边界 ✓✓
见第 4 节的表 ✓——N 个 99.9% 的服务串联,整体 = 0.999^N ✓
★ 四种补偿手段(真实架构都在用) ✓✓:
| 手段 | 做法 | 效果 |
|---|---|---|
| 降级 ★★★ | 非核心依赖失败时返回兜底 | 把"依赖"变成"可选" ✓✓ |
| 超时 + 熔断 ★★★ | 快速失败,不拖垮上游 | 防止雪崩(《Sentinel 限流与熔断》)✓✓ |
| 冗余与多实例 ★★ | 单实例可用性 → 集群可用性 | 需要无状态 ✓ |
| 减少串联 ★★★ | 能异步就异步、能合并就合并 | 从源头减少乘法项 ✓✓ |
★★ 最容易被忽略的是第一行 ✓✓:
不是所有依赖都必须成功 ✓
「推荐服务挂了,商品页还是要能打开」→ 推荐位显示兜底内容 ✓✓
设计微服务时,给每个依赖标注「核心 / 非核心」是必做功课 ✓✓
★ 一个务实的做法 ✓✓:
// 核心链路:库存不足就返回失败(必须成功才能继续)
inventoryService.deduct(request);
// 非核心链路:包一层 try-catch + 超时,失败就记日志继续
try {
pointsService.add(request); // 加积分失败不影响下单
} catch (Exception e) {
log.warn("加积分失败,已忽略", e);
}★ 什么该先拆 ✓✓:
| 信号 | 判断 | 动作 |
|---|---|---|
| 这块代码变更频率是其他的 10 倍 ★★★ | 变化率差异大 | 优先拆(拆分的第一驱动力)✓ |
| 两个模块的团队天天互相等发布 ★★★ | 团队边界清晰 | 优先拆(康威定律)✓ |
| 某模块要独立扩容(如秒杀) ★★★ | 伸缩需求不同 | 优先拆 ✓ |
| 共享同一张表、同一事务 ✗✗ | 数据强耦合 | 先别拆(拆了要处理分布式事务)✗ |
| 必须同步一致(余额与流水) ✗✗ | 不能用最终一致 | 先别拆或放同一服务 ✗ |
| 团队只有 3 个人 ✗✗ | 没有团队边界 | 别拆(只会增加沟通成本)✗✗ |
★★ 一句话原则 ✓✓:
按「业务能力 + 变化率 + 团队边界」拆,不按「技术分层」拆 ✓✓
★ 反例(很常见) ✗✗:
✗ 拆成「Controller 服务 / Service 服务 / DAO 服务」
→ 这是把「分层」变成了「分布式」
→ 等于给每次调用都加一次网络开销 ✗✗(呼应 5.1 的延迟账)★ 拆分的正确顺序(务实版) ✓✓:
1. 先把单体做好模块化(清晰的包边界 + 接口,即 S4-002 的 DIP)✓✓
2. 找出「变化最频繁 + 要独立扩容」的那一块,先拆它一个 ✓
3. 建立可观测性(链路追踪、日志聚合)
—— 没有它,拆完就查不出问题 ✗✗
4. 再考虑第二块、第三块……每一步都要能回退 ✓| 收益 | 代价 |
|---|---|
| 独立部署(改一个服务不用重发全部)★★★ | 分布式复杂度(网络、事务、一致性)✗✗ |
| 独立扩容(只扩热点服务)★★★ | 延迟增加(每步一次网络)✗ |
| 技术异构(不同服务用不同技术栈)★ | 运维成本(N 个服务 × 部署/监控/告警)✗✗ |
| 团队自治(各团队有自己的发布节奏)★★★ | 可用性下降(串联乘法)✗✗ |
| 故障隔离(一个服务挂了不拖垮全部)★★ | 排障更难(要跨服务追踪)✗✗ |
★★ 一条务实的判断 ✓✓:
只有当"独立部署"和"团队自治"的收益超过了上面所有代价时,才该拆 ✓
而这两个收益都依赖同一个前提:团队规模 ✓✓
10 个人以下的团队,微服务几乎总是过早优化 ✗✗
★ 三种架构的取舍 ✓:
| 架构 | 适合 | 典型 |
|---|---|---|
| 单体 ★★ | 小团队、需求快速变化、边界还不清晰 | 早期创业项目、内部系统 ✓ |
| 模块化单体 ★★★ | 团队 10~30 人、边界开始清晰 | 大多数团队的正确答案 ✓✓ |
| 微服务 ★★ | 多团队、边界稳定、需要独立伸缩 | 大型平台 ✓ |
★ "模块化单体"是被低估的选项 ✓✓:
它有清晰的模块边界(靠包和接口约束) ✓
但模块间是本地调用(没有网络开销、没有分布式事务) ✓✓
将来要拆时,因为接口已经清晰,拆出去也不难 ✓✓
★★ 最后一句忠告 ✓✓:
《软件工程与开发流程》 说的「复杂度、变化、沟通」才是要解决的问题 ✓
微服务只是其中一种手段,而且是有显著代价的那一种 ✓✓
// ✗ 串行:3 次远程调用,延迟相加
User user = userService.get(id);
Coupon coupon = couponService.get(id);
Points points = pointsService.get(id);
// ✓ 并行:延迟取最大值(用 CompletableFuture)
CompletableFuture<User> f1 = CompletableFuture.supplyAsync(() -> userService.get(id), pool);
CompletableFuture<Coupon> f2 = CompletableFuture.supplyAsync(() -> couponService.get(id), pool);
CompletableFuture<Points> f3 = CompletableFuture.supplyAsync(() -> pointsService.get(id), pool);
CompletableFuture.allOf(f1, f2, f3).join();★ 3 次 50 ms 的调用:串行 150 ms → 并行 50 ms ✓✓
见 5.2 ✓
✗ "我们要做微服务,所以把用户服务拆成用户查询服务 + 用户写入服务"
→ 读写本来就要一致,拆开只会引入分布式事务 ✗✗代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/MicroserviceCostLesson.java
运行:
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.micro.MicroserviceCostLesson实测内容 ✓:
| 实验 | 量出什么 |
|---|---|
| 单体 vs 微服务的延迟 | 5 步流程本地调用 vs 5 次远程调用的差距 ✓ |
| 可用性乘法表 | 1/2/3/5/10 个串联服务的整体可用性与年停机时长 ✓✓ |
| 拆分信号对照表 | 六个信号分别对应"该拆/别拆" ✓ |
★ 三条可落地的结论 ✓:
| # | 结论 | 依据 |
|---|---|---|
| ① | 串联依赖的可用性是乘法 | 5 个 99.9% → 99.5%(年停机近 2 天)✗ |
| ② | 必须给依赖标注核心/非核心,并配降级 | 否则一个非核心服务拖垮全站 ✗✗ |
| ③ | 小团队优先做"模块化单体" | 收益/代价比最优 ✓✓ |
| # | 错误 | 说明 |
|---|---|---|
| 1 | 按技术分层拆(Controller/Service/DAO) ✗✗ | 等于给每次调用加网络开销 ✓✓ |
| 2 | 小团队上微服务 ✗✗ | 没有团队边界,只有沟通成本 ✓ |
| 3 | 所有依赖都当核心 ✗✗ | 一个非核心服务挂掉拖垮全站 ✓ |
| 4 | 不配超时与熔断 ✗✗ | 雪崩(《Sentinel 限流与熔断》)✓ |
| 5 | 拆完才发现没有链路追踪 ✗✗ | 出问题查不出来 ✓ |
| 6 | 共享数据库却说是"微服务" ✗✗ | 数据耦合没解决,只是改了调用方式 ✓ |
| 7 | 一次大爆炸式拆分 ✗✗ | 无法回退 ✓✓ |
目标:算清自己系统的两笔账。
步骤:
MicroserviceCostLesson,记录延迟与可用性数据 ✓;验收方式:能说出"你的系统拆得对不对、最该补的是什么"。
扩展任务:写一份"该不该拆服务 X"的决策记录(ADR):
团队 5 人,一个单体应用 8 万行代码,
订单、用户、商品三个模块共享同一张订单表。该拆微服务吗?为什么? 5. (设计)给"商品详情页"设计依赖方案:要查商品、库存、评价、推荐,要求推荐服务挂了页面也能打开。
《Spring Cloud 核心组件》:网关、负载均衡、熔断、配置。