190|微服务故障治理:级联故障、流量控制、故障演练与自愈

微服务与服务治理约 16 分钟阅读9,059 字符
知识库文档 191 / 251

阅读时间:约 45 分钟 难度:中高级 前置知识:微服务基础架构、分布式系统基本原理


引言

微服务架构将单体拆分为独立部署的服务单元,每个服务可独立扩缩容、独立迭代。但拆分也带来了分布式系统固有的复杂性:一个服务的故障可能像多米诺骨牌一样推倒整个系统。故障治理不是"出了事再修",而是一套贯穿设计、开发、测试、运维全周期的系统性工程实践。

本文从四个维度展开:级联故障的根因与阻断、流量控制的策略与实现、故障演练的方法论、自愈能力的构建。读完你将理解:为什么 Netflix 的 Chaos Monkey 要在生产环境随机杀实例?为什么 Sentinel 的滑动窗口比固定窗口更精确?为什么重试不是越多越好?


一、级联故障:从单点异常到全局雪崩

1.1 什么是级联故障

级联故障(Cascading Failure) 是指分布式系统中,一个组件的故障引发依赖它的其他组件也相继故障,最终导致整个系统不可用。这个过程像多米诺骨牌:第一张倒下后,连锁反应不可阻挡。

经典案例:假设服务 A 调用服务 B,服务 B 调用服务 C。如果 C 的数据库发生慢查询,导致 C 响应变慢,B 的线程池被 C 的慢请求占满,A 调用 B 也开始超时,最终整个调用链全部阻塞。

1.2 级联故障的三种典型模式

模式一:线程池耗尽(Thread Pool Exhaustion)

每个服务通常使用线程池处理请求。当下游服务响应变慢时,上游的线程会长时间阻塞等待响应,线程池逐渐被占满。新的请求到来时无法获取线程,直接被拒绝或超时。

code
请求 → [服务A 线程池满] → 超时
         ↓ 等待
       [服务B 线程池满] → 超时
         ↓ 等待
       [服务C 响应慢]

关键指标:线程池活跃线程数、队列长度、拒绝次数。

模式二:重试风暴(Retry Storm)

当一个请求失败后,调用方自动重试。如果重试策略不当,一个下游的短暂抖动可能被上游的重试放大数倍甚至数十倍,形成"重试风暴"。

假设服务 A 每秒 1000 QPS 调用服务 B,B 出现 1 秒抖动导致 10% 失败。如果 A 配置了 3 次重试,那么这 1 秒内:

  • 正常请求:900 次
  • 失败请求:100 次,触发 300 次重试
  • 总计 B 收到的请求:900 + 300 = 1200 次

重试放大了 20% 的流量,可能让 B 的抖动变成彻底崩溃。

模式三:资源争抢(Resource Contention)

多个服务共享同一资源(数据库连接池、Redis 连接、CPU、内存),一个服务的异常使用导致其他服务无法获取资源。例如,一个服务的慢 SQL 占满数据库连接池,导致所有依赖该数据库的服务都不可用。

文章配图
文章配图

1.3 阻断级联故障的核心武器

熔断器(Circuit Breaker)

熔断器是阻断级联故障最经典的机制,灵感来自电气工程中的断路器。

三种状态:

code
         ┌──────────┐
         │  CLOSED  │ ← 正常状态,请求正常通过
         └────┬─────┘
              │ 失败次数达到阈值
              ▼
         ┌──────────┐
         │   OPEN   │ ← 熔断状态,请求直接拒绝(快速失败)
         └────┬─────┘
              │ 等待一段时间(Sleep Window)
              ▼
         ┌──────────┐
         │ HALF_OPEN│ ← 半开状态,放行少量请求探测
         └────┬─────┘
              │ 探测成功 → CLOSED
              │ 探测失败 → OPEN
              ▼

文章配图
文章配图

关键参数:

  • 失败阈值:滑动窗口内失败次数或失败率超过阈值时触发熔断
  • 休眠窗口:熔断后等待多久进入半开状态
  • 半开最大请求数:半开状态下允许通过的探测请求数
  • 慢调用阈值:响应时间超过此值也计入失败(Resilience4j 支持)

Resilience4j 熔断器示例:

