CC Switch 熔断器:三个状态、四个阈值,以及代码里同时存在的 4 和 5
如果你去 cc-switch 仓库里搜「熔断阈值默认是多少」,会拿到不止一个答案。src-tauri/src/proxy/circuit_breaker.rs:66 写的是 failure_threshold: 4,而 src-tauri/src/proxy/provider_router.rs:135-138 在读配置失败时兜底填的是 5。这两处都在同一个仓库、同一次提交里,我们把它当成一个可核实的事实记录下来,不去猜哪一处「才算数」。
本文全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10,仓库内版本号 3.19.2)。我们只读源码与文档文本,没有安装也没有运行过这个桌面应用,下面出现的所有数值都是源码里的默认配置,不是对运行结果的任何保证。
先把三个状态和五条边钉在行号上
熔断器的状态枚举在 circuit_breaker.rs:14-23,三态 Closed / Open / HalfOpen,序列化成 snake_case,所以你在别处看到 half_open 这种写法时它指的就是这一个。
真正决定行为的是状态之间的转移条件,能落到具体行号的一共五条,每一条都在同一个文件里:
| 转移 | 触发条件 | 位置 |
|---|---|---|
| Closed → Open(路径一) | 连续失败数 failures >= failure_threshold | circuit_breaker.rs:257-265 |
| Closed → Open(路径二) | total_requests >= min_requests 且 failed/total >= error_rate_threshold | circuit_breaker.rs:266-283 |
| Open → HalfOpen | 距 last_opened_at 已经 >= timeout_seconds | circuit_breaker.rs:139-152、165-190 |
| HalfOpen → Closed | 半开态连续成功数 >= success_threshold | circuit_breaker.rs:215-225 |
| HalfOpen → Open | 半开态一次失败立刻转 Open | circuit_breaker.rs:248-256 |
另外还有一条「手动重置」,我们只在日志码表里见到它(CB-006),它具体把状态置成什么、从哪些状态起算,我们没有核实,所以不放进上面这张表。
这张表最值得看的是两处不对称。
第一处,Closed → Open 有两条独立路径。连续失败是一条,错误率是另一条,后者还带一个 min_requests 门槛,意思是请求总数不到这个数就根本不算错误率。所以「四个阈值」严格说是四个计数或比例类参数(failure_threshold、success_threshold、error_rate_threshold、min_requests)加一个时间参数 timeout_seconds,五个字段的默认值一起写在 circuit_breaker.rs:63-73:4 / 2 / 60 / 0.6 / 10。
第二处,进出半开态的严格程度差很多。要从 HalfOpen 回到 Closed,需要连续成功 success_threshold 次;而 HalfOpen 里只要失败一次就直接被打回 Open。再叠加一个细节:半开态的并发探测名额在 circuit_breaker.rs:315-333 里写死为 max_half_open_requests = 1u32,不是可配置项。超额的请求会把计数回退掉然后被拒绝。也就是说半开期同一时刻只放一个真实请求进去试,这一个的成败几乎就决定了下一步走向。
还有两个容易被忽略的实现细节。transition_to_open 只重置连续失败/成功计数并记录 last_opened_at(circuit_breaker.rs:359-364),而 transition_to_closed 会额外把 total_requests 与 failed_requests 清零(circuit_breaker.rs:380-387)——错误率这条路径的分母是在回到 Closed 时才归零的。transition_to_half_open 带一个幂等守卫:状态不是 Open 就直接 return(circuit_breaker.rs:367-377),配套测试在 circuit_breaker.rs:453-475,目的是避免并发重复调用把 in-flight 的探测名额重置掉。这个文件自带 4 个 #[tokio::test](circuit_breaker.rs:401-495)。
同一个参数,四个位置四个值
现在说这篇真正想讲透的那一处。failure_threshold 这一个参数,在仓库里至少落在四个不同的位置上,取值并不统一:
| 位置 | 取值 | 什么时候生效 |
|---|---|---|
circuit_breaker.rs:66(CircuitBreakerConfig::default()) | 4 | 结构体默认构造 |
provider_router.rs:135-138(读配置失败的兜底分支) | 5 | record_result 读该应用熔断配置失败时 |
database/schema.rs:131-135(proxy_config 表列级 DEFAULT) | 4 | 建表后未显式赋值的行 |
database/schema.rs:152(claude 的 seed 行) | 8 | 初始化数据库时写入的 claude 那一行 |
第二行是最反直觉的:provider_router.rs 的 record_result 每次记录请求结果时,要先读该应用自己的熔断配置,而读失败的那个分支兜底填的是 5。这个 5 与 CircuitBreakerConfig::default() 的 4 不一致——两处都在代码里,只是触发条件不同:一个是构造配置结构体时的缺省,一个是读库失败时的降级路径。说完差异就停,我们不推断哪个是本意,也不拿它去评价这个项目。
第三、第四行则说明另一件事:代码默认值不等于你库里实际跑的值。proxy_config 表主键是 app_type,CHECK 约束只允许 'claude','codex','gemini','grokbuild' 四个值(schema.rs:126-127)。列级 DEFAULT 是那套通用值(max_retries 3、circuit_failure_threshold 4、circuit_success_threshold 2、circuit_timeout_seconds 60、circuit_error_rate_threshold 0.6、circuit_min_requests 10),但 seed 时四个应用各写了自己的一行:
- claude:
(6, 90, 180, 600, 8, 3, 90, 0.7, 15)(schema.rs:152,同值另见database/dao/proxy.rs:373) - codex:
(3, 60, 120, 600, 4, 2, 60, 0.6, 10)(schema.rs:161) - gemini:
(5, 60, 120, 600, 4, 2, 60, 0.6, 10)(schema.rs:170) - grokbuild:
(3, 60, 120, 600, 4, 2, 60, 0.6, 10)(schema.rs:179)
这几组数字的顺序是 max_retries、流式首字超时、流式静默超时、非流式总超时,然后才是五个熔断参数。所以对 Claude 这一行来说,实际写进库的熔断阈值是 8 / 3 / 90 / 0.7 / 15,跟代码里的 4 / 2 / 60 / 0.6 / 10 是两套数。用户手册 docs/user-manual/zh/4-proxy/4.3-failover.md:105-117 的熔断器配置表把这件事表述为「通用默认值」与「Claude 默认值」两列,并给了范围列;这一层与 seed 行是对得上的,对不上的是「代码里的那个 default」。
顺带记一条同方向的差异:4.3 的故障转移流程图(4.3-failover.md:86-92)画的顺序是「发送到当前供应商 → 失败 → 检查熔断状态」,而代码里熔断放行检查发生在发起请求之前(forwarder.rs:434-448)。候选是怎么挑出来的我们另有一篇专门讲,这里只标出图与代码在顺序上的差异,不展开。
怎么自己把这四个值核一遍
不用装应用,全是文本核对,三步:
grep -n "failure_threshold" src-tauri/src/proxy/circuit_breaker.rs
grep -n -A 8 "record_result" src-tauri/src/proxy/provider_router.rs
grep -n "circuit_failure_threshold\|INSERT INTO proxy_config" src-tauri/src/database/schema.rs
第一条把结构体默认值那一行拉出来,第二条把兜底 5 所在的读配置失败分支拉出来,第三条同时命中列级 DEFAULT 与四条 seed。比对的是同一个语义在这三个文件里各写了什么,不是去证明谁错。以上为按仓库中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准。
要判断你自己机器上跑的是哪一套,看的不是代码而是数据库:proxy_config 表里对应 app_type 的那一行 circuit_failure_threshold 才是路由层实际读到的值,代码里的 4 与 5 都只在特定路径下才有机会露面。这个数据库位于 CC Switch 的用户数据目录下(~/.cc-switch/),属于本机数据,同目录里还保存着 API Key 一类敏感内容,怎么处置请结合自身环境判断。
改这几个阈值会牵动什么
在事实卡有依据的范围内,只有下面这几条能说:
配置支持热更新,且热更新不重置状态(circuit_breaker.rs:120-123)。改阈值不会把一个已经 Open 的熔断器拽回 Closed,状态机该走的还得走。
调大 timeout_seconds 会同时拉长两个地方的等待:is_available()(circuit_breaker.rs:139-152)和 allow_request()(165-190)都用它判断 Open 是否该转半开,前者用于候选筛选阶段且明确不占用探测名额(circuit_breaker.rs:125-132),后者才是真正发起请求前拿放行许可的入口。
min_requests 会直接屏蔽错误率这条路径。总请求数没到这个门槛,错误率再高也不触发;而 total_requests 只在回到 Closed 时清零。
success_threshold 与写死的探测名额 1 是叠加关系。名额固定只有 1,success_threshold 调大意味着需要更多次连续成功的探测才回到 Closed,中间任何一次失败都会把状态打回 Open 并重新开始等 timeout_seconds。
至于「到底该调成多少」——取决于你的用法,项目没给通用值。仓库里给出的是四个应用各自的 seed 值,以及手册那张表里另附的一列取值范围(4.3-failover.md:105-117,具体端点请自己去表里看),没有任何依据支持从这些默认值推算「你会不会掉线」或者「切换会更快还是更慢」。
什么情况说明问题不在熔断器
这一节是排查时最省时间的部分,四条判据都能落到行号:
只配了一个供应商时,熔断器根本不参与。转发层在 forwarder.rs:412-444 里,providers.len() == 1 会整体绕过熔断检查。这种情况下请求一直失败,你去查熔断状态是查不出东西的。
故障转移关闭时同样跳过熔断检查。provider_router.rs:79-96 明写:故障转移关闭时只返回当前供应商一个,并且跳过熔断器检查。
连通性检查不会改变熔断状态。services/stream_check.rs:13-17 把这条写成了明确的不变量:连通性检查绝不触碰故障转移熔断器,熔断器只由 proxy/forwarder.rs 转发真实流量的成败被动驱动,命令层在 commands/stream_check.rs:1-4 又重申了一遍。所以「跑一次检查让它恢复」这条路在代码语义里是不成立的。这里还有一处文档与代码的口径差异,另有一篇专门讲。
尝试次数耗尽和全部熔断是两个不同的错误。max_attempts = max_retries + 1(saturating_add),且这个检查放在熔断放行之前,避免超限还占探测名额(forwarder.rs:143-148、217-219、423-432)。候选为空时也分两种:全部熔断报 AllProvidersCircuitOpen 并打 [FO-004],没有可用供应商报 NoProvidersConfigured 并打 [FO-005](provider_router.rs:98-106)。
对应到日志,熔断相关的六个码是这篇唯一需要的一小段(log_codes.rs:14-21):CB-001 Open→HalfOpen、CB-002 HalfOpen→Closed、CB-003 HalfOpen 探测失败、CB-004 连续失败触发、CB-005 错误率触发、CB-006 手动重置。整套 27 个错误码到 HTTP 状态码的映射另有一篇在讲,这里只引这六个,因为前五个恰好一一对应上面那张状态转移表(第六个是前面提过的手动重置)——你看到 CB-004 就知道是连续失败那条路径,看到 CB-005 就知道是错误率那条,两者需要检查的参数完全不同。
回到开头那个 4 与 5。它值得写一篇,不是因为差 1 有多大影响,而是因为它提醒了一件更普遍的事:在这个项目里,一个熔断参数的实际取值取决于代码默认值、数据库列级 DEFAULT、按应用 seed 的行、用户后来改过的值这四层里哪一层先命中。排查的时候,直接去看数据库那一行,比在源码里 grep 默认值靠谱。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、
src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。