Skip to content

微服务依赖治理:依赖分级、故障域、资源隔离、降级预案与容量闭环

微服务稳定性不只是给 Feign 加超时、给接口加熔断。真正的生产问题常常是:订单接口调用了库存、优惠券、会员、推荐、风控、支付、短信、ES、Redis、数据库和第三方接口,其中一个弱依赖变慢,却占满了核心线程池、HTTP 连接池或数据库连接,最后把本来应该成功的核心链路拖垮。

依赖治理要回答的是:

系统依赖了谁,哪些依赖是核心,哪些可以降级,每个依赖最多占多少资源,失败后谁承担后果,怎样证明降级后业务仍然正确。

本页把依赖图、强弱依赖、故障域、资源隔离、降级预案、容量预算、调用链路和 Runbook 串起来。详细熔断状态机、重试和 Deadline 见微服务稳定性治理;注册发现和旧实例窗口见服务发现

一、学习目标

学完本页应能做到:

  1. 画出一个业务接口的依赖图和关键路径。
  2. 区分强依赖、弱依赖、可选依赖、异步依赖和批处理依赖。
  3. 解释为什么弱依赖也能拖垮强链路。
  4. 按故障域设计线程池、信号量、连接池、队列和数据库资源隔离。
  5. 给每个依赖定义超时、重试、熔断、降级、缓存和补偿策略。
  6. 设计降级返回语义,避免把不确定结果伪装成成功。
  7. 用容量预算和 Little 定律估算依赖并发上限。
  8. 在线上故障时从依赖图、Trace、连接池、线程池和业务事实定位问题。

二、先画依赖图,而不是先调参数

假设订单详情接口需要聚合多类信息:

mermaid
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查询确认

错误做法是把所有依赖都当强依赖:推荐接口慢,订单接口也慢;短信接口失败,支付回调也失败。另一个错误是把强依赖伪装成弱依赖:库存服务失败时返回“库存充足”,这会造成超卖。

四、关键路径和并行聚合

串行调用耗时会叠加:

text
订单库80ms + 库存120ms + 优惠100ms + 会员70ms + 物流200ms = 570ms

如果库存、优惠、会员、物流可以并行,接口耗时接近:

text
订单库80ms + max(库存120ms, 优惠100ms, 会员70ms, 物流200ms) = 280ms

但并行不是免费午餐。并行会同时占用更多线程、连接和下游容量。错误并行可能把原来每次 1 个下游连接变成同时 5 个连接,让连接池和下游压力瞬间放大。

mermaid
flowchart TD
    A["请求进入订单聚合"] --> B["先查订单主数据"]
    B --> C["并行查库存"]
    B --> D["并行查优惠"]
    B --> E["并行查会员"]
    B --> F["并行查物流"]
    C --> G["合并强依赖结果"]
    D --> H["弱依赖失败则降级"]
    E --> H
    F --> H
    G --> I["返回订单详情"]
    H --> I

设计并行聚合时必须定义:

  1. 总 Deadline。
  2. 每个依赖的子 Deadline。
  3. 每个依赖的并发上限。
  4. 强依赖失败后的业务状态。
  5. 弱依赖降级后的返回字段语义。
  6. Trace 中每个依赖的 Span 和版本。

五、故障域是什么

故障域是“一处故障会共同影响的资源范围”。微服务中常见故障域:

故障域例子
下游服务库存服务整体慢
下游实例库存服务某个 Pod Full GC
数据库库存库锁等待
第三方接口医院 HIS 接口超时
租户某医院批量采集拖垮全局
线程池所有 Feign 调用共享一个线程池
连接池多个依赖共享同一个 HTTP 连接池
MQ 分区热 Key 让单分区堆积

依赖治理的目标是让一个故障域不要吞掉其他故障域的资源。例如物流接口慢,不应该占满订单核心线程;某医院采集慢,不应该拖垮全部医院。

六、资源隔离模型

