From f17eef3f746424472fca0dc93328573e709498f0 Mon Sep 17 00:00:00 2001 From: bigcode Date: Fri, 30 Oct 2026 17:08:37 +0800 Subject: [PATCH] =?UTF-8?q?stress:=20=E5=8F=8C=E8=B7=AF=E5=BE=84=E5=8E=8B?= =?UTF-8?q?=E6=B5=8B=E6=8E=A7=E5=88=B6=E5=99=A8=20v2=20=E2=80=94=E2=80=94?= =?UTF-8?q?=20=E8=BD=AF=E4=BF=A1=E5=8F=B7=E4=BA=8B=E4=BB=B6=E6=AE=B5?= =?UTF-8?q?=E8=BF=9F=E6=BB=9E+=E7=9F=AD=E7=AA=97,=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E6=B3=A2=E8=BF=9E=E7=A0=8D=E9=92=89?= =?UTF-8?q?=E6=AD=BB=E4=B8=8B=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dp1 实测教训:速率 15→100/min 全程成功率 99.8-100%(100/min 时 99.9%),08:46 出现 12 验证码/30min(12 个不同指纹各 1 次,单 IP 逐个越限的常态噪声,/sorry 自动恢复 逐个接管),成功率仍有 99.3%,但「窗口含验证码≥3 就每分钟 ×0.7」把速率连砍 10 分钟 钉死在 3/min 下限 —— 窗口滞后放大过度反应。 v2:硬约束(成功率<95/98%)看 30min 窗即时砍不变;软信号(验证码/门壳占比/池空) 改 10min 短窗 + 事件段(episode)迟滞 —— 进段砍一次(验证码按占比 >2%×0.5、>0.5%×0.7), 退段才重置;干净爬坡改看短窗且验证码≤0.3% 不阻追涨。dp1 数据归档保留。 --- stress/serp-dualpath-optimize.ts | 72 ++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/stress/serp-dualpath-optimize.ts b/stress/serp-dualpath-optimize.ts index a6cd48c..0395a5f 100644 --- a/stress/serp-dualpath-optimize.ts +++ b/stress/serp-dualpath-optimize.ts @@ -5,10 +5,14 @@ * 双路径原形态(runtime=auto,无 sticky,全池 LRU 选号): * GET /api/serp/json/search?q=...&num=10 * 令牌内走 go-fetch(全局限流 fetchPerSec),限流外并发走温池浏览器 —— 组合即生产形态。 - * AIMD 控制器(每 60s 决策,30min 滚动窗口;加退避即时、加速每 5 分钟一步): - * 成功率<95% → 速率×0.5;<98% → ×0.7;验证码≥3/窗口 → ×0.7; - * 内部门壳尝试占比>15% → ×0.85(抢先退避,藏不住的门壳墙来临前减速); - * 池空>5/窗口 → ×0.85;成功率≥99.5% 且门壳占比<2% 且无池空/验证码 → ×1.08。 + * AIMD 控制器(每 60s 决策;硬约束看 30min 窗,软信号看 10min 短窗): + * 硬约束即时退避:成功率<95%(失败≥3)×0.5;<98%(失败≥2)×0.7; + * 软信号按事件段(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。 * 速率区间 [3, 400] req/min;并发 worker 默认 12(真实并发让溢出自然流向浏览器路径)。 * 判定(客户端,JSON 出口):ok = 200 且 organic>0;gate = 200 且 organic=0 且 * attempts 含 gate;empty = 200 且 organic=0 无门壳尝试;错误按响应 code 分类 @@ -104,43 +108,69 @@ const decisions: Record[] = []; const fpMap = new Map(); /** # 函数 - * 窗口聚合求和。 + * 窗口聚合求和(参数 n:取最近 n 个分钟桶;不传 = 全窗口)。 */ -function sumWindow(): Counter { +function sumWindow(n = WINDOW_MIN): Counter { const s = newCounter(); - for (const { c } of window) { + for (const { c } of window.slice(-n)) { for (const k of Object.keys(s) as (keyof Counter)[]) s[k] += c[k]; } return s; } +/** # 成员变量 软信号事件段状态(迟滞:进段砍一次,退段重置;防同一段波逐分钟连砍) **/ +let captchaWave = false; +let gateWave = false; +let poolEmptyWave = false; + /** # 函数 * 控制器决策(每 60s 调用):按窗口成功率/内部压力调整速率,返回决策记录或 null。 */ function controlTick(): Record | null { - const w = sumWindow(); - const tot = w.sent; + const wH = sumWindow(30); // 硬约束窗口:成功率即约束本身 + const tot = wH.sent; if (tot < 10) return null; // 信号不足,保持 - const success = w.ok / tot; - const gateRatio = w.attemptsTotal > 0 ? w.gateAttempts / w.attemptsTotal : 0; + const wS = sumWindow(10); // 软信号短窗:波动退去快,防旧波滞留连砍(dp1 教训) + const success = wH.ok / tot; + const gateRatio = wH.attemptsTotal > 0 ? wH.gateAttempts / wH.attemptsTotal : 0; + const gateRatioS = wS.attemptsTotal > 0 ? wS.gateAttempts / wS.attemptsTotal : 0; + const capPctS = wS.sent > 0 ? wS.captcha / wS.sent : 0; const from = rate; let to = from; let reason = "hold"; - // 最少失败数门槛:低速率下单次偶发失败不该触发退避(3 发 1 失败 = 67%,2 发 1 失败 = 50%) - const fails = tot - w.ok; + // 硬约束即时退避,最少失败数门槛:低速率下单次偶发失败不该触发(3 发 1 失败 = 67%) + const fails = tot - wH.ok; if (success < 0.95 && fails >= 3) { to = from * 0.5; reason = `success=${(success * 100).toFixed(1)}%<95%(失败 ${fails}) ×0.5`; } else if (success < 0.98 && fails >= 2) { to = from * 0.7; reason = `success=${(success * 100).toFixed(1)}%<98%(失败 ${fails}) ×0.7`; } - else if (w.captcha >= 3) { to = from * 0.7; reason = `captcha 波(${w.captcha}) ×0.7`; } - else if (gateRatio > 0.15) { to = from * 0.85; reason = `门壳尝试占比 ${(gateRatio * 100).toFixed(1)}%>15% ×0.85`; } - else if (w.poolEmpty > 5) { to = from * 0.85; reason = `池空 ${w.poolEmpty} ×0.85`; } - else if (success >= 0.995 && gateRatio < 0.02 && w.poolEmpty === 0 && w.captcha === 0) { - // 加速步:每 RAMP_EVERY_MIN 个「干净节拍」至多一次(窗口滞后于用量,防预算烧穿前无信号) - rampSeq++; - if (rampSeq % RAMP_EVERY_MIN === 0) { to = from * 1.08 + 0.2; reason = `clean(success=${(success * 100).toFixed(1)}%,gate=${(gateRatio * 100).toFixed(1)}%) ×1.08`; } + else { + // 软信号:事件段(episode)迟滞 —— 进段砍一次,退段重置;段内不重复砍 + if (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) { + gateWave = true; + to = from * 0.85; + reason = `门壳尝试占比 ${(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; + reason = `池空 ${wS.poolEmpty} ×0.85`; + } else if (wS.poolEmpty === 0) poolEmptyWave = false; + } + // 干净爬坡:每 RAMP_EVERY_MIN 个「干净节拍」至多一次(验证码≤0.3% 属高压力常态噪声,不阻追涨) + if (to === from && wS.sent >= 10) { + const successS = wS.ok / wS.sent; + if (successS >= 0.995 && gateRatioS < 0.02 && wS.poolEmpty === 0 && capPctS < 0.003) { + 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`; } + } } to = Math.round(Math.min(RATE_MAX, Math.max(RATE_MIN, to)) * 10) / 10; rate = to; - const rec = { ts: new Date().toISOString(), from: +from.toFixed(1), to: +to.toFixed(1), reason, rampSeq, rampTick: rampSeq % RAMP_EVERY_MIN, window: { sent: tot, ok: w.ok, gate: w.gate, captcha: w.captcha, poolEmpty: w.poolEmpty, success: +(success * 100).toFixed(2), gateRatio: +(gateRatio * 100).toFixed(2) } }; + const rec = { ts: new Date().toISOString(), from: +from.toFixed(1), to: +to.toFixed(1), reason, rampSeq, rampTick: rampSeq % RAMP_EVERY_MIN, window: { sent: tot, ok: wH.ok, gate: wH.gate, captcha: wH.captcha, poolEmpty: wH.poolEmpty, success: +(success * 100).toFixed(2), gateRatio: +(gateRatio * 100).toFixed(2), capPctS: +(capPctS * 100).toFixed(2) }, waves: { captcha: captchaWave, gate: gateWave, poolEmpty: poolEmptyWave } }; decisions.push(rec); if (decisions.length > 500) decisions.splice(0, decisions.length - 500); if (to !== from) console.log(`[${rec.ts}] 速率 ${rec.from} → ${rec.to} req/min(${reason})`);