概述

凌晨三点,手机狂震。打开一看,告警群已经炸了——支付服务 P99 延迟从 50ms 飙到 12 秒,错误率突破 80%。更要命的是,故障像多米诺骨牌一样连锁反应:用户服务超时 → 鉴权失败 → 库存服务重试风暴 → 消息队列阻塞 → 支付回调全部超时。监控大屏上三十秒内全部变红。

这不是某家公司独有的惨案。微服务拆得越细,服务间调用链越长,一个局部故障就越容易演变成全局雪崩。我曾在某新能源物流平台治理 50+ 微服务时,亲历过三次类似事故,最后一次最严重——一个下游搜索服务的数据库连接池打满,导致上游所有依赖搜索结果的服务线程池连锁耗尽,整条交易链路瘫痪 17 分钟。

那次事故后我们花了两周时间重做限流熔断降级体系。最终把 P99 从 200ms 压到 170ms,更重要的是,之后再没出现过级联故障。这篇文章把我踩过的坑、做过的架构选型、以及生产环境真正有效的配置经验一次讲透。

先说清楚三者的关系——很多人混着用,但它们解决的问题不同:

防护手段解决什么问题类比作用位置
限流(Rate Limiting)流量超过系统承载能力超市收银台只开 3 个窗口,排满了就不让进了入口处
熔断(Circuit Breaking)下游服务故障,防止故障蔓延保险丝,电流过大自动断电调用链中间
降级(Degradation)核心功能不可用时提供替代方案停电了用应急灯,虽然不亮但能看见业务逻辑层

三者配合使用:限流挡在前面控制输入,熔断在中间切断故障传播,降级在尾部保证用户体验不至于完全崩溃。

雪崩效应:为什么微服务比单体更容易崩

单体应用里,方法调用是进程内函数调用,微秒级完成。微服务拆开后,每次调用都变成了网络请求——网络可能抖动、DNS 可能解析慢、TCP 三次握手有开销、序列化反序列化要时间。一个用户请求从网关进来,经过 5-7 层服务调用是常态。

故障传播路径

看一个真实的调用链:

用户请求 → API Gateway → 订单服务 → 用户服务(鉴权)
                                  → 库存服务(扣减)
                                  → 优惠券服务
                                  → 支付服务 → 第三方支付网关

假设第三方支付网关变慢了(比如响应从 100ms 变成 5 秒),会发生什么?

  1. 支付服务的线程被占用,等待第三方返回
  2. 支付服务线程池逐渐耗尽,新请求排队
  3. 订单服务调用支付服务超时,开始重试
  4. 重试请求叠加到原本就满负荷的支付服务上,雪上加霜
  5. 订单服务自己的线程池也被拖垮
  6. 库存服务、优惠券服务全部受影响
  7. API Gateway 线程池耗尽,整个系统拒绝服务

关键数据:如果订单服务有 200 个工作线程,每个请求都要调支付服务,支付服务变慢到 5 秒响应一次。那么 200 个线程在 5 秒内全部被占满,之后所有新请求直接排队或拒绝。订单服务本身没有任何代码 bug,但它被下游拖死了。

三种典型故障模式

故障模式触发条件表现危险等级
线程池耗尽下游慢响应 + 无超时服务完全无响应
连接池打满下游连接不释放新请求无法建立连接
重试风暴超时后自动重试放大流量 N 倍极高
级联雪崩多层依赖无隔离全链路瘫痪极高

重试风暴最容易被忽视。假设 A 调 B,超时 3 秒,重试 3 次。B 变慢后,A 每个原始请求变成 3 次请求打到 B 上。如果 B 上面还有 C,C 调 B 也重试 3 次——最终 C 的一个请求放大成 9 次打到 B 上。流量放大效应极其可怕。这也是为什么我强烈建议:重试和熔断必须配合使用,熔断打开时不要重试。

限流:把流量关在笼子里

限流算法选型

四种主流限流算法,各有适用场景:

算法原理优点缺点适用场景
计数器法固定窗口内计数实现简单窗口边界突刺低精度场景
滑动窗口细分窗口平滑计数精度高实现稍复杂通用场景
漏桶请求匀速处理流量绝对平滑无法应对突发保护下游弱服务
令牌桶按速率发放令牌允许突发流量需调参API 网关入口

漏桶和令牌桶的区别最容易被搞混。打个比方:

  • 漏桶:水龙头一直放水,桶底有个小孔匀速漏水。水来得太快就溢出丢弃。不管你多急,出水速度永远一样。
  • 令牌桶:系统以固定速率往桶里放令牌,请求来了先拿令牌,拿不到就等。桶里可以攒令牌,所以短时间突发流量可以处理。

我推荐:API 网关入口用令牌桶(允许突发),保护下游弱服务用漏桶(绝对平滑)。实际生产中大部分场景用令牌桶就够了,漏桶用在不允许任何突发的场景(比如写数据库)。

Go 实现令牌桶限流器

下面是一个可以直接用在生产环境的 Go 令牌桶实现。我把它用在自己维护的运维 API 网关上,日均处理几百万请求没问题:

package ratelimit

import (
	"context"
	"sync"
	"time"
)

// TokenBucket 令牌桶限流器
type TokenBucket struct {
	mu         sync.Mutex
	rate       float64   // 令牌发放速率(个/秒)
	burst      float64   // 桶容量(最大令牌数)
	tokens     float64   // 当前令牌数
	lastUpdate time.Time // 上次补充令牌的时间
}

// NewTokenBucket 创建限流器
// rate: 每秒发放多少令牌
// burst: 桶最多存多少令牌(允许的突发量)
func NewTokenBucket(rate, burst float64) *TokenBucket {
	return &TokenBucket{
		rate:       rate,
		burst:      burst,
		tokens:     burst, // 初始满桶
		lastUpdate: time.Now(),
	}
}

// Allow 尝试获取1个令牌,非阻塞
func (tb *TokenBucket) Allow() bool {
	return tb.AllowN(1)
}

// AllowN 尝试获取n个令牌,非阻塞
func (tb *TokenBucket) AllowN(n float64) bool {
	tb.mu.Lock()
	defer tb.mu.Unlock()

	now := time.Now()
	// 计算距上次过了多久,补充令牌
	elapsed := now.Sub(tb.lastUpdate).Seconds()
	tb.tokens += elapsed * tb.rate
	// 令牌不能超过桶容量
	if tb.tokens > tb.burst {
		tb.tokens = tb.burst
	}
	tb.lastUpdate = now

	// 令牌够不够
	if tb.tokens >= n {
		tb.tokens -= n
		return true
	}
	return false
}

// Wait 阻塞等待直到获取令牌或超时
func (tb *TokenBucket) Wait(ctx context.Context) error {
	for {
		if tb.Allow() {
			return nil
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(10 * time.Millisecond):
			// 10ms 重试一次,避免忙等
		}
	}
}

这个实现的关键点:

  1. 惰性补充令牌:不是开个 goroutine 定时往桶里放令牌,而是每次请求时根据时间差计算应补充的令牌数。这样做避免了额外的 goroutine 开销,在高并发下更稳定。
  2. 初始满桶:服务刚启动时桶是满的,可以应对启动瞬间的突发流量。
  3. 互斥锁保护sync.Mutex 保证并发安全。如果追求更高性能,可以换成 atomic 操作,但代码复杂度会上升。

分布式限流:单机不够用怎么办

单机限流只能保护单个实例。如果你的服务有 10 个副本,每个限流 1000 QPS,理论上整个服务能承受 10000 QPS。但如果网关层不做全局限流,10000 QPS 可能被某一个实例的不均匀分配打垮。

分布式限流的核心思路:把限流计数放到 Redis 里,所有实例共享一个计数器。

package ratelimit

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

// RedisRateLimiter 基于 Redis 的分布式限流器
// 使用滑动窗口算法
type RedisRateLimiter struct {
	client *redis.Client
	key    string        // 限流 key,如 "rate_limit:order_service"
	rate   int           // 窗口内允许的最大请求数
	window time.Duration // 窗口大小
}

func NewRedisRateLimiter(client *redis.Client, key string, rate int, window time.Duration) *RedisRateLimiter {
	return &RedisRateLimiter{
		client: client,
		key:    key,
		rate:   rate,
		window: window,
	}
}

// Allow 使用 Redis Lua 脚本保证原子性
func (r *RedisRateLimiter) Allow(ctx context.Context) (bool, error) {
	// Lua 脚本:原子性地移除窗口外数据 + 计数 + 判断
	script := `
		local key = KEYS[1]
		local now = tonumber(ARGV[1])
		local window = tonumber(ARGV[2])
		local rate = tonumber(ARGV[3])

		-- 移除窗口外的旧记录
		redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

		-- 获取当前窗口内请求数
		local count = redis.call('ZCARD', key)

		if count < rate then
			-- 未超限,记录当前请求
			redis.call('ZADD', key, now, now .. ':' .. math.random())
			redis.call('PEXPIRE', key, window)
			return 1
		else
			return 0
		end
	`

	now := time.Now().UnixMilli()
	result, err := r.client.Eval(ctx, script, []string{r.key},
		now, r.window.Milliseconds(), r.rate).Int()
	if err != nil {
		return false, fmt.Errorf("redis eval failed: %w", err)
	}
	return result == 1, nil
}

生产环境注意事项:

  1. Redis 不可用时的降级策略:如果 Redis 挂了,限流是直接放行还是拒绝?我推荐放行 + 降级到单机限流。限流是为了保护系统,但如果限流依赖的 Redis 挂了导致所有请求被拒,那比不限流还危险。
  2. Lua 脚本是必须的:如果不用 Lua 脚本,先 ZCARD 再 ZADD 两步操作之间可能有并发问题。Lua 脚本在 Redis 中是原子执行的。
  3. Sorted Set 内存开销:每个请求在 ZSet 里存一条记录,高 QPS 下内存增长快。如果 QPS 超过 10 万,建议换成 Redis 的 INCR + EXPIRE 方案,精度略低但内存占用小得多。