java
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)          // 失败率 50% 触发熔断
    .slowCallRateThreshold(50)         // 慢调用率 50% 触发熔断
    .slowCallDurationThreshold(Duration.ofSeconds(2))  // 超过 2s 算慢调用
    .slidingWindowType(SlidingWindowType.COUNT_BASED)  // 基于计数
    .slidingWindowSize(10)             // 窗口大小 10 次
    .minimumNumberOfCalls(5)           // 最少 5 次调用才开始计算
    .waitDurationInOpenState(Duration.ofSeconds(10))  // 熔断 10s
    .permittedNumberOfCallsInHalfOpenState(3)  // 半开放行 3 次
    .build();

CircuitBreaker circuitBreaker = CircuitBreaker.of("backendService", config);

// 使用熔断器包装调用
Supplier<String> decorated = CircuitBreaker.decorateSupplier(
    circuitBreaker, () -> backendService.call()
);

Sentinel 熔断规则:

java
DegradeRule rule = new DegradeRule("resourceName")
    .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO)  // 异常比例
    .setCount(0.5)       // 异常比例阈值 50%
    .setTimeWindow(10);  // 熔断时长 10s
DegradeRuleManager.loadRules(Collections.singletonList(rule));

Sentinel 支持三种熔断策略:

  • 慢调用比例:响应时间超过最大 RT 的请求比例
  • 异常比例:异常数占总请求数的比例
  • 异常数:分钟级异常数绝对值

舱壁隔离(Bulkhead)

舱壁模式借鉴了船舶设计:船体被分成多个密封舱,一个舱进水不会导致整船沉没。

两种实现方式:

1. 线程池隔离:为不同下游服务分配独立的线程池。

java
// Hystrix 线程池隔离(已停维,但思想值得理解)
@HystrixCommand(
    threadPoolKey = "serviceA-pool",
    threadPoolProperties = {
        @HystrixProperty(name = "coreSize", value = "10"),
        @HystrixProperty(name = "maxQueueSize", value = "5")
    }
)

2. 信号量隔离:限制同时访问某个资源的并发数。

java
// Resilience4j 信号量舱壁
BulkheadConfig config = BulkheadConfig.custom()
    .maxConcurrentCalls(10)     // 最大并发调用数
    .maxWaitDuration(Duration.ofMillis(100))  // 等待超时
    .build();

Bulkhead bulkhead = Bulkhead.of("serviceB", config);

线程池隔离 vs 信号量隔离:

维度 线程池隔离 信号量隔离
隔离程度 强(不同线程池) 弱(仅限制并发数)
开销 大(线程切换、队列) 小(仅计数器)
超时控制 支持异步超时 不支持
ThreadLocal 传递 需要额外处理 天然支持
适用场景 对外部依赖的高隔离要求 对内部服务的高性能隔离

重试策略的理性设计

重试不是越多越好,需要遵循以下原则:

1. 退避(Backoff):重试间隔应逐渐增大,避免瞬时冲击。

java
// 指数退避
long waitTime = (long) (Math.pow(2, retryCount) * baseInterval);
// retryCount=0: 1s, retryCount=1: 2s, retryCount=2: 4s, retryCount=3: 8s

2. 抖动(Jitter):在退避基础上增加随机偏移,避免多个客户端同时重试形成"惊群效应"。

java
long waitTime = (long) (Math.pow(2, retryCount) * baseInterval 
    + ThreadLocalRandom.current().nextLong(0, 1000));

3. 幂等性保证:重试的请求必须是幂等的,否则重复执行可能造成数据错误。

4. 熔断感知:重试应在熔断器未打开时进行,熔断期间不应重试。

5. 超时控制:每次重试请求都要有独立的超时时间,且总超时不应超过上游可接受的等待时间。

超时与快速失败

超时是最基本也最容易被忽视的防护手段。没有超时,一个慢请求会无限期占用线程。

java
// HTTP 客户端超时配置
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(1))  // 连接超时
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("http://service-b/api"))
    .timeout(Duration.ofSeconds(3))  // 请求超时
    .build();

超时配置的黄金法则:

  • 超时时间应小于上游调用方的超时时间(避免上游先超时)
  • 连接超时 < 读取超时 < 总超时
  • 超时不宜过短(正常请求可能被误杀),也不宜过长(故障时线程占用太久)

二、流量控制:在洪峰中守住底线

2.1 为什么要做流量控制

