diff --git a/research-wiki/designs/2026-07-21-m25-resilience-design.md b/research-wiki/designs/2026-07-21-m25-resilience-design.md new file mode 100644 index 0000000..3135a6f --- /dev/null +++ b/research-wiki/designs/2026-07-21-m25-resilience-design.md @@ -0,0 +1,123 @@ +# M2.5 治理韧性设计: 半死源隔离与健康感知调度 + +- **日期**: 2026-07-21;**状态**: 已批准(用户 2026-07-21 明示豁免人类门并授权全权实施:"不需要给我过目了…把成功率提升到 98% 以上") +- **验收目标(用户定)**: P6 压测场景**一字不改**(五源池含坏 key/黑洞/紧闸/紧看门狗,`.env` SOAK scope 不动、不删坏源、不调 `LLM_MAX_RETRIES` 等参数放水),最终调用成功率 **≥98%**,业务层零重试;允许多轮迭代。 +- **输入**: findings/2026-07-21-p6-soak-baseline.md(P6 首跑 58.1%)+ 六病灶数据核实(本文 §1)+ 业界调研(Envoy/Resilience4j/Finagle/Google SRE/gRPC/LiteLLM,调研报告要点内嵌 §3)。 + +## 1. 病灶(P6 遥测逐条核实,证据链完整) + +| # | 病灶 | 位置 | 定量证据 | +|---|---|---|---| +| 1 | 熔断"连续失败、成功清零"语义对高失败率源失明 | `backends/*/breaker.py` record_success→fails=0 | 源5(成功率 10%)全程未开路,吃掉 14455 次尝试中的 **11070 次(76%)**;对照黑洞源(0%)20 分钟内被压至每 10 分钟 ≤2 次 | +| 2 | 阈值自动抬升 = max(配置, **全局**并发×2) | `config.py:219` | SOAK 全局并发 100 → 阈值 200,熔断进一步失灵(M2 设计错误,本设计废除) | +| 3 | 轮询空洞漏斗: 游标落在被跳过源(开路/闸满)时流量全漏给序列中下一个活源 | `sources.py` RoundRobin + `retry.py:_pick_runnable` | 游标 2/3/4/5 位全落源5,预测 4:1,实测 11070:2892=3.83:1 | +| 4 | 失败源不进冷却(冷却备忘只服务熔断开路) | `retry.py:186` | 同一调用可背靠背重撞同一坏源 | +| 5 | 重试预算 3 次与源池结构无关 | `retry.py:148` | 3318 个调用(41.5%)耗尽预算且尝试链从未含健康源 | +| 6 | 同 key 源互相触发账号级 429 | 配置现实(源1/4/5 同 key) | 对源5 的 1.1 万次冲撞使健康源1 单次成功率被拖至 43%(996 次 429) | + +反事实: 8000 调用 = 2373 缓存 + ~5600 真实(75 次/分 ≪ RPM 600),健康源足以承载 → 98%+ 天花板存在。 + +## 2. 备选方案对比 + +| 方案 | 内容 | 预估成功率 | 权衡 | +|---|---|---|---| +| A 最小侵入 | 只改熔断: 滑动窗失败率 + 开路时长指数递增 | 90-96% | 改动最小;但轮询空洞仍在——源5 每次恢复期照旧吸走 4/5 流量直到再次攒满样本开路,flap 损耗大,收敛慢且不稳,大概率够不到 98% | +| **B 三件套(推荐)** | A + 本地健康感知选源(P2C×成功率×在途)+ 调用内失败降权 | 98-99.5% | 熔断(慢反馈、硬隔离)与健康选源(快反馈、软降权)双层互补: 选源在熔断攒样本期间就把流量撤走,熔断兜底硬隔离;三改动全部进程本地、零新依赖、零端口签名变更 | +| C 全家桶 | B + 账号级 429 共享退避(LiteLLM 式)+ SRE 10% 重试预算 + AIMD 自适应并发 | ≈B | 生产最强,但三个增项在本场景无增量收益(路由修好后账号 429 风暴自然消失、重试量自然回落);复杂度/Redis 状态面扩大、审查面扩大。**列为后续演进**,本轮不做(YAGNI) | + +**选 B。否决 A**: 数据表明选源空洞是独立放大器,单修熔断留下 flap 窗口期漏斗,98% 不稳。**否决 C**: 三增项各自成立但对本验收目标零边际,违反反 gold-plating;设计预留其接入点即可。 + +## 3. 方案 B 详设 + +### 3.1 B1 熔断语义升级(双通道 + 开路退避) + +蓝本: Resilience4j 滑动窗(failureRate + minimumNumberOfCalls)+ Envoy 驱逐时长指数递增。**声明: 此为对 CHS 连续失败语义的有意偏离**(CHS 1-2 源场景够用,五源池失明,见 §1 病灶 1);连续失败通道保留,CHS 迁移行为向后兼容。 + +**判据(record_failure 原子执行)**——满足任一即 OPEN: +1. **连续失败通道(CHS 兼容)**: 连续失败数 ≥ `fail_threshold`(配置键不变;**废除全局并发×2 自动抬升**,`config.py:219` 删除,改按**源级**并发抬升 `max(配置, source.max_concurrency×2)`——防误熔的本意保留,M2 的错在用了全局并发); +2. **失败率通道(新)**: 窗口内样本 ≥ `min_calls` 且 失败率 ≥ `fail_rate`。 + +**429 不入熔断(独立审查 C1/I2 的化解,Envoy outlier detection 同款)**: `rate_limited`(上游 429)既不计入连续失败也不入窗口——限速是背压不是源故障,网关抖动/账号级 429 脉冲不应熔断健康源(P6 基线: 健康源风暴期失败率 57%,与 0.6 阈值仅差 3 点,若计入必误熔);429 仍计入健康 EWMA 与调用内降权(选源层软处理)。timeout/network/5xx 正常入两通道。**连续通道触发的开路不递增 `reopen_streak`**(误熔代价封顶为单次 cooldown_s;只有率通道开路与探针失败重开递增)。补对抗性契约测试: 健康源 429 脉冲不开路、timeout 脉冲开路但 60s 自愈。 + +**窗口实现**: 固定 30s 桶 × 2(当前桶+上一桶合并读),每桶 `attempts/failures` 计数——服务器钟窗口 id 与限流器同构;Redis 侧并入现有 record_* Lua(gate HASH **固定域轮换**: `win_id/a0/f0/a1/f1`,写前按 win_id 轮换清零,O(1) 无扫描无陈旧桶,独立审查 M1),**不新增 key、不引入环形数组**。memory 侧同构双桶。 + +**开路时长指数递增(Envoy)**: `cooldown_eff = cooldown_s × 2^(reopen_streak-1)`,封顶 `max_cooldown_s`;`reopen_streak` 存 gate HASH,半开探针成功转 CLOSED 时**不清零**,由"持续 CLOSED 满 `2×cooldown_eff` 后的下一次 record_success"衰减归零(防 flap 抖动重置;`closed_since` 域仅在探针转 CLOSED 时写入,普通成功不刷新)。 + +**窗口/EWMA 口径(独立审查 I1 定稿)**: ResultInvalid 与"网关健康拒坏请求"(RequestRejected 且 provider_responded)**既不入 attempts 也不入 failures**,且不喂 `record_outcome`(坏结果/坏请求 ≠ 坏服务);真实成功入 attempts(否则率通道退化为纯失败计数)。 + +**状态机域清单(双后端契约的共同规格,独立审查 I4)**: + +| 域 | 写入时机 | 清除/重置时机 | +|---|---|---| +| `state/epoch/open_until/probe_*` | 既有五操作语义**逐字不动**(epoch 仅开断+1) | 不变 | +| `failures`(连续) | record_failure(429 除外)+1,在 fencing matches 分支内 | record_success 置 0(仅此域;**不得顺手清窗口/streak 域**) | +| `win_id/a0/f0/a1/f1` | record_success(真实成功)与 record_failure(429/ResultInvalid 除外),fencing matches 分支内 | 写前 win_id 轮换清零对应桶 | +| `reopen_streak` | 率通道开路/探针失败重开时 +1 | closed_since 满 2×cooldown_eff 后首次 record_success 置 0 | +| `closed_since` | 探针成功转 CLOSED 时写 now | 开路时删除 | + +率通道开路时 `failure_count` 沿用既有顶格语义(置 threshold,迁移文档标注)。 + +**半开/探针/epoch fencing 全部不动**(单探针租约 + epoch 仅开断+1 + release_probe 置 open_until=now,M2 已验收)。SourceDead force_open、探针租约惰性重发不动。 + +**新配置键(全部可选,库缺省;沿用 `{SCOPE}__BREAKER__*` 族)**: + +| 键 | 缺省 | 说明 | +|---|---|---| +| `BREAKER__MIN_CALLS` | 10 | 失败率通道最小样本(防低流量误判,Resilience4j 语义) | +| `BREAKER__FAIL_RATE` | 0.6 | 窗口失败率阈值(比业界 50% 宽,容忍 LLM 网关抖动) | +| `BREAKER__WINDOW_S` | 60 | 失败率窗口(双 30s 桶) | +| `BREAKER__MAX_COOLDOWN_S` | max(300, cooldown) | 开路退避封顶 | + +缺省合法性: 韧性参数缺省先例已有(probe_ttl 派生、jitter 系数);关键配置(阈值/冷却)仍必填不变。 + +### 3.2 B2 健康感知选源(进程本地,新默认) + +蓝本: Envoy least-request 权重公式 + gRPC WRR"权重×成功率"+ Envoy slow-start。健康记忆**每进程本地**(业界共识: Envoy/Finagle/gRPC 全本地;共享仅在单进程流量极低时划算——本库多 worker 各自流量充足)。 + +新增 `HealthAwareSelector`(实现既有 `SourceSelector` 端口,`order()` 签名不变): +- **健康分**: `score = max(ewma_success, 0.05) × 1 / (1 + inflight)`。`ewma_success` 为源级成功率 EWMA(α=0.2,初始 1.0 乐观起步);`inflight` 来自现有 `stats`。**0.05 地板 = 探索项**(独立审查 I5 定稿): 被打压源保有微量被选概率,恢复后靠真实成功让 EWMA 自然爬升(α=0.2 → ~10 次成功回到 0.9)——**EWMA 爬升本身就是 slow-start**,不再做独立的爬升机制,也消除"选源器感知不到冷却过期"的接线缺口;真坏源的探索流量在 try_enter 处被开路熔断拦下(零真实成本)。 +- **排序**: P2C——随机取两源比 score,高者列首,其余按 score 降序补全(`order()` 返回全序列,`_pick_runnable` 逐个试准入的既有逻辑不变,被跳过时自然落到次优)。已知取舍(独立审查 M5): 仅头名随机化,尾序确定,多 worker 溢出会集中到同一次优源——SOAK 规模可接受,记录备查。 +- **喂数**: `ports.py` 新增可选 `@runtime_checkable OutcomeAwareSelector` Protocol(仅 `record_outcome(source_name: str, ok: bool) -> None`);RetryMW 构造时 isinstance 判定一次并缓存(独立审查 I3,显式于 getattr 私约)。`_attempt` 真实成功/Transient/SourceDead/429 处喂数;ResultInvalid/健康拒绝不喂(§3.1 口径)。round_robin/least_inflight 两实现不动。 +- `LLM__SELECTOR` 新增值 `health_aware` 并设为**新缺省**(生产级默认;显式配 round_robin/least_inflight 者行为不变——迁移文档标注;`config.py` `_SELECTORS` 与 `.env.example` 注释同步更新)。 + +### 3.3 B3 调用内失败降权(替代硬排除) + +蓝本: Envoy previous_hosts 语义,但按 §1 病灶分析**改软降权**——硬排除在异构池会把"健康源偶发失败"的重试强推给已知坏源(计算: 硬排除下 1→5→1 链成功率 98.7%,软降权下 1→1→1 链 99.8%)。 + +实现(选源器无关,不触碰端口签名): `attempt_fails: dict[str, int]` 为 `__call__` **局部变量**逐层传参给 `_pick_runnable`(RetryMW 实例被并发调用共享,严禁实例属性,独立审查 M2);`_pick_runnable` 拿到 `order()` 结果后,把**本调用内已失败 ≥2 次**的源移到序列尾部(存在其他候选时)。效果: 健康源失败 1 次仍是首选(429/空补全属瞬态,原地退避重试最优);失败 2 次让位给次优源试一次;全部候选都失败过则按原序回退——自适应而非教条,调用结束状态即弃。病灶 5(预算与池结构无关)由此**间接闭环**: 预算数值不动,但尝试链质量由健康排序+降权保证(推演: 稳态下 3 次尝试大概率全落健康源,1−0.12³ ≈ 99.8%)。 + +### 3.4 不做与预留(方案 C 组件的接入点) + +- 账号级 429 共享退避: 不做;预留 = 冷却备忘 key 从 source_name 换 account_key 即可接入(LiteLLM 先例,治理粒度=配额粒度原则记入 ARCHITECTURE)。 +- SRE 10% 重试预算 / AIMD 自适应并发 / hedged requests / TTFB 慢调用计熔断: 不做,理由见 §2 方案 C;hedged 对 LLM 有 token 计费与 429 放大问题,默认永不启用。 +- 熔断配额(max_ejection_percent): 不做——全部源真实坏死时 fail-fast 抛 `CircuitOpenError`(带 retry_after)正是库对下游的既有承诺,放行反而违反"熔断不可用报错"铁律的精神。 + +## 4. 非功能维度(brainstorming 清单) + +- **并发与取消**: 全部新状态(EWMA/调用内降权/双桶计数)为 asyncio 单循环内存操作或 Lua 原子操作,无锁需求;新代码不引入 await 点于取消敏感路径,`CancelledError` 穿透路径不变。 +- **降级方向**: 熔断后端不可用 → 准入侧 fail-closed 报错、记账侧 `_record_quietly` 降级——**全部不变**;健康记忆纯本地无后端,不存在降级问题;`record_outcome` 任何异常吞并 warning(选源优化失效退化为轮询序,不影响正确性)。 +- **幂等与重复**: record_failure 双桶 HINCRBY 天然可重入;epoch fencing 防跨代写回不变;reopen_streak 衰减判据基于时刻比较,重复执行安全。 +- **持久化与原子性**: Redis 侧窗口计数并入既有 gate HASH 与既有 Lua 脚本(单脚本原子);HASH 随既有 TTL 过期,无新清理负担。 +- **纯 asyncio 中立**: 选源器健康态属 client 实例(构造注入),无全局/模块级状态;同一 GatewayClient 在 arq 与裸脚本行为一致。 + +## 5. 错误处理与测试策略 + +- 失败分类不变(四分类驱动);新逻辑只消费分类结果。 +- **契约测试(双后端)新增**(全部注入**确定性失败序列**,不做概率断言,独立审查 M3): ① 高失败率序列在 min_calls 样本处开路(病灶 1 回归);② 样本 < min_calls 永不率开路;③ 连续失败通道阈值=配置值且按源级并发抬升(全局抬升废除回归);④ 开路退避 2^n 递增且封顶、CLOSED 稳定期后衰减(redis 真实等待变体用小 `MAX_COOLDOWN_S` 配置防 slow 套件超时);⑤ ResultInvalid/健康拒绝不入窗口(attempts/failures 均不入);⑥ 429 脉冲不开路、timeout 脉冲开路且不增 streak(C1 对抗测试);⑦ 率通道开路 failure_count 顶格。 +- **选源单测**: EWMA 收敛、P2C 分布(注入 rng)、slow-start 爬升、`record_outcome` 缺失方法兼容、调用内降权重排。 +- **RetryMW 单测**: 健康源单失败仍首选、双失败让位、reasons 语义不变。 +- **端到端回归(验收门)**: 原封不动 P6 场景重跑(相同 .env、相同预算 8000、相同并发),成功率 ≥98%;八不变量全 PASS;对照首跑基线出对比报告。不达标按数据迭代(用户已授权多轮)。 + +## 6. 旧版行为审计(重写熔断判据部分) + +| 旧行为 | 处置 | +|---|---| +| 连续失败 ≥ 阈值开路 | **保留**(通道 1) | +| 成功清零连续失败计数 | **保留**(仅作用于通道 1 计数;窗口计数不清) | +| 阈值自动抬升 max(配置, 并发×2) | **有意废除**(病灶 2;M2 设计错误,迁移文档更新) | +| SourceDead force_open / 探针租约 / epoch fencing / release_probe / 半开单探针 | **保留**(逐字不动) | +| ResultInvalid 记成功 | **保留**(且不入窗口失败) | +| 开路冷却固定 cooldown_s | **替换**为指数递增(初值=cooldown_s,兼容: 首次开路时长不变) | +| RoundRobin 为缺省选源 | **替换**缺省为 health_aware;显式配置者不变 | +| retry_after_s 上限 = cooldown_s | **变更**为 max_cooldown_s(缺省 300s)——CHS arq defer 延期时长随之最多放大 5 倍,迁移文档(chsanalyzer G1)标注 | +| .env.example 注释"阈值自动取 max(此值,并发×2)" | 同步改为源级并发口径(交付清单项) | diff --git a/research-wiki/designs/m25-resilience.md b/research-wiki/designs/m25-resilience.md new file mode 100644 index 0000000..8582bf5 --- /dev/null +++ b/research-wiki/designs/m25-resilience.md @@ -0,0 +1,9 @@ +--- +type: design +node_id: design:m25-resilience +title: "M2.5 治理韧性: 半死源隔离与健康感知调度" +date: 2026-07-21 +--- + +# M2.5 治理韧性: 半死源隔离与健康感知调度 + diff --git a/research-wiki/graph/edges.json b/research-wiki/graph/edges.json index 508cb91..4650020 100644 --- a/research-wiki/graph/edges.json +++ b/research-wiki/graph/edges.json @@ -40,6 +40,11 @@ "id": "finding:p6-soak-baseline", "label": "P6 混合浸泡首跑基线与记分板三重伪击穿修复", "type": "finding" + }, + { + "id": "design:m25-resilience", + "label": "M2.5 治理韧性: 半死源隔离与健康感知调度", + "type": "design" } ], "links": [ diff --git a/research-wiki/index.md b/research-wiki/index.md index e8e6c32..d97058b 100644 --- a/research-wiki/index.md +++ b/research-wiki/index.md @@ -1,12 +1,14 @@ # Research Wiki 索引 -> 自动生成,更新时间:2026-07-21 11:19 UTC +> 自动生成,更新时间:2026-07-21 12:26 UTC -## design (4) +## design (6) - [2026-07-20-m1-core-design](designs/2026-07-20-m1-core-design.md) `design:2026-07-20-m1-core-design` - [2026-07-20-m2-distributed-design](designs/2026-07-20-m2-distributed-design.md) `design:2026-07-20-m2-distributed-design` +- [2026-07-21-m25-resilience-design](designs/2026-07-21-m25-resilience-design.md) `design:2026-07-21-m25-resilience-design` - [M1 核心里程碑设计:公共签名冻结与治理栈落地](designs/m1-core-design.md) `design:m1-core-design` - [M2 分布式:Redis 治理后端+背压+Postgres 遥测+pricing+Embedding+压测 harness](designs/m2-distributed.md) `design:m2-distributed` +- [M2.5 治理韧性: 半死源隔离与健康感知调度](designs/m25-resilience.md) `design:m25-resilience` ## finding (5) - [2026-07-20-m2-soak-workload](findings/2026-07-20-m2-soak-workload.md) `finding:2026-07-20-m2-soak-workload` diff --git a/research-wiki/log.md b/research-wiki/log.md index 4b92fba..75e7aa4 100644 --- a/research-wiki/log.md +++ b/research-wiki/log.md @@ -21,3 +21,5 @@ - [2026-07-21 06:00 UTC] 重建索引: 13 篇页面 - [2026-07-21 11:19 UTC] 新增 finding: P6 混合浸泡首跑基线与记分板三重伪击穿修复 (finding:p6-soak-baseline) - [2026-07-21 11:19 UTC] 重建索引: 15 篇页面 +- [2026-07-21 12:26 UTC] 新增 design: M2.5 治理韧性: 半死源隔离与健康感知调度 (design:m25-resilience) +- [2026-07-21 12:26 UTC] 重建索引: 17 篇页面