← 中文资源

简体中文版本

优雅降级(Graceful Degradation)

优雅降级在容量或依赖故障时削减非核心功能、保留核心能力,让服务在压力下仍然可用而不是整体失败。

阅读英文原文

核心思想

优雅降级在系统部分能力跟不上时保住核心服务。系统不返回全面错误,而是提供更少功能、更低精度或缓存数据。商店可以停止个性化推荐但保留搜索和结账;仪表盘可以显示过期指标而不是空白页。设计问题是:理想路径不可用时,用户仍必须能做什么。

先确定优先级

只有团队事先约定层级,降级才有效。把操作分为关键、重要和可选。认证、支付和数据持久性等关键路径不能被削减;重要路径可以降低质量;推荐、分析等可选功能最先关闭。没有显式排序,工程团队会在事故中才发现优先级,通常是偶然发现。

技术手段

用超时和熔断器停止等待失败依赖;提供带明确新鲜度标识的缓存或过期响应;关闭昂贵的个性化;返回带警告的部分结果而不是让整个请求失败;读必须继续时把写操作排队延后。功能开关让运维无需部署即可关闭组件。每种手段都需要负责人和明确的恢复条件。

设计与测试

在设计功能时而不是事故中决定回退行为。通过注入依赖故障测试降级路径是否工作、用户是否理解。例如,悄悄跳过税费计算的结账不是优雅降级,而是错误。降级必须为保留的功能保持正确性。降级会增加分支和失败模式,也无法挽救共享瓶颈(如单一数据库)已经失效的系统。与快速失败相比,优雅降级提高可用性却让推理和测试更复杂。只在部分服务对用户有真实价值时使用,并保持降级状态数量少。

代码示例

package main

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

type State int

const (
	StateClosed State = iota
	StateOpen
	StateHalfOpen
)

func (s State) String() string {
	switch s {
	case StateClosed:
		return "closed"
	case StateOpen:
		return "open"
	case StateHalfOpen:
		return "half-open"
	default:
		return "unknown"
	}
}

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

type Breaker struct {
	mu        sync.Mutex
	threshold int
	cooldown  time.Duration
	now       func() time.Time
	state     State
	failures  int
	openedAt  time.Time
}

func NewBreaker(threshold int, cooldown time.Duration) (*Breaker, error) {
	if threshold < 1 {
		return nil, fmt.Errorf("threshold must be positive")
	}
	if cooldown <= 0 {
		return nil, fmt.Errorf("cooldown must be positive")
	}
	return &Breaker{threshold: threshold, cooldown: cooldown, now: time.Now}, nil
}

func (b *Breaker) Allow() error {
	b.mu.Lock()
	defer b.mu.Unlock()

	if b.state == StateOpen {
		if b.now().Sub(b.openedAt) < b.cooldown {
			return ErrOpen
		}
		b.state = StateHalfOpen
	}
	return nil
}

func (b *Breaker) record(err error) {
	b.mu.Lock()
	defer b.mu.Unlock()

	if err == nil {
		b.state = StateClosed
		b.failures = 0
		return
	}
	b.failures++
	if b.state == StateHalfOpen || b.failures >= b.threshold {
		b.state = StateOpen
		b.openedAt = b.now()
	}
}

func (b *Breaker) Execute(operation func() error) error {
	if err := b.Allow(); err != nil {
		return err
	}
	err := operation()
	b.record(err)
	return err
}

func (b *Breaker) CurrentState() State {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.state
}

type fakeClock struct {
	current time.Time
}

func (c *fakeClock) Now() time.Time { return c.current }

func (c *fakeClock) Advance(step time.Duration) { c.current = c.current.Add(step) }

func main() {
	clock := &fakeClock{current: time.Date(2026, 10, 21, 12, 0, 0, 0, time.UTC)}
	breaker, err := NewBreaker(2, 100*time.Millisecond)
	if err != nil {
		fmt.Println(err)
		return
	}
	breaker.now = clock.Now

	failing := func() error { return errors.New("upstream unavailable") }
	for call := 1; call <= 3; call++ {
		fmt.Printf(
			"call %d: err=%v state=%s\n",
			call,
			breaker.Execute(failing),
			breaker.CurrentState(),
		)
	}

	clock.Advance(150 * time.Millisecond)
	fmt.Println("after cooldown state:", breaker.CurrentState())
	probeErr := breaker.Execute(func() error { return nil })
	fmt.Printf("probe err=%v state=%s\n", probeErr, breaker.CurrentState())
}
const RETRYABLE = new Set([408, 425, 429, 500, 502, 503, 504]);
const CACHEABLE = new Set([200, 203, 204, 206, 300, 301, 308, 404, 410]);

export function classifyStatus(status) {
  if (!Number.isInteger(status) || status < 100 || status > 599) {
    throw new RangeError("status must be a three-digit HTTP status code");
  }
  const group = Math.floor(status / 100);
  const labels = {
    1: "informational",
    2: "success",
    3: "redirect",
    4: "client-error",
    5: "server-error",
  };
  return {
    status,
    group,
    label: labels[group],
    success: group === 2,
    redirect: group === 3,
    error: group >= 4,
    retryable: RETRYABLE.has(status),
    cacheable: CACHEABLE.has(status),
  };
}

export function isRetryableStatus(status) {
  return classifyStatus(status).retryable;
}