服务降级与熔断策略从全挂了到核心功能可用一、所有服务都超时——一个真实的后端灾难Judge Worker 集群因为一次 Docker daemon 重启不可用。不到 30 秒连锁反应发生了API Gateway 的请求全部超时30s连接池被等待中的请求耗尽新的健康用户请求被阻塞在连接池其他正常服务题目浏览、讨论也因为连接池耗尽而无法访问这不是 Judge Worker 的问题——是缺少熔断机制导致的级联故障。熔断器Circuit Breaker的设计目标就是切断这种连锁反应当一个下游服务不可用时上游服务应该快速失败fail fast而不是排队等待。熔断器的三个状态和状态转移条件看似简单但实践中的难点在于如何设置失败阈值冷却时间多长半开状态允许多少个探测请求这些参数没有标准答案需要根据业务的容错需求来调优。二、熔断器实现滑动窗口 状态机最朴素的熔断器实现是连续失败 N 次就熔断。但这个方案有个致命缺陷如果请求量很低偶发的失败如网络抖动也会触发熔断。改进方案是用滑动窗口统计失败率。import time import threading from enum import Enum from typing import Callable, Any, Optional from dataclasses import dataclass, field from collections import deque class CircuitState(Enum): CLOSED closed # 正常 OPEN open # 熔断 HALF_OPEN half_open # 半开 dataclass class CircuitBreakerConfig: 熔断器配置 参数调优指南 - failure_threshold: 太小→误熔断太大→熔断不及时 - half_open_max_requests: 建议1-3一次探测失败说明服务真的没恢复 - cooldown_seconds: 太短→频繁波动太长→恢复太慢 failure_threshold: int 5 # 连续失败几次后熔断 failure_rate_threshold: float 0.5 # 失败率阈值窗口模式 window_size: int 20 # 滑动窗口请求数 half_open_max_requests: int 2 # 半开状态允许几个探测请求 cooldown_seconds: int 30 # 熔断后多久进入半开状态 success_min_to_close: int 3 # 半开状态下多少个连续成功才关闭熔断器 class CircuitBreaker: 熔断器实现 用法 cb CircuitBreaker(CircuitBreakerConfig()) result cb.call(lambda: http_client.get(/api/workers), default_value[]) 如果下游挂了cb.call 返回 fallback_value不抛异常。 def __init__(self, config: CircuitBreakerConfig): self.config config self.state CircuitState.CLOSED self.failure_count 0 self.success_count 0 self.last_failure_time: float 0.0 self.half_open_requests 0 self.window: deque deque(maxlenconfig.window_size) self._lock threading.Lock() def call( self, func: Callable[[], Any], fallback_value: Any None ) - Any: 通过熔断器执行调用 根据当前状态决定 - CLOSED: 正常调用 - OPEN: 直接返回 fallback_value - HALF_OPEN: 允许少量探测请求 with self._lock: self._state_transition() if self.state CircuitState.OPEN: return fallback_value if (self.state CircuitState.HALF_OPEN and self.half_open_requests self.config.half_open_max_requests): return fallback_value if self.state CircuitState.HALF_OPEN: self.half_open_requests 1 # 执行实际调用在锁外执行避免阻塞其他请求 try: result func() self._on_success() return result except Exception as e: self._on_failure() return fallback_value def _on_success(self): 记录成功 with self._lock: self.failure_count 0 self.success_count 1 self.window.append(True) if self.state CircuitState.HALF_OPEN: # LeetCode 模式连续 N 个成功就关闭熔断器 if self.success_count self.config.success_min_to_close: self.state CircuitState.CLOSED self.success_count 0 self.half_open_requests 0 def _on_failure(self): 记录失败 with self._lock: self.failure_count 1 self.success_count 0 self.last_failure_time time.time() self.window.append(False) def _state_transition(self): 检查是否需要状态转移 转移规则 CLOSED → OPEN: 滑动窗口内失败率 阈值 OPEN → HALF_OPEN: 冷却时间已过 HALF_OPEN → OPEN: 探测请求失败 HALF_OPEN → CLOSED: 连续多个探测请求成功 if self.state CircuitState.OPEN: elapsed time.time() - self.last_failure_time if elapsed self.config.cooldown_seconds: self.state CircuitState.HALF_OPEN self.half_open_requests 0 self.success_count 0 elif self.state CircuitState.CLOSED: # 滑动窗口失败率检查 if len(self.window) self.config.window_size: failures sum(1 for ok in self.window if not ok) failure_rate failures / len(self.window) if failure_rate self.config.failure_rate_threshold: self.state CircuitState.OPEN self.last_failure_time time.time() def get_state(self) - dict: 获取熔断器当前状态用于监控 failures sum(1 for ok in self.window if not ok) if self.window else 0 total len(self.window) return { state: self.state.value, failure_count: self.failure_count, window_failures: failures, window_total: total, failure_rate: failures / total if total 0 else 0.0, success_count: self.success_count }熔断器的关键设计决定快速失败而非等待超时——用户 30 秒后看到超时错误 vs 0.1 秒后看到服务暂时不可用体验天差地别自动恢复探测——半开状态允许少量请求服务恢复了自动切回不需要人工介入按服务粒度隔离——Judge Worker 的熔断不应影响题目推荐服务三、降级策略不只是一个 fallback 值熔断器提供的 fallback 值是降级的最简单形式。真正的降级策略是一整套当 X 不可用时用简化版 Y 替代的预案class DegradationStrategy: 服务降级策略集合 降级不是返回空列表而是返回一个虽然不完美但可用的替代品。 每一层降级都要经过评估替代数据对用户的体验影响有多大 staticmethod def judge_result_degradation(submission_id: str) - dict: 评测服务降级返回排队中占位符 当 Judge Worker 集群宕机时不返回错误 而是返回一个排队中状态让前端轮询等待。 用户体验不是服务坏了而是等一下就好。 return { submission_id: submission_id, status: queued, message: 评测集群繁忙您的提交已进入队列预计等待 30 秒, position: -1 # -1 表示无法预估位置 } staticmethod def recommendation_degradation(user_id: str, difficulty: float) - list: 推荐服务降级返回按难度排序的热门题 当推荐模型不可用时降级为同难度热门题 Top 10。 虽然不个性化但至少用户还有题可刷。 return [ {problem_id: 100 i, difficulty: difficulty} for i in range(10) ] staticmethod def code_analysis_degradation(code: str) - dict: 代码分析服务降级返回空分析 return { complexity: unknown, suggestions: [], note: 代码分析服务暂时不可用 } staticmethod def static_cache_degradation(problem_id: int) - Optional[dict]: 静态缓存降级层 最可靠的降级方案预生成的静态数据。 如果连数据库都挂了返回预生成的 JSON 文件中的题目数据。 这是最后一道防线。 static_data { 1: {title: Two Sum, difficulty: Easy, tags: [hashmap]}, 42: {title: Trapping Rain Water, difficulty: Hard, tags: [two_pointers]}, } return static_data.get(problem_id)降级的核心原则是每一层降级都应该让用户知道发生了什么并提供某种形式的有用信息。一个空白页比一个服务暂时不可用的提示更让人恐慌。四、多级熔断 降级的组合策略在实际系统中熔断和降级不是二选一而是按层级组合的Level 1 熔断: 特定 Worker → 降级为重新调度到其他 Worker Level 2 熔断: Worker 集群 → 降级为排队等待 Level 3 熔断: 评测服务整体 → 降级为当前仅支持浏览题目 Level 4 熔断: 数据库连接 → 降级为静态缓存数据class ServiceOrchestrator: 多级熔断 降级的编排器 每一级服务都有独立的熔断器互不影响。 熔断触发后自动切换到降级策略。 def __init__(self): # 为每个下游服务创建独立的熔断器 self.judge_worker_cb CircuitBreaker( CircuitBreakerConfig( failure_rate_threshold0.3, cooldown_seconds60, half_open_max_requests1 ) ) self.recommendation_cb CircuitBreaker( CircuitBreakerConfig( failure_rate_threshold0.5, cooldown_seconds120 ) ) self.database_cb CircuitBreaker( CircuitBreakerConfig( failure_rate_threshold0.2, # 数据库更严格 cooldown_seconds300, # 恢复更慢 half_open_max_requests1 ) ) def submit_code(self, submission_id: str, code: str) - dict: 提交代码依次尝试主路径自动降级 # 先检查数据库熔断器 db_state self.database_cb.get_state() if db_state[state] open: # 数据库不可用 → 返回不可持久化错误 return { status: error, message: 提交服务暂时不可用请稍后重试 } # 保存提交记录到数据库 result self.database_cb.call( lambda: {saved: True, submission_id: submission_id}, fallback_value{saved: False} ) if not result[saved]: return {status: error, message: 保存失败} # 提交到评测队列有熔断保护 return self.judge_worker_cb.call( lambda: {status: accepted, submission_id: submission_id}, fallback_valueDegradationStrategy.judge_result_degradation( submission_id ) ) def get_recommendations(self, user_id: str, difficulty: float) - list: 获取推荐熔断后降级为热门题 return self.recommendation_cb.call( lambda: [{id: 101, score: 0.95}], # 真实推荐结果 fallback_valueDegradationStrategy.recommendation_degradation( user_id, difficulty ) ) def get_all_states(self) - dict: 获取所有服务的熔断器状态用于 Dashboard return { judge_worker: self.judge_worker_cb.get_state(), recommendation: self.recommendation_cb.get_state(), database: self.database_cb.get_state() }五、总结熔断和降级是防御不是修复数据表明加入熔断器后Judge Worker 宕机时 API Gateway 的 P99 延迟从 30s超时降低到 5ms内存判断健康的服务不再被级联拖垮。但熔断器不能滥用每个降级策略都在牺牲用户体验换取系统稳定性。过度降级会让系统退化为一堆静态占位符用户感受和宕机无异。最佳实践为每个服务设置SLO服务等级目标如评测服务可用性 99.5%。如果熔断器频繁触发导致 SLO 不达标说明问题不在于要不要熔断而在于底层服务为什么这么不稳定——熔断器是防护墙不是修复方案。