Sentinel 限流实战

阿里的 Sentinel 是 Java 生态最流行的流量控制组件。相比手写限流器,Sentinel 的优势在于自带控制台、支持热点参数限流、可以动态调整规则。

// Sentinel 限流规则配置
FlowRule rule = new FlowRule();
rule.setResource("orderService.createOrder");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);  // QPS 模式
rule.setCount(1000);                           // 每秒最多 1000 次
rule.setLimitApp("default");                    // 对所有来源生效

// 热点参数限流:按用户ID限流,单用户每秒最多10次
ParamFlowRule paramRule = new ParamFlowRule("orderService.createOrder")
    .setParamIdx(0)        // 第0个参数作为限流维度
    .setGrade(RuleConstant.FLOW_GRADE_QPS)
    .setCount(10);          // 单用户 QPS 上限
// 特殊用户(VIP)放宽到 100
paramRule.setParamItem(new ParamFlowItem()
    .setObject("user_vip_123")
    .setClassType(String.class.getName())
    .setCount(100));

// 加载规则
FlowRuleManager.loadRules(Collections.singletonList(rule));
ParamFlowRuleManager.loadRules(Collections.singletonList(paramRule));

// 业务代码中使用
@SentinelResource(value = "orderService.createOrder",
    blockHandler = "createOrderBlockHandler")
public Order createOrder(String userId, OrderRequest req) {
    // 正常业务逻辑
    return orderRepository.save(req);
}

// 限流后的降级处理
public Order createOrderBlockHandler(String userId, OrderRequest req,
    BlockException ex) {
    // 返回默认订单或抛出友好提示
    throw new ServiceException("系统繁忙,请稍后重试", 429);
}

热点参数限流是 Sentinel 的杀手锏功能。普通限流只能按资源限流(“createOrder 接口每秒 1000 次”),热点参数可以按用户限流(“用户 A 每秒最多 10 次,VIP 用户放宽到 100 次”)。在做多租户 SaaS 时特别有用。

熔断:给故障装上保险丝

熔断器状态机

熔断器有三个状态,构成一个有限状态机:

              失败率超过阈值
  ┌─────────┐ ──────────────→ ┌─────────┐
  │ CLOSED  │                │  OPEN   │
  │ (正常)   │ ←───────────── │ (熔断)   │
  └─────────┘  探测成功        └─────────┘
       ↑                        │
       │                        │ 等待冷却时间结束
       │                        ↓
       │                   ┌──────────┐
       └────────────────── │HALF_OPEN │
           探测成功          │ (半开)    │
                           └──────────┘
                              │ 探测失败
                           ┌─────────┐
                           │  OPEN   │
                           └─────────┘

三个状态的含义:

  • CLOSED(闭合):正常状态,请求正常通过。熔断器在后台统计请求成功率和慢调用比例。
  • OPEN(断开):熔断状态,所有请求直接拒绝,不调用下游服务。执行降级逻辑。等冷却时间结束后进入半开状态。
  • HALF_OPEN(半开):试探状态,放行少量请求探测下游是否恢复。成功则回到 CLOSED,失败则回到 OPEN。

Resilience4j 熔断配置

Resilience4j 是 Hystrix 的继任者(Hystrix 已停止维护)。模块化设计,你只需要引入用得到的模块:

<!-- pom.xml 只引入需要的模块 -->
<dependencies>
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-circuitbreaker</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-ratelimiter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-bulkhead</artifactId>
        <version>2.2.0</version>
    </dependency>
</dependencies>

生产级熔断配置:

# application.yml
resilience4j:
  circuitbreaker:
    configs:
      default:
        # 滑动窗口配置
        slidingWindowType: COUNT_BASED       # 基于请求数统计
        slidingWindowSize: 100               # 最近 100 次请求
        minimumNumberOfCalls: 20            # 至少 20 次请求才开始计算
        # 失败判定
        failureRateThreshold: 50             # 失败率 >= 50% 触发熔断
        slowCallRateThreshold: 60            # 慢调用比例 >= 60% 触发熔断
        slowCallDurationThreshold: 2s        # 超过 2 秒算慢调用
        # 状态转换
        waitDurationInOpenState: 30s         # 熔断后等 30 秒再半开
        permittedNumberOfCallsInHalfOpenState: 10  # 半开状态放行 10 个探测请求
        # 哪些异常算失败
        recordExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
          - feign.FeignException.GatewayTimeout
        ignoreExceptions:
          - com.example.BusinessException    # 业务异常不计入熔断
    instances:
      paymentService:
        baseConfig: default
        # 支付服务单独调低阈值,因为支付链路更敏感
        failureRateThreshold: 30
        slowCallDurationThreshold: 1s
      userService:
        baseConfig: default
        # 用户服务允许更高的失败率,因为可以做降级
        failureRateThreshold: 60

这些参数怎么调?我给一套经验值,基于治理 50+ 微服务的实战总结:

参数推荐值(核心服务)推荐值(非核心服务)调参依据
slidingWindowSize10060太小统计样本不足,太大反应慢
failureRateThreshold30-50%50-70%核心服务更敏感
slowCallDurationThreshold1-2s3-5s对应下游 P99 的 2-3 倍
waitDurationInOpenState30-60s15-30s等下游恢复
halfOpen 探测数105不宜太多

踩坑经验minimumNumberOfCalls 这个参数很多人忽略。如果设为 0,服务刚启动还没几个请求,只要有一个失败,失败率就是 100%,立刻触发熔断。设为 20 表示至少要有 20 次请求才开始统计,避免小样本误判。

Go 实现熔断器

如果你的服务是 Go 写的(比如我们整个运维平台都是 Go),可以用 sony/gobreaker 或自己实现。下面是一个精简但可用的熔断器实现:

package circuitbreaker

import (
	"errors"
	"sync"
	"time"
)

// State 熔断器状态
type State int

const (
	StateClosed   State = iota // 正常
	StateOpen                  // 熔断
	StateHalfOpen              // 半开
)

// Settings 熔断器配置
type Settings struct {
	Name          string
	MaxRequests   uint32        // 半开状态允许的探测请求数
	Interval      time.Duration // Closed 状态下的统计周期
	Timeout       time.Duration // Open 状态的冷却时间
	ReadyToTrip   func(counts Counts) bool // 判断是否触发熔断
	OnStateChange func(name string, from, to State)
}

// Counts 请求计数
type Counts struct {
	Requests            uint32
	TotalSuccesses      uint32
	TotalFailures       uint32
	ConsecutiveFailures uint32
}

// CircuitBreaker 熔断器
type CircuitBreaker struct {
	mu          sync.Mutex
	settings    Settings
	state       State
	generation  uint64
	expiry      time.Time
	counts      Counts
}

// NewCircuitBreaker 创建熔断器
func NewCircuitBreaker(s Settings) *CircuitBreaker {
	if s.ReadyToTrip == nil {
		// 默认策略:连续失败 5 次触发熔断
		s.ReadyToTrip = func(c Counts) bool {
			return c.ConsecutiveFailures >= 5
		}
	}
	cb := &CircuitBreaker{settings: s}
	cb.toNewGeneration(time.Now())
	return cb
}

// Execute 执行请求,自动处理熔断逻辑
func (cb *CircuitBreaker) Execute(req func() error) error {
	generation, err := cb.beforeRequest()
	if err != nil {
		return err
	}

	err = req()

	cb.afterRequest(generation, err)
	return err
}

func (cb *CircuitBreaker) beforeRequest() (uint64, error) {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	now := time.Now()
	state, generation := cb.currentState(now)

	if state == StateOpen {
		// 熔断中,直接返回错误,不执行请求
		return generation, ErrCircuitOpen
	}
	return generation, nil
}

func (cb *CircuitBreaker) afterRequest(before uint64, err error) {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	now := time.Now()
	state, generation := cb.currentState(now)
	if before != generation {
		// 状态已经变了,丢弃这次计数
		return
	}

	cb.counts.Requests++
	if err != nil {
		cb.counts.TotalFailures++
		cb.counts.ConsecutiveFailures++
	} else {
		cb.counts.TotalSuccesses++
		cb.counts.ConsecutiveFailures = 0
	}

	// 检查是否需要切换状态
	if cb.state == StateClosed && cb.settings.ReadyToTrip(cb.counts) {
		cb.setState(StateOpen, now)
	} else if cb.state == StateHalfOpen && cb.counts.Requests >= cb.settings.MaxRequests {
		// 半开状态下探测请求已用完,根据结果决定状态
		if cb.counts.TotalSuccesses > 0 {
			cb.setState(StateClosed, now)
		} else {
			cb.setState(StateOpen, now)
		}
	}
}

var ErrCircuitOpen = errors.New("circuit breaker is open")

func (cb *CircuitBreaker) currentState(now time.Time) (State, uint64) {
	switch cb.state {
	case StateClosed:
		if !cb.expiry.IsZero() && now.After(cb.expiry) {
			cb.toNewGeneration(now)
		}
	case StateOpen:
		if now.After(cb.expiry) {
			// 冷却结束,进入半开
			cb.setState(StateHalfOpen, now)
		}
	}
	return cb.state, cb.generation
}

func (cb *CircuitBreaker) setState(state State, now time.Time) {
	if cb.state == state {
		return
	}
	prev := cb.state
	cb.state = state
	cb.toNewGeneration(now)
	if cb.settings.OnStateChange != nil {
		cb.settings.OnStateChange(cb.settings.Name, prev, state)
	}
}

func (cb *CircuitBreaker) toNewGeneration(now time.Time) {
	cb.generation++
	cb.counts = Counts{}

	switch cb.state {
	case StateClosed:
		if cb.settings.Interval == 0 {
			cb.expiry = time.Time{} // 不过期
		} else {
			cb.expiry = now.Add(cb.settings.Interval)
		}
	case StateOpen:
		cb.expiry = now.Add(cb.settings.Timeout)
	case StateHalfOpen:
		cb.expiry = time.Time{} // 不过期,等探测请求用完
	}
}

使用方式:

cb := circuitbreaker.NewCircuitBreaker(circuitbreaker.Settings{
    Name:        "payment-service",
    MaxRequests: 5,                    // 半开时放行 5 个探测
    Interval:    60 * time.Second,     // Closed 统计周期 60 秒
    Timeout:     30 * time.Second,     // Open 冷却 30 秒
    ReadyToTrip: func(c circuitbreaker.Counts) bool {
        // 失败率超过 50% 且至少 10 次请求才触发
        if c.Requests < 10 {
            return false
        }
        failureRate := float64(c.TotalFailures) / float64(c.Requests)
        return failureRate >= 0.5
    },
    OnStateChange: func(name string, from, to circuitbreaker.State) {
        log.Printf("[CircuitBreaker] %s: %v → %v", name, from, to)
        // 状态变化时推送告警
        alert.Send(fmt.Sprintf("熔断器 %s 从 %v 切换到 %v", name, from, to))
    },
})

// 在业务代码中使用
err := cb.Execute(func() error {
    return paymentClient.Charge(ctx, order)
})
if errors.Is(err, circuitbreaker.ErrCircuitOpen) {
    // 熔断已打开,执行降级逻辑
    return fallbackPayment(ctx, order)
}

Envoy 代理层熔断

如果你用了 Service Mesh(比如 Istio),熔断可以在网格层用 Envoy 配置,不需要改业务代码。这也是我推荐的方式——业务代码不侵入,治理逻辑下沉到基础设施层。

# Istio DestinationRule 配置 Envoy 熔断
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: payment-service-cb
spec:
  host: payment-service.default.svc.cluster.local
  trafficPolicy:
    outlierDetection:
      # 连续 5xx 错误数达到 5 触发驱逐
      consecutive5xxErrors: 5
      # 连续网关错误(502/503/504)达到 3 触发驱逐
      consecutiveGatewayErrors: 3
      # 驱逐时间间隔(最少驱逐多久)
      interval: 30s
      # 驱逐时长
      baseEjectionTime: 30s
      # 最大驱逐比例:最多驱逐 50% 的实例
      maxEjectionPercent: 50
      # 最小健康比例:至少保持 50% 实例可用
      minHealthPercent: 50
    connectionPool:
      tcp:
        maxConnections: 100        # 最大连接数
        connectTimeout: 2s         # 连接超时
      http:
        http1MaxPendingRequests: 50    # 最大等待中的请求
        http2MaxRequests: 200          # 最大并发请求
        maxRequestsPerConnection: 10   # 每个连接最大请求数
        maxRetries: 2                  # 最大重试次数

Envoy 的熔断和 Resilience4j 的熔断有什么区别?

维度Envoy(Service Mesh)Resilience4j(SDK)
侵入性零侵入,配置即生效需要改代码加注解
粒度实例级(驱逐不健康实例)请求级(按资源熔断)
语言无关仅 Java
动态调整支持(K8s CRD 热更新)支持但需配置中心
适用场景多语言微服务单语言 Java 生态