微服务架构中,每个服务的容量是有限的。当流量超过服务容量时,如果不对流量进行控制,服务会:

  1. 响应变慢 → 线程池堆积 → 内存暴涨 → OOM(Out of Memory)
  2. 数据库连接耗尽 → 所有请求失败
  3. 级联影响上游服务

流量控制的目标是:在服务过载时,优雅地拒绝超出能力的请求,保证核心功能的可用性。

2.2 限流算法对比

固定窗口计数器

code
时间线:|--窗口1--|--窗口2--|--窗口3--|
请求数:   80         20         90
阈值:100

问题:在窗口边界可能产生流量突刺。例如,窗口 1 的最后 100ms 来了 80 个请求,窗口 2 的前 100ms 来了 80 个请求,在 200ms 内实际通过了 160 个请求,远超阈值。

滑动窗口计数器

将时间窗口细分为多个小格子,统计时滑动计算。

code
窗口大小:1s,格子数:10(每个格子 100ms)
             [0][1][2][3][4][5][6][7][8][9]
当前时间 →

每 100ms 滑动一次窗口,丢弃最老的格子,统计当前窗口内的请求总数。这比固定窗口更精确,但仍存在"格子边界"问题。

滑动窗口日志(Sliding Window Log)

记录每个请求的时间戳,统计最近 N 秒内的请求数。精度最高,但内存开销大。

漏桶(Leaky Bucket)

请求先进入漏桶,以固定速率流出。不管进入速率多快,流出速率恒定。

code
请求 → → → → [漏桶] → → →(固定速率流出)
      ↓ 溢出丢弃

特点:强制平滑输出,适用于需要严格控制输出速率的场景。

令牌桶(Token Bucket)

以固定速率生成令牌放入桶中,桶满则丢弃令牌。请求到达时消耗一个令牌,无令牌则拒绝。

code
令牌生成器 → → [令牌桶: 容量 N] → 请求消耗令牌
                     ↓ 桶满丢弃令牌

特点:允许一定的突发流量(桶内积累的令牌可一次性消费),更加灵活。Sentinel 默认使用令牌桶算法。

文章配图
文章配图

2.3 Sentinel 流量控制实战

Sentinel 是阿里巴巴开源的流量治理组件,提供了丰富的流控策略。

核心概念:

  • 资源(Resource):需要保护的代码块,可以是方法、接口等
  • 规则(Rule):对资源的流控规则
  • Entry:进入一个资源的入口,资源调用结束时退出

流控规则示例:

java
// QPS 限流:每秒最多 100 个请求
FlowRule rule = new FlowRule("getUserById")
    .setGrade(RuleConstant.FLOW_GRADE_QPS)  // 按 QPS 限流
    .setCount(100);  // QPS 阈值
FlowRuleManager.loadRules(Collections.singletonList(rule));

基于调用关系的流控策略:

java
FlowRule rule = new FlowRule("resource")
    .setStrategy(RuleConstant.STRATEGY_RELATE)  // 关联流控
    .setRefResource("writeResource");

// 当 writeResource 达到阈值时,限制 resource 的流量
// 典型场景:写操作繁忙时,限制读操作,保证写入优先

链路流控(只限制入口资源):

java
FlowRule rule = new FlowRule("resource")
    .setStrategy(RuleConstant.STRATEGY_CHAIN)
    .setRefResource("entryA");

// 只限制从 entryA 入口访问 resource 的流量

流控效果:

java
// 1. 直接拒绝(默认)
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);

// 2. 匀速排队(漏桶效果)
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)
    .setMaxQueueingTimeMs(500);  // 最长排队时间

// 3. 冷启动(预热)
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)
    .setWarmUpPeriodSec(10);  // 预热时长 10s

文章配图
文章配图

2.4 负载丢弃(Load Shedding)

当系统负载超过阈值时,主动丢弃部分请求,优先保证核心功能。

丢弃策略:

  • 随机丢弃:最简单的策略,但不区分请求重要性
  • 优先级丢弃:根据请求的优先级矩阵决定丢弃哪些请求
  • 按比例降级:丢弃非核心功能,保留核心功能

Netflix 的 Adaptive Concurrency Limit:

Netflix 使用梯度算法(Gradient Algorithm)动态调整并发限制。核心思想:通过观察系统指标(如延迟),动态调整并发上限,当延迟开始上升时减小并发限制,延迟下降时增大并发限制。