隔离方式保护什么适合场景风险
信号量隔离限制同时进入某依赖的并发快速、同步、已有线程执行下游阻塞时仍占调用线程
线程池隔离把某依赖阻塞隔到独立线程池慢第三方、阻塞 HTTP、老 SDK线程切换、队列、上下文传播成本
连接池隔离限制某依赖最多连接数HTTP、DB、Redis、MQ连接池太大仍会打爆下游
队列隔离限制等待任务数量异步任务、批处理队列过大会制造长尾
租户隔离大客户不影响小客户B端、医疗、SaaS容量碎片和配置复杂
数据库隔离独立库、连接池或读写分离高风险核心数据成本高,事务边界复杂

最常见的事故是“表面隔离,底层共享”。例如给医院接口调用配置了独立线程池,但所有任务共用同一个数据库连接池;第三方慢了以后线程池没满,数据库连接池先被占满,核心接口照样失败。

七、资源预算怎样估算

用 Little 定律做直觉估算:

text
在途并发 ≈ QPS × 平均耗时

如果库存查询目标 200 QPS,正常平均 80ms:

text
平均在途 ≈ 200 × 0.08 = 16

但生产不能把并发上限设 16 后就结束。还要考虑 P99、GC、网络抖动、实例下线、慢启动和突发。可以先设:

text
库存依赖并发上限 = 25 ~ 40
连接池每路由上限 = 并发上限附近
队列等待上限 = 极小或直接拒绝

如果下游数据库只有 30 个可用连接,却给上游 200 个并发许可,本质是在上游排队换成下游排队,不能提高吞吐,只会拉长 P99。

八、每个依赖都要有策略矩阵

依赖强弱超时重试隔离降级结果未知处理
库存冻结短,受订单Deadline写请求不盲目重试独立连接池和并发许可不返回成功按订单号查询冻结流水
优惠试算弱或强,取决于业务承诺中等查询可少量重试独立Bulkhead不使用优惠或提示重试可重新试算
会员等级可重试一次共享只读池但有限并发默认普通会员展示后续刷新
推荐可选极短不重试严格低配额返回空列表无需补偿
短信异步不进主链路MQ重试MQ和发送池隔离不影响支付成功失败补发或人工处理
ES同步异步不进写事务Outbox重试消费者隔离搜索短暂旧数据重建索引或补偿

这张表比“我们用了 Sentinel 和 Resilience4j”更有价值,因为它明确了业务语义和资源边界。

九、降级返回不能伪装成功

降级的目标是保住核心能力,不是把错误藏起来。

错误示例:

java
public StockResult fallback(String skuId) {
    return new StockResult(skuId, 9999, "OK");
}

库存服务失败时返回库存充足,会让订单继续创建,最后超卖。正确做法是区分业务语义:

java
public StockResult fallback(String skuId) {
    return StockResult.unknown(skuId, "STOCK_SERVICE_UNAVAILABLE");
}

前端或上游可以根据 UNKNOWN 显示“库存确认中”或拒绝下单。支付、库存、余额这类强一致核心链路,宁可明确失败或待确认,也不能把降级值当事实。

常见降级语义:

场景合理降级错误降级
推荐列表返回空推荐阻塞订单详情
会员头像返回默认头像整个用户页失败
优惠试算提示优惠暂不可用默认给最大优惠
库存冻结返回失败或待确认默认库存充足
支付确认返回处理中并查询事实默认支付失败或成功
ES搜索返回缓存或提示稍后把空结果当不存在

十、Spring Cloud 示例配置

下面示例不是让所有项目照抄数值,而是演示“每个依赖独立命名、独立预算、独立观察”。

yaml
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

命名建议使用稳定业务资源名,例如 inventoryQuerycouponCalculatehospitalHisQuery,不要把订单号、用户ID放进资源名或 Metric Label,否则会造成指标高基数。

十一、JDK 8 Demo:强弱依赖聚合

这个 Demo 用 Java 8 写法演示:订单主数据是强依赖,推荐是弱依赖,弱依赖失败不能拖垮订单。

java
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();
    }
}

注意边界:

  1. Future.cancel(true) 只是中断信号,底层阻塞库未必真正停止。
  2. 弱依赖可以返回空推荐,但强依赖不能伪造业务事实。
  3. 生产要用有界线程池、连接池、超时、Bulkhead、Trace 和指标。

