推荐服务降级:特征服务挂了,推荐不能一起挂

发布时间:2026/7/22 11:30:01
推荐服务降级:特征服务挂了,推荐不能一起挂 推荐服务降级特征服务挂了推荐不能一起挂一、推荐系统的可用性悖论越个性化越脆弱推荐系统是典型的个性化即依赖架构。一个推荐请求从进入到返回沿着链路往下走特征服务 → 召回服务 → 精排模型 → 重排策略。这条链路上的每一个节点都是个性化的来源也都是潜在的故障点。特征服务挂了——用户画像和物品特征查不到排不出个性化结果。召回服务挂了——协同过滤、向量召回全部失效候选池为空。精排模型挂了——所有物品没有预估分数无法排序。每一个环节的故障都会导致推荐链路中断。常规思路是做高可用——每个服务多副本、异地多活、自动故障转移。这些是基础设施的必要保障但架构上还有一个更根本的问题推荐服务的正常运转依赖 5 个以上子服务的协同工作任何一个子服务不可用都会拖垮整体。做一个简单的概率计算假设每个子服务可用性 99.9%3 个 95 个子服务串联起来的整体可用性是 0.999^5 99.5%。听起来还行但这是理想情况——实际上故障往往是关联的一次机房网络抖动可能同时影响 2-3 个子服务。降级的本质不是让推荐服务更稳定而是让推荐服务在子服务不可用时仍能返回一个还过得去的结果。这和返回最优结果的目标不同——降级追求的是有损但可用而非完美但不可达。二、逐层降级策略每个环节都要有 Plan B降级不是一刀切——全挂了就只推热门。降级策略应该是逐层递进的每一层都有自己的兜底方案而且要和上层降级组合使用。特征服务降级特征查不到时使用默认特征向量。默认向量不是全 0 或随机数而是全体用户的平均特征向量——离线 Spark 任务每天计算一次所有活跃用户特征的平均值写入本地配置文件。平均向量虽然丢失了个性化但比零向量好——零向量在双塔模型中做内积会导致所有物品得分相同排序退化到随机。召回服务降级多路召回全挂时降级为全局热门召回。热门池不是实时计算的而是离线每小时更新一次写入 Redis。热门召回不需要任何用户特征只需要拉取 Top 200 热门物品。推荐效果会大幅下降点击率降低 40%-60%但用户至少能看到内容不会出现暂无推荐的白屏。精排模型降级模型推理服务不可用时跳过精排直接用召回阶段的预估分数排序。召回阶段协同过滤、向量检索都会输出一个粗略的分数虽然不如精排模型准确但排序逻辑仍然存在——相关性高的物品仍然排在前面。降级后点击率下降约 20%-30%明显好于随机排序。重排策略降级多样性调节和打散逻辑不可用时降级为简单去重——只保证同一物品不重复出现。损失的是内容多样性可能连续出现同作者、同品类的内容但不影响基本可用性。三、Go 实现熔断器 降级组合的代码框架降级策略在 Go 代码中通过熔断器 回调函数实现。每个外部依赖服务前面放一个熔断器熔断状态打开后自动跳转到降级逻辑type RecommendationEngine struct { featureClient *FeatureClient recallClient *RecallClient rankClient *RankClient featureBreaker *gobreaker.CircuitBreaker[FeatureResult] } func NewRecommendationEngine() *RecommendationEngine { return RecommendationEngine{ featureBreaker: gobreaker.NewCircuitBreaker[FeatureResult](gobreaker.Settings{ Name: feature-service, MaxRequests: 3, // 半开状态最多尝试3次 Interval: 60 * time.Second, // 熔断后60秒进入半开 Timeout: 10 * time.Second, // 熔断超时10秒 ReadyToTrip: func(counts gobreaker.Counts) bool { failureRatio : float64(counts.TotalFailures) / float64(counts.Requests) return counts.Requests 10 failureRatio 0.5 // 10个请求中失败率超50%则熔断 }, }), } } func (e *RecommendationEngine) GetUserFeatures(ctx context.Context, uid string) (*FeatureResult, error) { // 熔断器包裹正常调用 result, err : e.featureBreaker.Execute(func() (FeatureResult, error) { ctx, cancel : context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() resp, err : e.featureClient.GetUserFeatures(ctx, uid) if err ! nil { return FeatureResult{}, err } return resp, nil }) if err ! nil { // 熔断或超时降级到默认特征 metrics.IncDegradationCount(feature_service) log.Warn(feature service degraded for user, zap.String(uid, uid), zap.Error(err)) return e.getDefaultFeatures(uid), nil } return result, nil } // 默认特征全体用户平均值离线计算 func (e *RecommendationEngine) getDefaultFeatures(uid string) *FeatureResult { return FeatureResult{ ClickRate: 0.12, CategoryPref: e.globalAvgCategoryPref, Embedding: e.globalAvgEmbedding, IsDefault: true, // 标记为降级数据精排模型可以差异化处理 } }降级信号的传递是整个降级体系的关键。当特征服务降级返回默认特征时IsDefault: true标记会让下游精排模型感知到这批特征的置信度很低从而降低特征权重。这个标记传递需要贯穿整个链路——召回降级、精排降级也同样需要标记最终在监控面板上能看到每个请求经历了多少层降级。四、降级不是免费的降级期间的业务损失评估降级保证了服务可用但有明确的代价。不同层级的降级对应不同程度的推荐效果损失降级层级点击率损失用户可感知程度恢复时间特征降级默认特征-15% ~ -25%轻微推荐变泛化秒级恢复召回降级纯热门-40% ~ -60%明显推荐同质化熔断器半开后恢复精排降级召回分排序-20% ~ -30%轻微排序精度下降秒级恢复全链路降级随机热门-70% ~ -85%非常明显需要人工介入降级期间的核心关注点不是推荐效果降了多少而是降级持续时间。特征降级持续 5 分钟影响很小——用户在刷内容推荐变泛化几屏后特征服务恢复体验回到正常。但如果全链路降级持续 30 分钟以上用户会明显感受到内容质量下降可能直接退出 App。所以降级系统的优先级是先保证服务不挂即使推热门再尽快恢复全链路能力。还需要关注的是降级的级联效应。当特征服务降级、默认特征进入精排模型后模型输出的预估分数是偏低的因为特征不准确。精排模型拿低分排序推荐出的内容质量差用户点击率低。这反过来导致后续的实时特征质量也变差——用户不点击没有正反馈信号Flink 计算出的实时特征空转。降级不一定爆炸在原发故障点也可能在降级链路的下游引发次生故障。五、总结推荐服务的降级体系是可用性的最后一道防线。核心原则是逐层降级、有损可用——每一层子服务都有独立的降级预案特征用默认值兜底召回用热门兜底精排用召回分兜底重排用简单去重兜底。工程实现上熔断器是降级触发的核心机制。Go 的gobreaker或sony/gobreaker提供开箱即用的熔断功能配合超时控制和半开恢复策略能自动完成服务恢复。降级标记的传递是跨层协作的关键——每个降级环节都要告诉下游我这边已经降级了下游据此调整自身策略。降级的代价是推荐效果的损失但这个代价在可用性面前是必须接受的取舍。一个可用的低质量推荐远好于一个不可用的零推荐。降级系统的目标是极限情况下的不断线而不是理想情况下的最准。这两个目标的优先级在推荐系统正式上线第一天就要想清楚。