java
// Netflix concurrency-limits 库的核心算法
// 使用 Vegas 算法(类似 TCP Vegas)
// 通过 RTT 变化探测系统是否过载

long currentLimit = 100;
long rtt = measureRTT();  // 当前往返时间
long minRtt = getMinRTT(); // 历史最小 RTT

if (rtt > minRtt * 2) {
    // RTT 显著增加,减小并发限制
    currentLimit = currentLimit / 2;
} else {
    // RTT 正常,逐步增加并发限制
    currentLimit = currentLimit + 1;
}

2.5 背压(Backpressure)

背压是一种下游通知上游"我处理不过来了,慢一点"的机制。在响应式编程中,背压是核心概念。

实现方式:

  • 应用层:通过请求头传递限流信号(如 X-RateLimit-Remaining)
  • 协议层:HTTP/2 的流控、TCP 的滑动窗口、gRPC 的 flow control
  • 消息队列:消费者拉取模式天然支持背压
java
// Reactor 的背压机制
Flux.range(1, 1000)
    .onBackpressureBuffer(100)  // 缓冲 100 个元素
    .subscribe(new BaseSubscriber<Integer>() {
        @Override
        protected void hookOnSubscribe(Subscription subscription) {
            request(10);  // 初始只请求 10 个
        }
        @Override
        protected void hookOnNext(Integer value) {
            process(value);
            request(1);  // 处理完一个再请求下一个
        }
    });

三、故障演练:在可控的混乱中建立信心

3.1 混沌工程(Chaos Engineering)

混沌工程由 Netflix 在 2010 年提出,核心理念是:与其等待故障发生,不如主动注入故障,验证系统的容错能力。

混沌工程的五个原则(来自《混沌工程》一书):

  1. 建立稳态假设:定义系统正常运行的指标(如延迟、错误率、吞吐量)
  2. 多样化现实事件:模拟真实世界中可能发生的故障
  3. 在生产环境运行:预发环境无法完全模拟生产环境的行为
  4. 自动化持续运行:混沌实验应成为持续集成的一部分
  5. 最小化爆炸半径:控制实验的影响范围,避免造成大面积故障

3.2 故障注入层次

从底层到上层,故障注入可以分为多个层次:

code
┌─────────────────────────────────┐
│        应用层注入                │
│  - 方法延迟/异常                 │
│  - 返回值篡改                    │
│  - 线程池耗尽                    │
├─────────────────────────────────┤
│        网络层注入                │
│  - 网络延迟                      │
│  - 丢包                          │
│  - 带宽限制                      │
│  - DNS 故障                      │
├─────────────────────────────────┤
│        主机层注入                │
│  - CPU 高负载                    │
│  - 内存不足                      │
│  - 磁盘 I/O 故障                 │
│  - 进程 Kill                      │
├─────────────────────────────────┤
│        基础设施层注入             │
│  - 机器宕机                      │
│  - 机架断电                      │
│  - 数据中心网络分区               │
└─────────────────────────────────┘

文章配图
文章配图

3.3 混沌工程工具选型

ChaosBlade(阿里开源)

ChaosBlade 支持多层次的故障注入,使用简单:

bash
# CPU 负载注入:CPU 使用率提升到 60%
blade create cpu load --cpu-percent 60

# 网络延迟注入:eth0 网卡延迟 3s,偏移 100ms
blade create network delay --time 3000 --offset 100 --interface eth0

# 磁盘 I/O 故障注入:磁盘读负载 80%
blade create disk burn --read --path /data

# 进程 Kill:杀掉指定进程
blade create process kill --process java

# 实验结束
blade destroy <uid>

Chaos Mesh(PingCAP 开源)

Chaos Mesh 是 Kubernetes 原生的混沌工程平台,通过 CRD 定义混沌实验:

yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
  name: web-show-network-delay
spec:
  action: delay
  mode: one
  selector:
    namespaces:
      - default
    labelSelectors:
      "app": "web-show"
  delay:
    latency: "10ms"
    correlation: "100"
    jitter: "0ms"
  duration: "30s"

LitmusChaos

LitmusChaos 是 CNCF 项目,提供丰富的混沌实验 Hub,支持 Kubernetes 和非 Kubernetes 环境。

文章配图
文章配图

3.4 故障演练的完整流程

阶段一:演练计划