十二、线上依赖故障 Runbook

12.1 一个弱依赖拖慢核心接口

  1. 从入口指标确认哪个接口 P99 升高。
  2. 用 Trace 聚合慢 Span,确认慢在推荐、物流、会员还是库存。
  3. 查该依赖的线程池 active、queue、reject。
  4. 查 HTTP 连接池 acquire 等待和每路由连接数。
  5. 查是否所有依赖共享同一个线程池或连接池。
  6. 先把弱依赖降级或限流,保护核心接口。
  7. 再修复依赖慢点,不能先把线程池无限调大。

12.2 强依赖超时

  1. 判断请求是否已经到达下游。
  2. 如果是写操作,按业务幂等号查询事实。
  3. 如果事实成功,补返回或推进状态。
  4. 如果事实失败,明确失败或允许用户重试同一业务意图。
  5. 如果事实未知,进入待确认、补偿或人工对账。
  6. 检查下游 P99、数据库锁、连接池、重试和熔断状态。

12.3 隔离配置了但仍然雪崩

常见原因:

原因解释
线程池隔离但连接池共享慢依赖占满底层连接
Bulkhead许可大于下游容量更多请求进入下游排队
队列太大拒绝变成长时间等待
fallback又远程调用降级路径再次依赖故障域
多层重试入口、Feign、HTTP Client、Mesh同时重试
指标没有按依赖拆只能看到总接口慢,看不到谁慢

十三、商业场景

13.1 医疗采集平台

采集服务依赖医院 HIS、字典服务、规则服务、数据库、MQ 和 ES:

依赖治理策略
医院 HIS每医院独立并发上限、短超时、失败批次补偿
字典服务本地缓存、版本号、短超时、失败使用旧版本并标记
规则服务强版本依赖,灰度按医院隔离
数据库批量大小受连接池和慢SQL约束
MQ异步入库削峰,Lag和最老消息年龄告警
ESOutbox或CDC同步,失败重试和重建索引

13.2 订单详情聚合

订单主数据和库存是强依赖;推荐、活动、物流轨迹可以弱化:

  1. 订单主数据失败,接口失败。
  2. 库存状态未知,返回“库存确认中”或禁止提交。
  3. 推荐失败,返回空列表并记录降级原因。
  4. 物流失败,展示“暂不可查”。
  5. 每个依赖都有独立 P99、错误率、Bulkhead rejected 和连接池等待指标。

13.3 支付回调

支付回调链路不应该同步调用积分、短信、ES 和报表:

  1. 校验签名、金额、订单状态。
  2. 本地事务更新订单为已支付。
  3. 同事务写 Outbox 事件。
  4. 事务提交后异步投递 MQ。
  5. 积分、短信、ES 各自幂等消费。
  6. 失败进入重试、死信、补偿和对账。

十四、面试标准回答

text
微服务依赖治理要先画依赖图,区分强依赖、弱依赖、可选依赖、异步依赖和外部依赖。
强依赖失败会影响业务承诺,弱依赖失败只能影响体验,不能让推荐、短信、画像这类弱依赖
拖垮下单、支付、库存这类核心链路。

生产上要按故障域做资源隔离:线程池、信号量、HTTP连接池、数据库连接池、队列和租户
都可能成为隔离边界。每个依赖要有自己的超时、重试、Bulkhead、熔断、降级和补偿策略。
降级不能伪装成功,库存、支付、余额这类强语义接口超时后应返回失败或UNKNOWN,再按
幂等号查询事实,而不是默认成功。

排查时先从依赖图和Trace找慢Span,再查依赖维度P99、错误率、线程池、连接池、队列、
重试次数和下游数据库。不能只把线程池调大,因为真实瓶颈可能是下游连接、锁、CPU或
第三方配额。

十五、关联知识点

知识点入口
微服务稳定性治理超时、重试、隔离、熔断
分布式限流限流算法与生产治理
服务发现注册、订阅、本地缓存
Spring Cloud调用链端到端调用流程
Resilience4jResilience4j内部原理
SentinelSentinel流量治理
可靠消息最终一致Outbox与事务消息