微服务依赖治理:依赖分级、故障域、资源隔离、降级预案与容量闭环
微服务稳定性不只是给 Feign 加超时、给接口加熔断。真正的生产问题常常是:订单接口调用了库存、优惠券、会员、推荐、风控、支付、短信、ES、Redis、数据库和第三方接口,其中一个弱依赖变慢,却占满了核心线程池、HTTP 连接池或数据库连接,最后把本来应该成功的核心链路拖垮。
依赖治理要回答的是:
系统依赖了谁,哪些依赖是核心,哪些可以降级,每个依赖最多占多少资源,失败后谁承担后果,怎样证明降级后业务仍然正确。
本页把依赖图、强弱依赖、故障域、资源隔离、降级预案、容量预算、调用链路和 Runbook 串起来。详细熔断状态机、重试和 Deadline 见微服务稳定性治理;注册发现和旧实例窗口见服务发现。
一、学习目标
学完本页应能做到:
- 画出一个业务接口的依赖图和关键路径。
- 区分强依赖、弱依赖、可选依赖、异步依赖和批处理依赖。
- 解释为什么弱依赖也能拖垮强链路。
- 按故障域设计线程池、信号量、连接池、队列和数据库资源隔离。
- 给每个依赖定义超时、重试、熔断、降级、缓存和补偿策略。
- 设计降级返回语义,避免把不确定结果伪装成成功。
- 用容量预算和 Little 定律估算依赖并发上限。
- 在线上故障时从依赖图、Trace、连接池、线程池和业务事实定位问题。
二、先画依赖图,而不是先调参数
假设订单详情接口需要聚合多类信息:
flowchart TD
A["GET /orders/{id}"] --> B["订单库"]
A --> C["库存服务"]
A --> D["优惠券服务"]
A --> E["会员服务"]
A --> F["物流服务"]
A --> G["推荐服务"]
C --> H["库存库"]
D --> I["优惠券库"]
F --> J["第三方物流"]如果不画依赖图,排查时只能看到“订单接口慢”。画出图后才能继续问:
| 问题 | 为什么重要 |
|---|---|
| 哪些调用在关键路径 | 决定总 RT 下限 |
| 哪些可以并行 | 决定是否能缩短响应时间 |
| 哪些失败必须失败 | 决定强依赖和弱依赖 |
| 哪些可以异步 | 决定是否放进主请求 |
| 哪些依赖共享资源 | 决定隔离边界 |
| 哪些依赖有外部副作用 | 决定能否重试和降级 |
依赖图不是架构图装饰,而是容量、超时、隔离、降级和事故排查的输入。
三、强依赖、弱依赖和可选依赖
| 类型 | 定义 | 例子 | 失败策略 |
|---|---|---|---|
| 强依赖 | 不成功就不能完成当前业务承诺 | 下单查商品、库存冻结、支付确认 | 明确失败或进入待确认 |
| 弱依赖 | 失败不影响核心结果,但影响体验或辅助信息 | 推荐、营销标签、非核心画像 | 降级为空、缓存或默认值 |
| 可选依赖 | 有则增强,无则跳过 | 活动弹窗、猜你喜欢 | 快速跳过 |
| 异步依赖 | 不要求同步完成 | 短信通知、ES同步、积分发放 | Outbox、MQ、重试、死信 |
| 批处理依赖 | 离线或定时处理 | 报表、对账、清洗任务 | 限速、断点续跑、补偿 |
| 外部强依赖 | 外部系统决定结果 | 银行、支付渠道、医院接口 | 超时后按UNKNOWN查询确认 |
错误做法是把所有依赖都当强依赖:推荐接口慢,订单接口也慢;短信接口失败,支付回调也失败。另一个错误是把强依赖伪装成弱依赖:库存服务失败时返回“库存充足”,这会造成超卖。
四、关键路径和并行聚合
串行调用耗时会叠加:
订单库80ms + 库存120ms + 优惠100ms + 会员70ms + 物流200ms = 570ms如果库存、优惠、会员、物流可以并行,接口耗时接近:
订单库80ms + max(库存120ms, 优惠100ms, 会员70ms, 物流200ms) = 280ms但并行不是免费午餐。并行会同时占用更多线程、连接和下游容量。错误并行可能把原来每次 1 个下游连接变成同时 5 个连接,让连接池和下游压力瞬间放大。
flowchart TD
A["请求进入订单聚合"] --> B["先查订单主数据"]
B --> C["并行查库存"]
B --> D["并行查优惠"]
B --> E["并行查会员"]
B --> F["并行查物流"]
C --> G["合并强依赖结果"]
D --> H["弱依赖失败则降级"]
E --> H
F --> H
G --> I["返回订单详情"]
H --> I设计并行聚合时必须定义:
- 总 Deadline。
- 每个依赖的子 Deadline。
- 每个依赖的并发上限。
- 强依赖失败后的业务状态。
- 弱依赖降级后的返回字段语义。
- Trace 中每个依赖的 Span 和版本。
五、故障域是什么
故障域是“一处故障会共同影响的资源范围”。微服务中常见故障域:
| 故障域 | 例子 |
|---|---|
| 下游服务 | 库存服务整体慢 |
| 下游实例 | 库存服务某个 Pod Full GC |
| 数据库 | 库存库锁等待 |
| 第三方接口 | 医院 HIS 接口超时 |
| 租户 | 某医院批量采集拖垮全局 |
| 线程池 | 所有 Feign 调用共享一个线程池 |
| 连接池 | 多个依赖共享同一个 HTTP 连接池 |
| MQ 分区 | 热 Key 让单分区堆积 |
依赖治理的目标是让一个故障域不要吞掉其他故障域的资源。例如物流接口慢,不应该占满订单核心线程;某医院采集慢,不应该拖垮全部医院。
六、资源隔离模型
| 隔离方式 | 保护什么 | 适合场景 | 风险 |
|---|---|---|---|
| 信号量隔离 | 限制同时进入某依赖的并发 | 快速、同步、已有线程执行 | 下游阻塞时仍占调用线程 |
| 线程池隔离 | 把某依赖阻塞隔到独立线程池 | 慢第三方、阻塞 HTTP、老 SDK | 线程切换、队列、上下文传播成本 |
| 连接池隔离 | 限制某依赖最多连接数 | HTTP、DB、Redis、MQ | 连接池太大仍会打爆下游 |
| 队列隔离 | 限制等待任务数量 | 异步任务、批处理 | 队列过大会制造长尾 |
| 租户隔离 | 大客户不影响小客户 | B端、医疗、SaaS | 容量碎片和配置复杂 |
| 数据库隔离 | 独立库、连接池或读写分离 | 高风险核心数据 | 成本高,事务边界复杂 |
最常见的事故是“表面隔离,底层共享”。例如给医院接口调用配置了独立线程池,但所有任务共用同一个数据库连接池;第三方慢了以后线程池没满,数据库连接池先被占满,核心接口照样失败。
七、资源预算怎样估算
用 Little 定律做直觉估算:
在途并发 ≈ QPS × 平均耗时如果库存查询目标 200 QPS,正常平均 80ms:
平均在途 ≈ 200 × 0.08 = 16但生产不能把并发上限设 16 后就结束。还要考虑 P99、GC、网络抖动、实例下线、慢启动和突发。可以先设:
库存依赖并发上限 = 25 ~ 40
连接池每路由上限 = 并发上限附近
队列等待上限 = 极小或直接拒绝如果下游数据库只有 30 个可用连接,却给上游 200 个并发许可,本质是在上游排队换成下游排队,不能提高吞吐,只会拉长 P99。
八、每个依赖都要有策略矩阵
| 依赖 | 强弱 | 超时 | 重试 | 隔离 | 降级 | 结果未知处理 |
|---|---|---|---|---|---|---|
| 库存冻结 | 强 | 短,受订单Deadline | 写请求不盲目重试 | 独立连接池和并发许可 | 不返回成功 | 按订单号查询冻结流水 |
| 优惠试算 | 弱或强,取决于业务承诺 | 中等 | 查询可少量重试 | 独立Bulkhead | 不使用优惠或提示重试 | 可重新试算 |
| 会员等级 | 弱 | 短 | 可重试一次 | 共享只读池但有限并发 | 默认普通会员展示 | 后续刷新 |
| 推荐 | 可选 | 极短 | 不重试 | 严格低配额 | 返回空列表 | 无需补偿 |
| 短信 | 异步 | 不进主链路 | MQ重试 | MQ和发送池隔离 | 不影响支付成功 | 失败补发或人工处理 |
| ES同步 | 异步 | 不进写事务 | Outbox重试 | 消费者隔离 | 搜索短暂旧数据 | 重建索引或补偿 |
这张表比“我们用了 Sentinel 和 Resilience4j”更有价值,因为它明确了业务语义和资源边界。
九、降级返回不能伪装成功
降级的目标是保住核心能力,不是把错误藏起来。
错误示例:
public StockResult fallback(String skuId) {
return new StockResult(skuId, 9999, "OK");
}库存服务失败时返回库存充足,会让订单继续创建,最后超卖。正确做法是区分业务语义:
public StockResult fallback(String skuId) {
return StockResult.unknown(skuId, "STOCK_SERVICE_UNAVAILABLE");
}前端或上游可以根据 UNKNOWN 显示“库存确认中”或拒绝下单。支付、库存、余额这类强一致核心链路,宁可明确失败或待确认,也不能把降级值当事实。
常见降级语义:
| 场景 | 合理降级 | 错误降级 |
|---|---|---|
| 推荐列表 | 返回空推荐 | 阻塞订单详情 |
| 会员头像 | 返回默认头像 | 整个用户页失败 |
| 优惠试算 | 提示优惠暂不可用 | 默认给最大优惠 |
| 库存冻结 | 返回失败或待确认 | 默认库存充足 |
| 支付确认 | 返回处理中并查询事实 | 默认支付失败或成功 |
| ES搜索 | 返回缓存或提示稍后 | 把空结果当不存在 |
十、Spring Cloud 示例配置
下面示例不是让所有项目照抄数值,而是演示“每个依赖独立命名、独立预算、独立观察”。
resilience4j:
bulkhead:
instances:
inventoryQuery:
maxConcurrentCalls: 40
maxWaitDuration: 20ms
logisticsQuery:
maxConcurrentCalls: 10
maxWaitDuration: 0
circuitbreaker:
instances:
inventoryQuery:
slidingWindowType: COUNT_BASED
slidingWindowSize: 100
minimumNumberOfCalls: 50
failureRateThreshold: 50
slowCallDurationThreshold: 300ms
slowCallRateThreshold: 60
logisticsQuery:
slidingWindowSize: 50
minimumNumberOfCalls: 20
failureRateThreshold: 40
timelimiter:
instances:
inventoryQuery:
timeoutDuration: 500ms
logisticsQuery:
timeoutDuration: 200ms命名建议使用稳定业务资源名,例如 inventoryQuery、couponCalculate、hospitalHisQuery,不要把订单号、用户ID放进资源名或 Metric Label,否则会造成指标高基数。
十一、JDK 8 Demo:强弱依赖聚合
这个 Demo 用 Java 8 写法演示:订单主数据是强依赖,推荐是弱依赖,弱依赖失败不能拖垮订单。
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public final class DependencyGovernanceDemo {
static final ExecutorService RECOMMEND_POOL = Executors.newFixedThreadPool(2);
static OrderDetail queryOrder(String orderId) {
OrderDetail detail = new OrderDetail(orderId);
detail.items = Arrays.asList("P100", "P200");
return detail;
}
static List<String> queryRecommendSlowly() throws Exception {
Thread.sleep(1000);
return Arrays.asList("R1", "R2");
}
static OrderDetail getOrderDetail(String orderId) throws Exception {
OrderDetail detail = queryOrder(orderId);
Future<List<String>> recommendFuture = RECOMMEND_POOL.submit(new Callable<List<String>>() {
@Override
public List<String> call() throws Exception {
return queryRecommendSlowly();
}
});
try {
detail.recommends = recommendFuture.get(100, TimeUnit.MILLISECONDS);
} catch (Exception ex) {
recommendFuture.cancel(true);
detail.recommends = Arrays.asList();
detail.degraded = true;
detail.degradeReason = "RECOMMEND_TIMEOUT";
}
return detail;
}
static final class OrderDetail {
final String orderId;
List<String> items;
List<String> recommends;
boolean degraded;
String degradeReason;
OrderDetail(String orderId) {
this.orderId = orderId;
}
}
public static void main(String[] args) throws Exception {
OrderDetail detail = getOrderDetail("O1001");
System.out.println(detail.orderId + ", degraded=" + detail.degraded + ", reason=" + detail.degradeReason);
RECOMMEND_POOL.shutdown();
}
}注意边界:
Future.cancel(true)只是中断信号,底层阻塞库未必真正停止。- 弱依赖可以返回空推荐,但强依赖不能伪造业务事实。
- 生产要用有界线程池、连接池、超时、Bulkhead、Trace 和指标。
十二、线上依赖故障 Runbook
12.1 一个弱依赖拖慢核心接口
- 从入口指标确认哪个接口 P99 升高。
- 用 Trace 聚合慢 Span,确认慢在推荐、物流、会员还是库存。
- 查该依赖的线程池 active、queue、reject。
- 查 HTTP 连接池 acquire 等待和每路由连接数。
- 查是否所有依赖共享同一个线程池或连接池。
- 先把弱依赖降级或限流,保护核心接口。
- 再修复依赖慢点,不能先把线程池无限调大。
12.2 强依赖超时
- 判断请求是否已经到达下游。
- 如果是写操作,按业务幂等号查询事实。
- 如果事实成功,补返回或推进状态。
- 如果事实失败,明确失败或允许用户重试同一业务意图。
- 如果事实未知,进入待确认、补偿或人工对账。
- 检查下游 P99、数据库锁、连接池、重试和熔断状态。
12.3 隔离配置了但仍然雪崩
常见原因:
| 原因 | 解释 |
|---|---|
| 线程池隔离但连接池共享 | 慢依赖占满底层连接 |
| Bulkhead许可大于下游容量 | 更多请求进入下游排队 |
| 队列太大 | 拒绝变成长时间等待 |
| fallback又远程调用 | 降级路径再次依赖故障域 |
| 多层重试 | 入口、Feign、HTTP Client、Mesh同时重试 |
| 指标没有按依赖拆 | 只能看到总接口慢,看不到谁慢 |
十三、商业场景
13.1 医疗采集平台
采集服务依赖医院 HIS、字典服务、规则服务、数据库、MQ 和 ES:
| 依赖 | 治理策略 |
|---|---|
| 医院 HIS | 每医院独立并发上限、短超时、失败批次补偿 |
| 字典服务 | 本地缓存、版本号、短超时、失败使用旧版本并标记 |
| 规则服务 | 强版本依赖,灰度按医院隔离 |
| 数据库 | 批量大小受连接池和慢SQL约束 |
| MQ | 异步入库削峰,Lag和最老消息年龄告警 |
| ES | Outbox或CDC同步,失败重试和重建索引 |
13.2 订单详情聚合
订单主数据和库存是强依赖;推荐、活动、物流轨迹可以弱化:
- 订单主数据失败,接口失败。
- 库存状态未知,返回“库存确认中”或禁止提交。
- 推荐失败,返回空列表并记录降级原因。
- 物流失败,展示“暂不可查”。
- 每个依赖都有独立 P99、错误率、Bulkhead rejected 和连接池等待指标。
13.3 支付回调
支付回调链路不应该同步调用积分、短信、ES 和报表:
- 校验签名、金额、订单状态。
- 本地事务更新订单为已支付。
- 同事务写 Outbox 事件。
- 事务提交后异步投递 MQ。
- 积分、短信、ES 各自幂等消费。
- 失败进入重试、死信、补偿和对账。
十四、面试标准回答
微服务依赖治理要先画依赖图,区分强依赖、弱依赖、可选依赖、异步依赖和外部依赖。
强依赖失败会影响业务承诺,弱依赖失败只能影响体验,不能让推荐、短信、画像这类弱依赖
拖垮下单、支付、库存这类核心链路。
生产上要按故障域做资源隔离:线程池、信号量、HTTP连接池、数据库连接池、队列和租户
都可能成为隔离边界。每个依赖要有自己的超时、重试、Bulkhead、熔断、降级和补偿策略。
降级不能伪装成功,库存、支付、余额这类强语义接口超时后应返回失败或UNKNOWN,再按
幂等号查询事实,而不是默认成功。
排查时先从依赖图和Trace找慢Span,再查依赖维度P99、错误率、线程池、连接池、队列、
重试次数和下游数据库。不能只把线程池调大,因为真实瓶颈可能是下游连接、锁、CPU或
第三方配额。十五、关联知识点
| 知识点 | 入口 |
|---|---|
| 微服务稳定性治理 | 超时、重试、隔离、熔断 |
| 分布式限流 | 限流算法与生产治理 |
| 服务发现 | 注册、订阅、本地缓存 |
| Spring Cloud调用链 | 端到端调用流程 |
| Resilience4j | Resilience4j内部原理 |
| Sentinel | Sentinel流量治理 |
| 可靠消息最终一致 | Outbox与事务消息 |