code
1. 确定演练目标:验证哪个系统的哪个容错能力?
2. 定义稳态指标:哪些指标正常才算系统正常?
3. 选择故障类型:注入什么故障?
4. 确定爆炸半径:影响范围多大?如何回滚?
5. 制定终止条件:什么情况下立即停止实验?

阶段二:演练执行

code
1. 通知相关人员
2. 确认监控和告警正常
3. 注入故障
4. 观察系统行为
5. 记录指标变化

阶段三:复盘与改进

code
1. 系统是否按预期容错?
2. 是否有预期外的故障传播?
3. 告警是否及时触发?
4. 恢复时间是否在 SLA 范围内?
5. 形成改进 Action Item

3.5 生产环境演练的最佳实践

Netflix 的实践经验表明,真正有效的混沌工程必须在生产环境进行。但生产环境演练需要严格的安全控制:

  1. 从小范围开始:先在单个实例上实验,再扩展到多个实例
  2. 吃饭时间演练:选择流量低谷期进行
  3. 随时可终止:实验触发自动熔断,系统可一键回滚
  4. 可观测性先行:充分监控后再开始实验
  5. 渐进式复杂度:从简单故障(CPU 高负载)到复杂故障(网络分区)

四、自愈:让系统自己修复自己

4.1 自愈的三个层次

层次一:被动自愈(Reactive)

故障发生后,系统通过健康检查发现异常,触发恢复动作。

yaml
# Kubernetes 健康检查
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3  # 连续 3 次失败后重启 Pod

层次二:主动自愈(Proactive)

系统监控指标趋势,在故障发生前采取预防措施。

java
// 内存使用率超过 80% 时主动触发 GC 并发出告警
if (memoryUsage > 0.8) {
    System.gc();  // 主动 GC
    alertManager.send("内存使用率过高,当前: " + memoryUsage);
}

层次三:智能自愈(Intelligent)

系统基于历史数据和机器学习模型,自动判断最优恢复策略。

4.2 健康检查的三种模式

code
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Liveness    │  │  Readiness   │  │  Startup     │
│  Probe       │  │  Probe       │  │  Probe       │
├──────────────┤  ├──────────────┤  ├──────────────┤
│ "我还活着吗?"│  │ "能接请求吗?"│  │ "启动完了吗?"│
│              │  │              │  │              │
│ 失败 → 重启  │  │ 失败 → 摘流  │  │ 失败 → 等待  │
│              │  │              │  │              │
│ 频繁检查      │  │ 频繁检查     │  │ 仅启动时检查  │
└──────────────┘  └──────────────┘  └──────────────┘

Liveness 和 Readiness 的关键区别:

  • Liveness 探针失败意味着进程已死或僵死,唯一选择是重启
  • Readiness 探针失败意味着进程还活着但暂时不能服务(如连接池满了、正在预热),应从负载均衡中摘除,但不重启

Liveness 探针的常见陷阱:

  • 探针依赖外部服务(如数据库)→ 外部服务故障导致所有 Pod 重启
  • 探针过重(执行复杂逻辑)→ 探针本身消耗大量资源
  • 探针过轻(只检查端口)→ 进程僵死时探针仍通过

文章配图
文章配图

4.3 自动扩缩容(Auto Scaling)

自愈的一个重要维度是自动扩缩容:当系统负载上升时自动扩容,负载下降时自动缩容。

Kubernetes HPA(Horizontal Pod Autoscaler):

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容前等待 5 分钟
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0    # 扩容立即触发
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

4.4 故障自愈的自动化流程

一个完整的自愈流程包含以下步骤:

code
1. 检测(Detect)
   ↓ 监控告警 / 健康检查失败
2. 诊断(Diagnose)
   ↓ 日志分析 / 指标关联 / 链路追踪
3. 决策(Decide)
   ↓ 从预定义策略中选择恢复操作
4. 执行(Execute)
   ↓ 重启、扩容、降级、切流、回滚
5. 验证(Verify)
   ↓ 确认服务恢复,指标恢复正常
6. 闭环(Close)
   ↓ 记录事件,更新知识库,触发改进

恢复策略矩阵:

故障类型 恢复策略 恢复时间 自动化程度
进程 Crash 重启 Pod 秒级 全自动
内存泄漏 定期重启 + 告警 分钟级 半自动
慢 SQL 自动 Kill 慢查询 + 告警 分钟级 半自动
依赖服务故障 熔断 + 降级 秒级 全自动
数据不一致 人工介入 小时级 手动
网络分区 牺牲一致性/可用性 分钟级 半自动

