概述
服务降级(Fallback / Degradation) 是指当服务依赖的下游出现故障、超时或熔断器打开时,不再执行正常的业务逻辑,而是返回一个预设的兜底结果,以保证核心链路的可用性。
降级的核心思想:宁可返回不完美的结果,也不要让用户看到错误。
与熔断的区别:
- 熔断关注的是"切断调用,防止故障扩散"
- 降级关注的是"返回兜底结果,保证用户体验"
两者通常配合使用:熔断是手段,降级是目的。
请求进入
↓
┌──────────────┐
│ 熔断检查 │── 熔断打开 ──→ 进入降级逻辑
└──────┬───────┘
↓ 允许
┌──────────────┐
│ 正常调用 │── 成功 ──→ 返回正常结果
└──────┬───────┘
↓ 失败/超时
┌──────────────┐
│ 降级逻辑 │── 返回兜底数据
└──────────────┘
降级策略分类
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 默认值降级 | 返回固定的默认值或空值 | 列表查询、配置读取 |
| 缓存降级 | 返回本地/远程缓存中的旧数据 | 商品详情、用户信息 |
| Mock 降级 | 返回模拟数据 | 非核心功能、开发测试环境 |
| 页面降级 | 返回简化版页面或静态页 | 前端展示类服务 |
| 组合降级 | 多级降级:实时→缓存→默认值 | 高可用要求高的核心链路 |
方案一:Middleware 实现通用降级
在 Kitex 客户端通过 Middleware 统一拦截,实现声明式降级。
降级处理器接口
package fallback
import (
"context"
)
// FallbackFunc 降级函数签名
type FallbackFunc func(ctx context.Context, req, err error) (resp interface{}, fallbackErr error)
// FallbackProvider 降级提供者
type FallbackProvider struct {
funcMap map[string]FallbackFunc // 按方法名映射降级函数
}
// NewFallbackProvider 创建降级提供者
func NewFallbackProvider() *FallbackProvider {
return &FallbackProvider{
funcMap: make(map[string]FallbackFunc),
}
}
// Register 注册特定方法的降级逻辑
func (fp *FallbackProvider) Register(method string, fn FallbackFunc) {
fp.funcMap[method] = fn
}
// Get 获取指定方法的降级函数
func (fp *FallbackProvider) Get(method string) FallbackFunc {
return fp.funcMap[method]
}
降级 Middleware
func FallbackMW(fp *FallbackProvider) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
// 1. 执行正常调用
err = next(ctx, req, resp)
// 2. 如果调用成功,直接返回
if err == nil {
return nil
}
// 3. 提取方法名
method := getMethodName(ctx)
// 4. 查找是否有对应的降级逻辑
fallbackFn := fp.Get(method)
if fallbackFn == nil {
// 没有降级逻辑,向上返回错误
return err
}
// 5. 执行降级
fallbackResp, fallbackErr := fallbackFn(ctx, req, err)
if fallbackErr != nil {
// 降级也失败了,返回原始错误
return err
}
// 6. 将降级结果写回 resp(需要类型断言)
if respVal, ok := resp.(interface{ Reset() }); ok {
respVal.Reset()
}
// 具体写入方式取决于 RPC 框架的 resp 结构
_ = fallbackResp
return nil
}
}
}
使用示例
// 1. 创建降级提供者
fallbackProvider := NewFallbackProvider()
// 2. 注册降级逻辑
fallbackProvider.Register("GetUserInfo", func(ctx context.Context, req interface{}, err error) (interface{}, error) {
// 从本地缓存读取用户信息
cacheKey := req.(*GetUserInfoRequest).UserId
userInfo := getUserInfoFromCache(cacheKey)
if userInfo != nil {
return &GetUserInfoResponse{
User: userInfo,
Fallback: true, // 标记为降级数据
}, nil
}
// 缓存也没有,返回默认值
return &GetUserInfoResponse{
User: &UserInfo{
Name: "未知用户",
},
Fallback: true,
}, nil
})
fallbackProvider.Register("QueryProductList", func(ctx context.Context, req interface{}, err error) (interface{}, error) {
// 返回缓存的商品列表
list := getProductListFromCache()
return &QueryProductListResponse{
Products: list,
Fallback: true,
}, nil
})
// 3. 注入客户端
client, err := shop.NewClient(
"dqq.shop",
client.WithMiddleware(FallbackMW(fallbackProvider)),
client.WithMiddleware(CircuitBreakerMW(cb)), // 与熔断配合
client.WithRPCTimeout(200*time.Millisecond),
)
方案二:熔断 + 降级联动
这是最常见的生产实践:熔断器打开时自动走降级逻辑。
func CircuitBreakerWithFallback(cb *CircuitBreaker, fp *FallbackProvider) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
// 1. 熔断检查
if !cb.AllowRequest() {
// 熔断打开,走降级
method := getMethodName(ctx)
fallbackFn := fp.Get(method)
if fallbackFn != nil {
fallbackResp, _ := fallbackFn(ctx, req, ErrCircuitBreakerOpen)
if respVal, ok := resp.(interface{ Reset() }); ok {
respVal.Reset()
}
_ = fallbackResp
return nil
}
// 无降级逻辑,返回熔断错误
return ErrCircuitBreakerOpen
}
// 2. 正常调用
err = next(ctx, req, resp)
if err != nil {
cb.RecordFailure()
} else {
cb.RecordSuccess()
}
// 3. 调用失败且存在降级逻辑
if err != nil {
method := getMethodName(ctx)
fallbackFn := fp.Get(method)
if fallbackFn != nil {
fallbackResp, fallbackErr := fallbackFn(ctx, req, err)
if fallbackErr == nil {
if respVal, ok := resp.(interface{ Reset() }); ok {
respVal.Reset()
}
_ = fallbackResp
return nil
}
}
}
return err
}
}
}
方案三:Sentinel 降级规则
如果使用 Apache Sentinel,可以通过规则配置实现声明式降级:
import (
"github.com/alibaba/sentinel-golang/core/degrade"
)
// 降级规则:当错误率达到阈值时触发降级
rule := degrade.Rule{
Resource: "dqq.shop/Service.GetUserInfo",
Strategy: degrade.StrategyErrorRatio,
Threshold: 0.5, // 错误率 50%
MinRequestNumber: 10, // 最少 10 个请求
StatIntervalMs: 10000, // 统计窗口 10s
RestoreTimeoutMs: 30000, // 降级恢复时间 30s
}
degrade.PutRule(rule)
// Sentinel 支持三种降级策略:
// StrategyExceptionCount — 异常数阈值
// StrategyErrorRatio — 异常比率阈值
// StrategySlowRequestRatio — 慢调用比率阈值
Sentinel 配合 FallbackFunction 可以实现更细粒度的降级控制:
// Sentinel 的 FlowProtection 支持 fallback 回调
sentinel.SeatProtection("resource_name",
sentinel.WithFallback(func(ctx context.Context, err error) {
// 降级逻辑
return fallbackResponse, nil
}),
)
方案四:本地缓存降级(实战常用)
对于读多写少的场景,本地缓存是最实用的降级手段:
type CachedClient struct {
inner Client // 真实的 Kitex 客户端
cache *localcache.Cache // 本地缓存
ttl time.Duration
stats *FallbackStats // 降级统计
}
func (cc *CachedClient) GetUserInfo(ctx context.Context, req *GetUserInfoRequest) (*GetUserInfoResponse, error) {
// 1. 先查本地缓存
if cached := cc.cache.Get(req.UserId); cached != nil {
cc.stats.IncCacheHit()
resp := cached.(*GetUserInfoResponse)
resp.Fallback = true
return resp, nil
}
// 2. 缓存未命中,调用远程服务
resp, err := cc.inner.GetUserInfo(ctx, req)
if err != nil {
cc.stats.IncFallback()
// 3. 远程调用失败,返回缓存的旧数据(如果有的话)
if cached := cc.cache.Get(req.UserId); cached != nil {
resp := cached.(*GetUserInfoResponse)
resp.Fallback = true
return resp, nil
}
return nil, err
}
// 4. 成功则写入缓存
cc.cache.Set(req.UserId, resp, cc.ttl)
return resp, nil
}
降级标记与可观测性
降级数据应当与正常数据区分,以便排查问题和监控:
响应中标记降级
type BaseResponse struct {
Fallback bool `json:"fallback,omitempty"` // 是否为降级数据
FallbackReason string `json:"fallback_reason,omitempty"` // 降级原因
}
// 在降级函数中设置
return &GetUserInfoResponse{
BaseResponse: BaseResponse{
Fallback: true,
FallbackReason: "downstream_timeout",
},
User: userInfo,
}, nil
降级指标上报
type FallbackStats struct {
totalRequests prometheus.Counter
fallbackCalls prometheus.Counter
cacheHits prometheus.Counter
fallbackErrors prometheus.Counter
}
func (fs *FallbackStats) IncFallback() {
fs.fallbackCalls.Inc()
}
func (fs *FallbackStats) IncCacheHit() {
fs.cacheHits.Inc()
}
// 在 Middleware 中集成
func FallbackWithMetricsMW(fp *FallbackProvider, stats *FallbackStats) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
err = next(ctx, req, resp)
if err != nil {
method := getMethodName(ctx)
fallbackFn := fp.Get(method)
if fallbackFn != nil {
stats.IncFallback()
_, fallbackErr := fallbackFn(ctx, req, err)
if fallbackErr != nil {
stats.IncFallbackErrors()
}
}
}
return err
}
}
}
降级 vs 熔断 vs 限流
| 机制 | 位置 | 触发条件 | 行为 | 目的 |
|---|---|---|---|---|
| 限流 | 服务端 | QPS/并发超限 | 拒绝新请求 | 保护自身 |
| 熔断 | 客户端 | 下游错误率过高 | 切断调用,快速失败 | 防止故障扩散 |
| 降级 | 客户端 | 熔断打开 / 调用失败 | 返回兜底结果 | 保证用户体验 |
三者协作关系:
限流(服务端) 熔断(客户端) 降级(客户端)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 保护自身 │ │ 切断调用 │ │ 兜底响应 │
│ 拒绝请求 │──────→ │ 快速失败 │──────→ │ 返回默认 │
│ │ │ │ │ 数据 │
└──────────┘ └──────────┘ └──────────┘
第一道防线 第二道防线 最后一道防线
最佳实践
1. 分级降级策略
按业务重要性分层设计降级方案:
| 级别 | 策略 | 示例 |
|---|---|---|
| P0 核心 | 实时 → 缓存 → 默认值 → 友好提示 | 用户登录、支付 |
| P1 重要 | 实时 → 缓存 → 默认值 | 商品列表、订单查询 |
| P2 一般 | 实时 → 缓存 → 跳过 | 推荐、评论 |
| P3 边缘 | 直接跳过 | 消息通知、日志上报 |
2. 降级数据时效性标注
所有降级数据必须标注来源和时间,避免误导用户:
type UserInfoResponse struct {
User *UserInfo `json:"user"`
Fallback bool `json:"fallback"`
CacheTime string `json:"cache_time,omitempty"` // 缓存时间
DataAge string `json:"data_age,omitempty"` // 数据距今多久
}
3. 降级开关
通过配置中心控制降级开关,便于紧急场景手动触发:
var fallbackEnabled atomic.Bool
func init() {
fallbackEnabled.Store(true) // 默认开启
// 监听配置中心变更
config.Watch("fallback.enabled", func(val bool) {
fallbackEnabled.Store(val)
})
}
func FallbackMW(fp *FallbackProvider) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
err = next(ctx, req, resp)
if err == nil {
return nil
}
if !fallbackEnabled.Load() {
return err // 降级关闭,直接返回错误
}
// ... 降级逻辑
}
}
}
4. 避免降级雪崩
- 降级逻辑本身也要加超时和限流,避免降级函数阻塞
- 降级返回的数据应尽可能轻量,避免二次调用重型依赖
- 定期评估降级覆盖率,确保核心链路都有兜底方案
5. 灰度降级
对部分用户开启降级,观察效果后再全量:
func FallbackWithGrayMW(fp *FallbackProvider, grayRate float64) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
err = next(ctx, req, resp)
if err == nil {
return nil
}
// 按用户 ID 哈希决定是否走降级
userID := extractUserID(req)
if hash(userID)%100 >= int(grayRate*100) {
return err // 不走降级,返回真实错误
}
// 走降级逻辑
fallbackFn := fp.Get(getMethodName(ctx))
if fallbackFn != nil {
fallbackResp, _ := fallbackFn(ctx, req, err)
_ = fallbackResp
}
return nil
}
}
}
注意事项
- 降级不是万能药:降级数据可能与实时数据不一致,需告知用户
- 降级逻辑要简单:避免在降级函数中调用其他远程服务,否则可能引发连锁故障
- 注意缓存穿透:降级返回的缓存数据可能是空的,需设置合理的 TTL
- 监控覆盖率:统计降级触发次数和比例,及时发现异常
- 测试降级场景:通过混沌工程模拟下游故障,验证降级是否生效
相关笔记
- [[Kitex/熔断]] — 熔断是降级的前置条件,两者通常配合使用
- [[Kitex/限流]] — 限流保护服务端,降级保护客户端体验
- [[Kitex/超时]] — 超时会触发降级逻辑
- [[Kitex/中间件]] — 降级通过 Middleware 机制嵌入调用链
- [[Kitex/重试]] — 重试失败后再走降级