From 329d0e4c5df2b312d62a275745fe2d015c373d36 Mon Sep 17 00:00:00 2001 From: bigcode Date: Sun, 1 Nov 2026 01:47:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=8B=E6=B5=8B:AIMD=20=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=99=A8=20v4(dp5=20=E5=85=A8=E5=A4=A9=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?=E6=95=99=E8=AE=AD:=E4=BD=8E=E9=80=9F=E7=8E=87=E6=AF=94?= =?UTF-8?q?=E7=8E=87=E9=99=B7=E9=98=B1=E4=B8=8E=E7=A1=AC=E7=A0=8D=E7=BA=A7?= =?UTF-8?q?=E8=81=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dp5 实测两类控制器自残(系统侧 20h poolEmpty=0,温池 150 ready 空转 7h): - 硬砍级联:同一波旧失败被短窗反复引用,10:24-10:28 五分钟连砍五刀 175→30/min,打灭浏览器溢出路径 → 续砍需短窗<95% 且增量新失败≥2 - 低速率比率陷阱:背景验证码供给恒定,速率越低比率越高,比率规则在低位 永真,12:00-17:00 自 52/min 自残至 3/min → 软信号加绝对数护栏 (验证码/门壳 ≥3 才允许比率规则)+ 软砍下限 SOFT_FLOOR=25 - 深跌恢复过慢:×1.08/5拍 从 3/min 爬回需 3 小时 → 引入干净水位, 向水位以 ×1.3/拍 快速回收,水位之上维持 ×1.08/5 拍 --- stress/serp-dualpath-optimize.ts | 75 ++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/stress/serp-dualpath-optimize.ts b/stress/serp-dualpath-optimize.ts index ee204a4..74c6a57 100644 --- a/stress/serp-dualpath-optimize.ts +++ b/stress/serp-dualpath-optimize.ts @@ -5,16 +5,18 @@ * 双路径原形态(runtime=auto,无 sticky,全池 LRU 选号): * GET /api/serp/json/search?q=...&num=10 * 令牌内走 go-fetch(全局限流 fetchPerSec),限流外并发走温池浏览器 —— 组合即生产形态。 - * AIMD 控制器(每 60s 决策;硬约束看 30min 窗,重复砍需 10min 短窗新证据,退段需 30min 窗恢复): - * 硬约束(成功率即约束本身):首次跌破 98%(失败≥2)×0.7、<95% ×0.5;退化段内 - * 连砍需短窗(10min)成功率也 <98% —— dp3 实测教训:开局 3 个失败在 30min 硬窗 - * 滞留导致每分钟连砍到下限(窗口滞后放大),迟滞后一段失败只砍一次; - * 软信号按事件段(episode)迟滞,每段波只砍一次(波退去才重置): - * 验证码占比>2% ×0.5、>0.5% ×0.7(10min 短窗;dp1 实测 0.5% 噪声时成功率 - * 仍有 99.3%,按占比计而非按次数计,且不逐分钟连砍钉死下限); - * 门壳尝试占比>15% ×0.85;池空>5 ×0.85; - * 干净爬坡:不在退化段 且 短窗成功率≥99.5% 且门壳<2% 且无池空且验证码≤0.3% - * (高压力下单 IP 逐个越限的常态噪声不阻止追涨)→ 每 5 拍 ×1.08。 + * AIMD 控制器 v4(每 60s 决策;dp5 全程实测教训驱动): + * 硬约束(成功率即约束本身):首次跌破 98%(失败≥2)×0.7、<95% ×0.5;退化段内续砍 + * 需短窗 <95% 且全程增量新失败 ≥2 —— dp5 教训:10:24-10:28 同一波旧失败被短窗 + * 反复引用,五分钟连砍五刀 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 小时)。 * 速率区间 [3, 400] req/min;并发 worker 默认 12(真实并发让溢出自然流向浏览器路径)。 * 判定(客户端,JSON 出口):ok = 200 且 organic>0;gate = 200 且 organic=0 且 * attempts 含 gate;empty = 200 且 organic=0 无门壳尝试;错误按响应 code 分类 @@ -138,11 +140,20 @@ function sumWindow(n = WINDOW_MIN): Counter { let captchaWave = false; let gateWave = false; let poolEmptyWave = false; -/** # 成员变量 硬约束退化段(进段砍一次;段内连砍需短窗新证据;30min 窗恢复≥98% 退段) **/ +/** # 成员变量 硬约束退化段(进段砍一次;段内连砍需增量新失败;30min 窗恢复≥98% 退段) **/ 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 全程空转) **/ +let maxCleanRate = 0; +/** # 成员变量 软砍速率下限(req/min):背景验证码供给恒定,速率越低比率越高,比率规则在低位永真 → 钉死地板(dp5 实测 12:00-17:00 自 52/min 自残至 3/min)。软规则不砍破此位;真退化由硬规则(无下限)兜底 **/ +const SOFT_FLOOR = 25; /** # 函数 * 控制器决策(每 60s 调用):按窗口成功率/内部压力调整速率,返回决策记录或 null。 + * v4(dp5 教训):①硬砍连砍需「增量新失败」(全程失败数较上次砍时新增 ≥2),同一波旧失败不再反复引用; + * ②软信号加绝对数护栏(10min 窗内验证码 ≥3 / 门壳 ≥3 才允许比率规则),低速率噪音不再触发; + * ③软砍下限 SOFT_FLOOR,消除比率陷阱钉死地板;④爬坡:向干净水位快速回收 ×1.3/拍,水位之上 ×1.08/5 拍。 */ function controlTick(): Record | null { const wH = sumWindow(30); // 硬约束窗口:成功率即约束本身 @@ -156,40 +167,50 @@ function controlTick(): Record | null { const from = rate; let to = from; let reason = "hold"; - // 硬约束:首次跌破即砍;段内连砍需短窗新证据(防 30min 窗内旧失败连砍钉死下限,dp3 教训) + // 硬约束:首次跌破 98% 即砍;段内续砍需「进行中」证据 —— 短窗 <95%(比入段门槛更强的 + // 活跃失败信号,波尾噪声不算)且全程增量新失败 ≥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; - const failsS = wS.sent - wS.ok; if (success >= 0.98) degraded = false; const entry = !degraded && success < 0.98 && fails >= 2; - const ongoing = degraded && wS.sent >= 10 && successS < 0.98 && failsS >= 2; + const ongoing = degraded && wS.sent >= 10 && successS < 0.95 && 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 ? "" : ";短窗新证据"}) ×${success < 0.95 ? 0.5 : 0.7}`; + reason = `success=${(success * 100).toFixed(1)}%<${success < 0.95 ? 95 : 98}%(失败 ${fails}${entry ? "" : ";短窗<95%+新失败≥2"}) ×${success < 0.95 ? 0.5 : 0.7}`; } else { - // 软信号:事件段(episode)迟滞 —— 进段砍一次,退段重置;段内不重复砍 - if (capPctS > 0.005 && !captchaWave) { + // 软信号:事件段迟滞 + 绝对数护栏(低速率下 1~2 个噪音即高比率,不得触发;dp5 教训) + if (wS.captcha >= 3 && capPctS > 0.005 && !captchaWave) { captchaWave = true; - to = capPctS > 0.02 ? from * 0.5 : from * 0.7; - reason = `验证码占比 ${(capPctS * 100).toFixed(2)}% ${capPctS > 0.02 ? "×0.5" : "×0.7"}`; - } else if (capPctS < 0.001) captchaWave = false; - if (to === from && gateRatioS > 0.15 && !gateWave) { + const heavy = wS.captcha >= 5 && capPctS > 0.02; + to = Math.max(SOFT_FLOOR, from * (heavy ? 0.5 : 0.7)); + reason = `验证码 ${wS.captcha} 个占比 ${(capPctS * 100).toFixed(2)}% ${heavy ? "×0.5" : "×0.7"}`; + } else if (wS.captcha === 0) captchaWave = false; + if (to === from && wS.gate >= 3 && gateRatioS > 0.15 && !gateWave) { gateWave = true; - to = from * 0.85; - reason = `门壳尝试占比 ${(gateRatioS * 100).toFixed(1)}%>15% ×0.85`; + to = Math.max(SOFT_FLOOR, from * 0.85); + reason = `门壳 ${wS.gate} 占比 ${(gateRatioS * 100).toFixed(1)}%>15% ×0.85`; } else if (gateRatioS < 0.05) gateWave = false; if (to === from && wS.poolEmpty > 5 && !poolEmptyWave) { poolEmptyWave = true; - to = from * 0.85; + to = Math.max(SOFT_FLOOR, from * 0.85); reason = `池空 ${wS.poolEmpty} ×0.85`; } else if (wS.poolEmpty === 0) poolEmptyWave = false; } - // 干净爬坡:每 RAMP_EVERY_MIN 个「干净节拍」至多一次;退化段中不爬(30min 窗未恢复 98%) + // 干净水位记账: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 时比率已无意义(绝对护栏统一交给软砍规则) if (to === from && !degraded && wS.sent >= 10) { - if (successS >= 0.995 && gateRatioS < 0.02 && wS.poolEmpty === 0 && capPctS < 0.003) { + if (successS >= 0.995 && gateRatioS < 0.02 && wS.poolEmpty === 0 && (wS.captcha === 0 || wS.sent >= 300)) { rampSeq++; - if (rampSeq % RAMP_EVERY_MIN === 0) { to = from * 1.08 + 0.2; reason = `clean(success=${(successS * 100).toFixed(1)}%,gate=${(gateRatioS * 100).toFixed(1)}%) ×1.08`; } + if (maxCleanRate > 0 && from < maxCleanRate * 0.75) { + to = from * 1.3 + 0.2; + reason = `clean-fast(水位 ${maxCleanRate.toFixed(0)}) ×1.3`; + } else if (rampSeq % RAMP_EVERY_MIN === 0) { to = from * 1.08 + 0.2; reason = `clean(success=${(successS * 100).toFixed(1)}%,gate=${(gateRatioS * 100).toFixed(1)}%) ×1.08`; } } } to = Math.round(Math.min(RATE_MAX, Math.max(RATE_MIN, to)) * 10) / 10;