五、故障治理体系的全景图

将以上四个维度整合,形成完整的故障治理体系:

code
                    ┌─────────────────────────────────┐
                    │       故障治理体系全景           │
                    └─────────────────────────────────┘
                                    │
        ┌───────────┬───────────────┼───────────────┬───────────┐
        ▼           ▼               ▼               ▼           ▼
   ┌─────────┐ ┌─────────┐   ┌─────────┐     ┌─────────┐ ┌─────────┐
   │ 预防    │ │ 防护    │   │ 检测    │     │ 恢复    │ │ 改进    │
   │Prevent  │ │Protect  │   │Detect   │     │Recover  │ │Improve  │
   └────┬────┘ └────┬────┘   └────┬────┘     └────┬────┘ └────┬────┘
        │           │             │               │           │
   ·代码审查    ·熔断器       ·健康检查        ·自动重启    ·故障复盘
   ·单元测试    ·舱壁隔离     ·指标监控        ·自动扩缩容  ·混沌实验
   ·集成测试    ·限流降级     ·链路追踪        ·降级切换    ·预案更新
   ·容量规划    ·超时控制     ·日志聚合        ·数据修复    ·架构优化
   ·架构评审    ·幂等设计     ·告警规则        ·流量调度    ·SOP 优化

文章配图
文章配图

5.1 技术选型参考

能力 开源方案 云厂商方案
熔断/限流 Sentinel, Resilience4j AWS Service Quotas, GCP Quota
服务网格 Istio, Linkerd AWS App Mesh, ASM
混沌工程 ChaosBlade, Chaos Mesh, Litmus AWS FIS, GCP Chaos
可观测性 Prometheus + Grafana, SkyWalking CloudWatch, ARMS
自动扩缩 K8s HPA/VPA, KEDA AWS Auto Scaling, AS

六、面试官视角提问

以下问题从面试官视角出发,考察候选人对故障治理的深度理解和实践经验。每题附参考答案。


Q1:你在项目中遇到过最严重的级联故障是什么?根因是什么?你是如何"止血"和"根治"的?

考察点:故障排查能力、应急响应经验、系统性思维。

参考答案:

面试官期待的答案应当包含三个层次:

故障场景:以典型的"线程池耗尽 → 级联超时"为例。某次大促期间,订单服务调用库存服务的 RT 从 50ms 飙升至 5s,原因是库存服务依赖的 Redis 集群发生主从切换。订单服务的 Tomcat 线程池(200 线程)在 3 秒内被全部占满,进而导致网关层超时,用户端大量报错。

止血方案(1-3 分钟内):

  1. 立即摘除库存服务的故障节点,切流到备集群
  2. 对订单服务的 /order/create 接口做手动限流,降至正常 QPS 的 50%
  3. 重启订单服务实例,释放堆积的线程池

根治方案(事后):

  1. 为订单服务接入熔断器(Sentinel),当库存服务错误率 > 50% 时自动熔断
  2. 设置合理的超时时间(连接超时 1s + 读取超时 3s),避免线程无限期等待
  3. 引入舱壁隔离,为库存服务调用分配独立线程池(20 线程),防止其耗尽主线程池
  4. Redis 集群增加哨兵监控,缩短故障转移时间

Q2:一个服务调用链为 A → B → C,B 配置了 3 次重试(指数退避,初始间隔 100ms),C 配置了 2 次重试。如果 C 有 10% 概率超时,A 的一个请求在最坏情况下会产生多少次对 C 的调用?这种重试放大效应如何治理?

考察点:重试风暴的定量分析,以及系统化治理思路。

参考答案:

定量分析:A 发 1 次请求 → B 收到 1 次。B 调用 C 时,如果 C 超时,B 最多重试 3 次,即 B 最多调用 C 共 4 次(1 次原始 + 3 次重试)。C 每次收到请求后如果内部超时,C 最多重试 2 次,即每个到达 C 的请求最多产生 3 次内部调用。因此最坏情况:1 × 4 × 3 = 12 次。如果链路上还有更多跳,放大效应呈指数增长。

