stress: AIMD 控制器 v5 —— 可用率约束 98%→99%(用户 2026-11-02)+ 低速恢复锁修复(爬坡备选条件「验证码≤1且短窗≥98.5%」,修 dp6 实测 05:00-06:10 滞留 70min 于 24-42/min);附 dp6 24h 实测优化报告(104,440 请求 98.11% 72.6/min,+29% 吞吐)

This commit is contained in:
bigcode
2026-11-02 11:03:47 +08:00
parent 329d0e4c5d
commit a2e1f0d923
2 changed files with 174 additions and 26 deletions
+32 -26
View File
@@ -1,22 +1,24 @@
/** # 脚本说明
* SERP 双路径(fetch+浏览器)24h 自适应吞吐优化测试(用户 2026-10-30 目标):
* 在保证客户端成功率 >98% 的约束下,自适应逼近系统可持续的最大每分钟请求数。
* SERP 双路径(fetch+浏览器)24h 自适应吞吐优化测试(用户 2026-10-30 目标;
* 2026-11-02 用户要求可用率约束上调至 >99%):
* 在保证客户端成功率 >99% 的约束下,自适应逼近系统可持续的最大每分钟请求数。
* 与 serp-profile-capacity.ts(单号 sticky+fetch,绕过池/令牌)不同,本脚本走生产
* 双路径原形态(runtime=auto,无 sticky,全池 LRU 选号):
* 双路径原形态(runtime=auto,无 sticky,全池公平负载选号):
* GET /api/serp/json/search?q=...&num=10
* 令牌内走 go-fetch(全局限流 fetchPerSec),限流外并发走温池浏览器 —— 组合即生产形态。
* AIMD 控制器 v4(每 60s 决策;dp5 全程实测教训驱动):
* 硬约束(成功率即约束本身):首次跌破 98%(失败≥2)×0.7、<95% ×0.5;退化段内续砍
* 需短窗 <95% 且全程增量新失败 ≥2 —— dp5 教训:10:24-10:28 同一波旧失败被短窗
* 反复引用,五分钟连砍五刀 175→30/min,直接打灭浏览器溢出路径;
* AIMD 控制器 v5(dp6 全天实测教训驱动;v4 基础上改四点):
* 硬约束(成功率即约束本身):首次跌破 99%(失败≥2)×0.7、<96% ×0.5;退化段内续砍
* 需短窗 <96% 且全程增量新失败 ≥2 —— dp5 教训:同一波旧失败被短窗反复引用,
* 五分钟连砍五刀 175→30/min,直接打灭浏览器溢出路径;
* 软信号按事件段(episode)迟滞 + 绝对数护栏(低速率下 1~2 个噪音 = 高比率,不得触发):
* 验证码 10min 窗 ≥3 个且占比>2% ×0.5、≥3 个且>0.5% ×0.7;门壳 ≥3 且占比>15% ×0.85;
* 池空>5 ×0.85;软砍下限 SOFT_FLOOR=25 —— dp5 教训:背景验证码供给恒定,速率越低
* 比率越高,比率规则在低位永真,12:00-17:00 自 52/min 自残至 3/min 而温池 150
* ready 全程空转(poolEmpty 20h=0);
* 干净爬坡:不在退化段 且 短窗成功率≥99.5% 且门壳<2% 且无池空 且(验证码 0 个或
* 样本 ≥300)→ 深跌后向「干净水位」(30min 窗 ≥98.5% 时的最高速率)以 ×1.3/拍
* 快速回收,水位之上每 5 拍 ×1.08(dp5 教训:×1.08/5 拍从 3/min 爬回需 3 小时)。
* 池空>5 ×0.85;软砍下限 SOFT_FLOOR=25 —— 背景验证码供给恒定,速率越低比率越高,
* 比率规则在低位永真(dp5 实测 12:00-17:00 自 52/min 自残至 3/min);
* 干净爬坡:不在退化段 且 短窗干净 且 无池空 且(验证码 0 个或样本 ≥300)→ 向「干净
* 水位」(30min 窗 ≥99.5% 时的最高速率)以 ×1.3/拍快速回收,水位之上每 5 拍 ×1.08。
* v5 修 dp6 实测「低速恢复锁死」:短窗成功率 ≥99.5% 在低速涓流下永不可达
* (05:00-06:10 滞留 70min 在 24-42/min,该小时实际 98.86%)→ 增加备选条件
* 「验证码 ≤1 个且短窗 ≥98.5%」(稀释论:同量涓流在高速率下占比可忽略,可爬)。
* 速率区间 [3, 400] req/min;并发 worker 默认 12(真实并发让溢出自然流向浏览器路径)。
* 判定(客户端,JSON 出口):ok = 200 且 organic>0;gate = 200 且 organic=0 且
* attempts 含 gate;empty = 200 且 organic=0 无门壳尝试;错误按响应 code 分类
@@ -140,17 +142,18 @@ function sumWindow(n = WINDOW_MIN): Counter {
let captchaWave = false;
let gateWave = false;
let poolEmptyWave = false;
/** # 成员变量 硬约束退化段(进段砍一次;段内连砍需增量新失败;30min 窗恢复≥98% 退段) **/
/** # 成员变量 硬约束退化段(进段砍一次;段内连砍需增量新失败;30min 窗恢复≥99% 退段) **/
let degraded = false;
/** # 成员变量 上次硬砍时的全程累计失败数(段内连砍要求全程新失败 ≥2,dp5 教训:同一波旧失败被短窗反复引用,10:24-10:28 五分钟连砍五刀 175→30/min) **/
let failsAtLastCut = 0;
/** # 成员变量 干净水位:30min 窗 ≥98.5% 且短窗 ≥99% 时达到过的最高速率(深跌后向水位快速回收;dp5 教训:×1.08/5 拍从 3/min 爬回需 3 小时,温池 150 ready 全程空转) **/
/** # 成员变量 干净水位:30min 窗 ≥99.5% 且短窗 ≥99% 时达到过的最高速率(深跌后向水位快速回收;dp5 教训:×1.08/5 拍从 3/min 爬回需 3 小时,温池 150 ready 全程空转) **/
let maxCleanRate = 0;
/** # 成员变量 软砍速率下限(req/min):背景验证码供给恒定,速率越低比率越高,比率规则在低位永真 → 钉死地板(dp5 实测 12:00-17:00 自 52/min 自残至 3/min)。软规则不砍破此位;真退化由硬规则(无下限)兜底 **/
const SOFT_FLOOR = 25;
/** # 函数
* 控制器决策(每 60s 调用):按窗口成功率/内部压力调整速率,返回决策记录或 null。
* v5(dp6 教训):目标上调 99%;爬坡备选条件「验证码 ≤1 且短窗 ≥98.5%」修低速恢复锁死。
* v4(dp5 教训):①硬砍连砍需「增量新失败」(全程失败数较上次砍时新增 ≥2),同一波旧失败不再反复引用;
* ②软信号加绝对数护栏(10min 窗内验证码 ≥3 / 门壳 ≥3 才允许比率规则),低速率噪音不再触发;
* ③软砍下限 SOFT_FLOOR,消除比率陷阱钉死地板;④爬坡:向干净水位快速回收 ×1.3/拍,水位之上 ×1.08/5 拍。
@@ -167,20 +170,20 @@ function controlTick(): Record<string, unknown> | null {
const from = rate;
let to = from;
let reason = "hold";
// 硬约束:首次跌破 98% 即砍;段内续砍需「进行中」证据 —— 短窗 <95%(比入段门槛更强的
// 硬约束:首次跌破 99% 即砍;段内续砍需「进行中」证据 —— 短窗 <96%(比入段门槛更强的
// 活跃失败信号,波尾噪声不算)且全程增量新失败 ≥2(高速率下 2 个新失败转瞬即攒,单靠
// 增量计数仍会逐分钟级联,dp5 教训:10:24-10:28 五分钟连砍五刀 175→30/min 打灭浏览器溢出)
const fails = tot - wH.ok;
const allFails = totals.sent - totals.ok;
const successS = wS.sent > 0 ? wS.ok / wS.sent : 1;
if (success >= 0.98) degraded = false;
const entry = !degraded && success < 0.98 && fails >= 2;
const ongoing = degraded && wS.sent >= 10 && successS < 0.95 && allFails - failsAtLastCut >= 2;
if (success >= 0.99) degraded = false;
const entry = !degraded && success < 0.99 && fails >= 2;
const ongoing = degraded && wS.sent >= 10 && successS < 0.96 && allFails - failsAtLastCut >= 2;
if (entry || ongoing) {
degraded = true;
failsAtLastCut = allFails;
to = success < 0.95 ? from * 0.5 : from * 0.7;
reason = `success=${(success * 100).toFixed(1)}%<${success < 0.95 ? 95 : 98}%(失败 ${fails}${entry ? "" : ";短窗<95%+新失败≥2"}) ×${success < 0.95 ? 0.5 : 0.7}`;
to = success < 0.96 ? from * 0.5 : from * 0.7;
reason = `success=${(success * 100).toFixed(1)}%<${success < 0.96 ? 96 : 99}%(失败 ${fails}${entry ? "" : ";短窗<96%+新失败≥2"}) ×${success < 0.96 ? 0.5 : 0.7}`;
} else {
// 软信号:事件段迟滞 + 绝对数护栏(低速率下 1~2 个噪音即高比率,不得触发;dp5 教训)
if (wS.captcha >= 3 && capPctS > 0.005 && !captchaWave) {
@@ -200,12 +203,15 @@ function controlTick(): Record<string, unknown> | null {
reason = `池空 ${wS.poolEmpty} ×0.85`;
} else if (wS.poolEmpty === 0) poolEmptyWave = false;
}
// 干净水位记账:30min 窗 ≥98.5% 且短窗 ≥99% 时的速率即为「实测可行」
if (success >= 0.985 && successS >= 0.99 && wS.sent >= 10) maxCleanRate = Math.max(maxCleanRate, from);
// 干净爬坡:退化段中不爬(30min 窗未恢复 98%);深跌后向水位快速回收 ×1.3/拍,水位之上 ×1.08/5 拍;
// 爬坡验证码条件:短窗 0 个,或样本 ≥300 时比率已无意义(绝对护栏统一交给软砍规则)
// 干净水位记账:30min 窗 ≥99.5% 且短窗 ≥99% 时的速率即为「实测可行」
if (success >= 0.995 && successS >= 0.99 && wS.sent >= 10) maxCleanRate = Math.max(maxCleanRate, from);
// 干净爬坡:退化段中不爬(30min 窗未恢复 99%);深跌后向水位快速回收 ×1.3/拍,水位之上 ×1.08/5 拍;
// 爬坡验证码条件:主条件短窗成功率 ≥99.5%,备选「验证码 ≤1 且短窗 ≥98.5%」修 dp6 实测
// 低速恢复锁死(涓流 1~2 个验证码在低速小分母下永远压住 99.5%,高速率下占比可忽略);
// 短窗 0 个验证码恒可爬,或样本 ≥300 时比率已无意义(绝对护栏统一交给软砍规则)
if (to === from && !degraded && wS.sent >= 10) {
if (successS >= 0.995 && gateRatioS < 0.02 && wS.poolEmpty === 0 && (wS.captcha === 0 || wS.sent >= 300)) {
const cleanS = successS >= 0.995 || (wS.captcha <= 1 && successS >= 0.985);
if (cleanS && gateRatioS < 0.02 && wS.poolEmpty === 0 && (wS.captcha === 0 || wS.sent >= 300)) {
rampSeq++;
if (maxCleanRate > 0 && from < maxCleanRate * 0.75) {
to = from * 1.3 + 0.2;