我的建议:如果微服务超过 3 种语言,用 Service Mesh 层熔断。如果全是 Java,Resilience4j 更灵活。如果全是 Go,用 gobreaker 或自己实现。(参见 相关文章:Istio Service Mesh 入门

降级:让系统优雅地变差

降级是最后一道防线。限流和熔断都是在"拒绝请求",降级是"提供替代方案"。一个好的降级策略,能让用户几乎感知不到系统出了问题。

降级策略分类

降级类型触发条件处理方式示例
返回默认值下游不可用返回预设默认响应推荐列表不可用,返回热门商品
返回缓存数据源不可用返回旧数据商品详情读缓存,容忍数据过期
简化流程核心链路受影响跳过非核心步骤下单时跳过优惠券校验
异步化同步调用超时转为异步处理支付转异步,先返回处理中
功能关闭非核心功能故障直接禁用功能评论功能挂了,隐藏评论区

设计降级策略的原则

  1. 区分核心链路和非核心链路。下单支付是核心,推荐评论是非核心。非核心可以随时降级,核心链路要尽力保护。
  2. 降级方案必须提前测试。别等故障来了才发现降级代码有 bug。混沌工程演练要包含降级验证。
  3. 降级要有开关。通过配置中心动态切换,不要写死在代码里。出问题时一条命令降级,恢复时一条命令切回。
  4. 降级不等于错误。降级返回的应该是"可用的次优结果",不是 500 错误码。

降级实现示例

Go 中实现降级链:

// PaymentService 支付服务,包含降级链
type PaymentService struct {
	primaryPayment  PaymentClient   // 主支付通道
	fallbackPayment PaymentClient   // 备用支付通道
	cache           CacheClient     // 缓存
	cb              *circuitbreaker.CircuitBreaker // 熔断器
}

// Charge 支付,包含三级降级策略
func (s *PaymentService) Charge(ctx context.Context, order *Order) (*PaymentResult, error) {
	// 策略1:主通道正常 → 正常支付
	result, err := s.cb.Execute(func() error {
		result, err := s.primaryPayment.Charge(ctx, order)
		if err == nil {
			order.Result = result
		}
		return err
	})

	if err == nil {
		return result, nil
	}

	// 策略2:主通道熔断 → 切换备用通道
	if errors.Is(err, circuitbreaker.ErrCircuitOpen) {
		log.Printf("primary payment circuit open, switching to fallback")
		result, err := s.fallbackPayment.Charge(ctx, order)
		if err == nil {
			return result, nil
		}
	}

	// 策略3:两个通道都不行 → 异步处理
	// 先返回"处理中",把请求丢到队列,后台重试
	if order.Amount < 10000 { // 小额订单可以异步
		if err := s.enqueueAsyncPayment(order); err != nil {
			return nil, fmt.Errorf("payment unavailable, please retry later")
		}
		return &PaymentResult{
			Status:  "processing",
			Message: "支付处理中,请稍后查看结果",
		}, nil
	}

	// 大额订单不允许异步,直接拒绝
	return nil, fmt.Errorf("payment service unavailable")
}

这段代码体现了三级降级:主通道 → 备用通道 → 异步处理。注意大额订单(超过 1 万)不允许走异步路径——因为异步支付如果最终失败,退款流程复杂,金额大时风险不可控。这是我做支付系统时的一个实际决策。

配置化降级开关

生产环境最重要的能力是"一键降级"。通过配置中心(比如 Nacos、Apollo)管理降级开关:

// 从配置中心读取降级配置
type DegradeConfig struct {
	// 功能开关
	EnableRecommendation bool `json:"enable_recommendation"` // 推荐功能
	EnableComments       bool `json:"enable_comments"`       // 评论功能
	EnableCoupons        bool `json:"enable_coupons"`        // 优惠券功能
	// 降级策略
	RecommendationDegrade string `json:"recommendation_degrade"` // "default" | "cache" | "disabled"
	// 限流阈值(可动态调整)
	OrderRateLimit int `json:"order_rate_limit"` // 下单 QPS 限制
}

// 降级管理器
type DegradeManager struct {
	config *DegradeConfig
	mu     sync.RWMutex
}

// CheckFeature 检查功能是否可用
func (dm *DegradeManager) CheckFeature(feature string) bool {
	dm.mu.RLock()
	defer dm.mu.RUnlock()
	switch feature {
	case "recommendation":
		return dm.config.EnableRecommendation
	case "comments":
		return dm.config.EnableComments
	case "coupons":
		return dm.config.EnableCoupons
	}
	return true // 默认开启
}

// UpdateConfig 配置中心回调,热更新降级配置
func (dm *DegradeManager) UpdateConfig(newConfig *DegradeConfig) {
	dm.mu.Lock()
	dm.config = newConfig
	dm.mu.Unlock()
	log.Printf("degrade config updated: %+v", newConfig)
}

配置变更后,运维人员通过 Nacos 控制台一键关闭某个功能,不需要发版、不需要重启。

三者协同:生产级流量治理架构

限流、熔断、降级不是孤立使用的,需要组合成一个多层防护体系。下面是我实际落地的架构:

                        用户请求
                 ┌──────────────────┐
                 │   API Gateway    │  ← 全局限流(令牌桶)
                 │  (Kong/APISIX)   │     按 API 限流,按租户限流
                 └────────┬─────────┘
              ┌───────────┼───────────┐
              ▼           ▼           ▼
         ┌────────┐ ┌────────┐ ┌────────┐
         │订单服务 │ │用户服务 │ │搜索服务 │  ← 服务级限流(Sentinel/gobreaker)
         │        │ │        │ │        │     按接口限流,按用户限流
         │熔断器 A │ │熔断器 B │ │熔断器 C │  ← 调用下游时熔断
         └───┬────┘ └───┬────┘ └───┬────┘
             │          │          │
             ▼          ▼          ▼
         ┌────────┐ ┌────────┐ ┌────────┐
         │支付服务 │ │认证服务 │ │数据库   │  ← 资源级保护
         │        │ │        │ │        │     连接池限流,慢SQL熔断
         │降级策略 │ │降级策略 │ │        │
         └────────┘ └────────┘ └────────┘

多层防护的职责划分

层级防护手段工具配置建议
网关层全局限流Kong/APISIX/Nginx按API限流,QPS = 峰值×1.5
服务层接口限流Sentinel/Resilience4j按接口+用户维度限流
调用层熔断+重试Resilience4j/gobreaker失败率50%熔断,最多重试2次
资源层连接池限制HikariCP/Go sql.DB连接数=CPU核心×2+1
业务层降级开关配置中心核心链路保护,非核心降级

超时配置的层级关系

超时配置必须从外到内递减,否则外层等不到内层返回就超时重试了:

API Gateway 超时 5s
    └─ 订单服务超时 3s
        └─ 支付服务超时 2s
            └─ 第三方支付超时 1.5s

每一层预留 0.5-1 秒的余量处理自身逻辑。如果最内层超时 1.5 秒,支付服务超时必须大于 1.5 秒(比如 2 秒),否则支付服务还没等到第三方返回就自己超时了。

踩坑案例:我们有一次线上故障,就是因为超时层级倒挂。订单服务超时 3 秒,支付服务超时 5 秒。支付服务在等第三方返回时没超时,但订单服务等支付服务等了 3 秒就超时了,然后重试——又给支付服务发了个请求。支付服务那边两个请求同时在处理,数据库行锁冲突,又拖慢了更多请求。最后不得不紧急把支付服务超时降到 2 秒才恢复。

关于告警策略的设计,可以参考 相关文章:告警策略设计——从噪声到信号,合理的告警规则能让你在熔断触发前就收到预警。

生产环境踩坑实录

坑1:熔断恢复后的流量洪峰

熔断器从 OPEN 切到 HALF_OPEN 时放行探测请求。如果探测成功,立刻切回 CLOSED,之前积压的请求瞬间全部涌过来——又把刚恢复的下游打挂了。

解决方案:在 CLOSED 状态恢复时做渐进式放量。不是一下子切回 100% 流量,而是先放 10%,观察 30 秒没问题再放 30%,再观察后放 100%。

// 渐进式恢复:在熔断器恢复后控制流量比例
type GradualRecovery struct {
	mu              sync.Mutex
	recoveryPhase   int      // 0=关闭, 1=10%, 2=30%, 3=100%
	phaseStartTime  time.Time
}

func (gr *GradualRecovery) Allow() bool {
	gr.mu.Lock()
	defer gr.mu.Unlock()

	ratios := []float64{0.1, 0.3, 1.0} // 渐进比例
	if gr.recoveryPhase >= len(ratios) {
		return true // 完全恢复
	}

	// 检查是否该进入下一阶段
	if time.Since(gr.phaseStartTime) > 30*time.Second {
		gr.recoveryPhase++
		gr.phaseStartTime = time.Now()
	}

	// 按比例放行
	ratio := ratios[gr.recoveryPhase]
	if rand.Float64() < ratio {
		return true
	}
	return false
}

坑2:重试放大效应

前面提到过,A 调 B 重试 3 次,B 调 C 也重试 3 次,流量放大 9 倍。但还有更隐蔽的版本:

场景:订单服务调库存服务,超时 2 秒,重试 3 次。库存服务一切正常,只是某次 GC 暂停了 1.5 秒。订单服务超时后重试,库存服务收到了 3 个相同请求,扣了 3 次库存。

解决方案

  1. 重试必须幂等。库存扣减不能直接 UPDATE stock SET count = count - 1,要用幂等键:先检查请求 ID 是否已处理,处理过直接返回上次结果。
  2. 限制重试的总次数。不是每层独立重试 3 次,而是整条链路最多重试 3 次。可以通过请求头传递剩余重试次数。
  3. 重试加抖动。不要立即重试,等一个随机时间(比如 100ms-500ms),避免多个调用方同时重试。

坑3:Sentinel 规则未持久化

Sentinel 默认规则存在内存里,服务重启就丢了。有次服务器重启后限流规则消失,正好赶上流量高峰,直接把下游数据库打爆。

解决方案:规则必须持久化到配置中心(Nacos/Apollo),Sentinel 启动时从配置中心拉取,规则变更时推送到所有实例。

坑4:熔断器状态变更没告警

熔断器从 CLOSED 切到 OPEN 是重要信号——说明下游出了问题。但如果没接告警,你可能到用户投诉才知道。

解决方案:在 OnStateChange 回调里推送告警。不仅推送 OPEN,HALF_OPEN 和 CLOSED 也要推送——知道什么时候恢复了同样重要。告警内容要包含:熔断器名称、状态变化、当前失败率、触发原因。(参见 相关文章:告警自动化处理——从告警风暴到自愈系统

坑5:Envoy 熔断驱逐导致可用实例不足

Envoy 的 maxEjectionPercent: 50 意味着最多驱逐 50% 的实例。但如果服务只有 2 个实例,驱逐 1 个就到 50% 了,只剩 1 个实例扛全部流量。

解决方案:小规模服务降低驱逐比例,或者增加 minHealthPercent 到 100(不允许驱逐任何实例,只做降权)。另一个办法是让服务实例数至少 3 个——这在 K8s 环境下很容易做到。

工具选型对比

工具语言限流熔断降级控制台适用场景
SentinelJavaJava 生态首选
Resilience4jJava轻量级,模块化
HystrixJava已停止维护,不推荐新项目
gobreakerGoGo 熔断
Envoy/Istio语言无关Service Mesh 方案
Kong/APISIX语言无关API 网关层

我的选型建议:

  • 纯 Java 微服务:Sentinel + Resilience4j。Sentinel 做限流和热点参数控制,Resilience4j 做熔断。
  • 纯 Go 微服务:gobreaker + 自研限流。Go 生态没有 Sentinel 级别的综合方案,但自己实现的限流器+熔断器足够用。
  • 多语言混合:Istio/Envoy 在网格层做,不侵入业务代码。
  • 不想改代码:用 API 网关层限流 + Envoy 熔断,零代码侵入。

监控与可观测性

限流熔断降级上线后,必须能看到它们的运行状态。需要监控的关键指标:

指标含义告警阈值
限流拒绝率被限流请求/总请求> 5%
熔断器状态OPEN/CLOSED/HALF_OPENOPEN 持续 > 1min
熔断触发次数单位时间内 OPEN 次数> 3次/5min
降级触发次数执行降级逻辑的次数> 100次/min
熔断恢复时间OPEN → CLOSED 耗时> 5min

Prometheus + Grafana 的监控配置:

# Prometheus 抓取熔断器指标(Resilience4j 暴露的 metrics)
scrape_configs:
  - job_name: 'resilience4j'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['order-service:8080', 'payment-service:8080']
# 熔断器状态(0=CLOSED, 1=OPEN, 2=HALF_OPEN)
resilience4j_circuitbreaker_state

# 熔断器调用失败率
resilience4j_circuitbreaker_failure_rate

# 被限流的请求速率
rate(resilience4j_ratelimiter_available_permissions[1m])

# 告警规则:熔断器打开超过1分钟
ALERT CircuitBreakerOpen
IF resilience4j_circuitbreaker_state == 1
FOR 1m
LABELS { severity = "critical" }
ANNOTATIONS {
  summary = "熔断器 {{ $labels.name }} 已打开",
  description = "熔断器已打开超过1分钟,请检查下游服务状态"
}

容量评估与压力测试

在配置限流阈值之前,必须知道你的系统到底能扛多少。不能拍脑袋设一个数字。

压测方法

  1. 基准压测:逐步加压,找到系统拐点(响应时间开始急剧上升的点)。拐点之前的 QPS 就是安全水位。
  2. 极限压测:持续加压到系统崩溃,记录崩溃前的最大 QPS。这个值除以 2-3 就是限流阈值的参考值。
  3. 混合压测:模拟真实流量比例(读:写 = 8:2),而不是只测单一接口。

容量计算公式

安全 QPS = 基准压测拐点 QPS × 安全系数(0.6-0.7)
限流阈值 = 安全 QPS × 实例数 × 负载不均衡系数(0.8)

举个例子:单实例基准压测拐点是 2000 QPS,安全系数取 0.7,安全 QPS = 1400。10 个实例,负载不均衡系数 0.8(考虑 Nginx/K8s 分配不均),限流阈值 = 1400 × 10 × 0.8 = 11200 QPS。

关于容量规划的完整方法论,可以参考 相关文章:SLO 设计实战——从业务目标到技术指标,把限流阈值和 SLO 目标对齐。

总结

微服务限流熔断降级不是一个"装上就完事"的组件,而是一套需要持续调优的流量治理体系。回到我在 50+ 微服务治理中的实战经验,总结几条核心原则:

限流:网关层用令牌桶允许突发,服务层用 Sentinel 做精细化控制。分布式限流用 Redis + Lua 脚本保证原子性。限流阈值必须基于压测数据,不能拍脑袋。

熔断:Resilience4j(Java)或 gobreaker(Go)做应用层熔断,Envoy/Istio 做网格层熔断。核心调参是 failureRateThresholdslowCallDurationThreshold——前者决定多敏感,后者决定多快的调用算慢。minimumNumberOfCalls 必须设置,避免小样本误判。

降级:区分核心链路和非核心链路。降级方案必须提前测试,通过配置中心做开关。降级返回的是"次优结果"不是错误码。

协同:多层防护从外到内递进——网关限流、服务限流、调用熔断、资源保护、业务降级。超时配置从外到内递减。重试必须幂等且加抖动。

可观测:熔断器状态变更必须告警。限流拒绝率、熔断次数、降级触发次数都要有监控面板和告警规则。

最后一条经验:限流熔断降级上线后,一定要做混沌工程演练——故意杀掉一个服务,看防护体系是否按预期工作。纸上谈兵和真实故障之间差着十万八千里。别等到凌晨三点被告警吵醒才发现你的熔断配置有问题。关于故障演练的系统性方法,可以参考 相关文章:SRE 故障预案与演练

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Circuit Breaker Design Pattern — GitHub Topics,收录了主流熔断器开源项目,包括 Sentinel、Resilience4j、gobreaker 等
  2. Spring Boot 微服务熔断技术从演进到实战:2026 年最新指南 — CSDN,提供了 Resilience4j 熔断器状态机模型的详细解释
  3. 微服务中的超时、重试与熔断:别让一次慢调用拖垮整个系统 — CSDN,提供了超时配置和重试风暴的分析
  4. 微服务中集成大模型调用的降级限流与优雅容灾实践 — CSDN,提供了多层防护架构的设计思路
  5. Resilience4j 简介 — 博客园,提供了 Resilience4j 各模块的对比和配置说明
  6. Performance Modeling of Microservices with Circuit Breakers using Stochastic Petri Nets — IEEE,学术论文,研究熔断器对微服务性能的量化影响,实测熔断器可降低请求丢弃率 71.4%
  7. Self-Adaptive Circuit Breaker Open-State Interval for Enhancing Resiliency of Microservices — IEEE,学术论文,提出自适应熔断器冷却时间方案