治理方案:

  1. 重试上限:全链路总重试次数不超过 3 次,通过请求头传递 x-retry-count,每跳递减
  2. 退避 + 抖动:指数退避(100ms → 200ms → 400ms)+ 随机抖动(±50ms),避免惊群
  3. 熔断感知:重试前检查熔断器状态,已熔断则直接失败不重试
  4. 幂等保证:所有重试请求携带幂等键(如 idempotency-key),下游通过去重表保证只执行一次
  5. 超时总和约束:每个请求携带 x-deadline-ms(截止时间),链路上每跳的超时时间之和不超过截止时间

Q3:Sentinel 的滑动窗口和固定窗口计数器在限流精度上有什么区别?如果在 1 秒窗口的前 100ms 来了 100 个请求,后 900ms 没有请求,固定窗口和滑动窗口分别会怎样处理窗口边界处的请求?

考察点:限流算法的深入理解,以及能否识别边界条件问题。

参考答案:

固定窗口问题:假设限流阈值 100 QPS。窗口 1(0s-1s)的前 100ms 来了 100 个请求,全部通过。窗口 1 的后 900ms 没有请求。窗口 2(1s-2s)的前 100ms 又来了 100 个请求,全部通过。在 200ms 的时间窗口内(窗口 1 末尾 100ms + 窗口 2 开头 100ms),实际通过了 200 个请求——是阈值的 2 倍。这就是"临界突刺"问题。

滑动窗口改进:Sentinel 将 1 秒窗口划分为 N 个格子(默认 2 个,可通过 sampleCount 配置)。每个格子独立计数,窗口滑动时丢弃最老的格子,用当前窗口内所有格子的计数之和判断是否限流。当窗口 2 的请求到来时,窗口 1 前 100ms 的请求计数已被丢弃,当前窗口统计的是窗口 1 后 900ms(0 请求)+ 窗口 2 前 100ms(100 请求)= 100,不超过阈值,不会误限。

滑动窗口的局限性:格子数是固定的,如果请求恰好集中在格子边界,仍可能出现微小的精度偏差。但通过增加格子数(如 10 个格子,每个 100ms),可以将偏差控制在可接受范围内。

画图说明(面试时建议在白板上画出时间轴):

code
固定窗口:|==窗口1(100通过)==|==窗口2(100通过)==|  → 200ms内通过200,超限
滑动窗口:|格1(100)|格2(0)|  → 滑动后:|格2(0)|格3(100)|  → 100,不超限

Q4:你如何设计一个生产环境的混沌实验?从计划到执行到复盘,每一步需要考虑什么?爆炸半径如何控制?

考察点:混沌工程的实践经验和方法论。

参考答案:

实验计划(最重要的阶段):

  1. 明确假设:例如"订单服务在库存服务延迟 3s 时,应在 500ms 内返回降级响应"
  2. 定义稳态指标:业务指标(下单成功率 > 99.9%)+ 技术指标(P99 延迟 < 200ms,错误率 < 0.1%)
  3. 选择故障类型:网络延迟注入(对库存服务的 50% 流量注入 3s 延迟)
  4. 控制爆炸半径:
    • 范围:仅影响 1 个可用区的 2 个 Pod(共 10 个 Pod)
    • 流量:通过服务网格给 20% 流量染色,只对染色流量注入故障
    • 时间:选择业务低峰期(凌晨 3 点),实验时长 5 分钟
  5. 制定终止条件:下单成功率 < 95% 持续 30 秒,或 P99 延迟 > 1s,立即终止实验

执行过程:

  1. 提前 30 分钟通知值班人员和相关团队
  2. 确认监控大盘、告警规则正常
  3. 通过 ChaosBlade 注入故障:blade create network delay --time 3000 --interface eth0 --percent 50
  4. 实时观察稳态指标变化
  5. 实验结束后执行 blade destroy 清理

复盘输出:

  1. 系统是否按预期容错?(熔断器是否在 2s 内触发?降级逻辑是否生效?)
  2. 是否有预期外的故障传播?(是否影响了其他服务?)
  3. 告警是否及时?(从故障注入到告警触发的延迟是多少?)
  4. 恢复时间是否达标?(故障停止后,服务多久恢复正常?)
  5. 形成 Action Item:如"熔断器阈值需要从 50% 调整为 30%""降级响应需要增加缓存兜底"

Q5:Kubernetes 的 Liveness Probe 和 Readiness Probe 有什么区别?如果一个服务的 Liveness Probe 检查了数据库连接,会有什么问题?请给出正确的探针设计。

考察点:对健康检查模式的深入理解,以及能否识别反模式。

参考答案:

核心区别:

维度 Liveness Probe Readiness Probe
语义 "进程还活着吗?" "能接请求吗?"
失败动作 重启 Pod(kubelet 杀进程) 从 Service Endpoint 摘除
检查内容 死锁、无限循环、进程僵死 依赖就绪、连接池可用、预热完成
频率 应该较低 可以较高
恢复方式 重启后重建所有状态 重新加入 Endpoint 即可

Liveness 检查数据库连接的问题(经典反模式):

code
数据库短暂故障(如主从切换 5s)
  → 所有 Pod 的 Liveness 探针连接数据库失败
  → Kubelet 杀掉所有 Pod 并重启
  → 新 Pod 启动时也需要连接数据库来初始化
  → 数据库压力更大,恢复更慢
  → 探针继续失败,Pod 循环重启
  → 服务彻底不可用,远超原始故障影响

正确设计:

  • Liveness Probe:只检查进程本身——如 /health/liveness 端点仅返回 200(不访问任何外部依赖)。如果进程僵死(如死锁导致 HTTP 端口不响应),探针自然失败。
  • Readiness Probe:可以检查数据库连接池可用性、Redis 连接状态等。失败时摘除流量但不重启,给依赖服务恢复的时间窗口。
  • 为 Liveness 设置更长的 initialDelaySeconds(如 60s)和 failureThreshold(如 5 次),避免启动阶段误杀。

Q6:在微服务架构中,重试、超时、熔断、限流、舱壁这些机制应该如何协同工作?它们之间有依赖关系吗?请画出它们的协作顺序,并说明每个机制的职责边界。

考察点:系统化思维,能否理解各机制之间的协作关系。

参考答案:

协作顺序(一次请求的完整经过):

code
请求到达
  ↓
[1. 限流] —— 是否超过系统容量?超过 → 快速拒绝(429 Too Many Requests)
  ↓ 通过
[2. 舱壁] —— 是否有可用的隔离资源?无 → 拒绝(信号量/线程池满)
  ↓ 获取资源
[3. 熔断检查] —— 下游是否已熔断?是 → 快速失败(CircuitBreakerOpenException)
  ↓ 未熔断
[4. 执行调用] —— 发起 HTTP/RPC 调用
  ↓
[5. 超时控制] —— 是否在 deadline 内返回?超时 → TimeoutException
  ↓ 成功
[6. 返回结果]
  ↓ 失败(超时/异常)
[7. 重试判断] —— 是否可重试?(幂等?次数未超?熔断未开?)
  ↓ 是 → 回到步骤 4(带退避+抖动)
  ↓ 否
[8. 熔断记录] —— 本次失败计入熔断器统计
  ↓
[9. 降级返回] —— 返回兜底结果或抛出异常

职责边界:

  • 限流:保护自身,控制入口流量不超过系统容量
  • 舱壁:资源隔离,防止一个下游的问题耗尽所有资源
  • 熔断:快速失败,防止反复调用已故障的下游
  • 超时:线程保护,防止无限期等待
  • 重试:提高成功率,但需幂等+退避+熔断感知

协同关键点:

  1. 重试前必须检查熔断器状态,熔断期间不重试
  2. 重试的超时时间要小于原始超时,且总超时不超过上游 deadline
  3. 限流应分层:网关层全局限流 + 服务层接口限流 + 方法级热点限流
  4. 熔断和舱壁可以组合使用:同一服务的不同接口用不同舱壁,每个舱壁有独立熔断器

总结

微服务故障治理不是单一的"熔断"或"限流",而是一套贯穿全生命周期的系统工程:

  1. 级联故障是分布式系统的固有风险,需要熔断器、舱壁隔离、理性重试、超时控制等多层防护
  2. 流量控制是服务过载时的最后防线,从固定窗口到滑动窗口到令牌桶,算法选择决定精度和性能
  3. 故障演练是把"未知的未知"变成"已知的已知"的手段,混沌工程不是"搞破坏",而是"建立信心"
  4. 自愈能力是系统成熟度的标志,从被动重启到主动预防到智能决策,自动化程度越高,运维负担越低

记住:故障一定会发生,区别在于你是有备而来还是措手不及。


下一篇预告:191|架构质量属性:可用性、性能、扩展性、安全性、可维护性和成本