stress: 双路径压测控制器 v2 —— 软信号事件段迟滞+短窗,修复验证码波连砍钉死下限
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 数据归档保留。
This commit is contained in:
@@ -5,10 +5,14 @@
|
|||||||
* 双路径原形态(runtime=auto,无 sticky,全池 LRU 选号):
|
* 双路径原形态(runtime=auto,无 sticky,全池 LRU 选号):
|
||||||
* GET /api/serp/json/search?q=...&num=10
|
* GET /api/serp/json/search?q=...&num=10
|
||||||
* 令牌内走 go-fetch(全局限流 fetchPerSec),限流外并发走温池浏览器 —— 组合即生产形态。
|
* 令牌内走 go-fetch(全局限流 fetchPerSec),限流外并发走温池浏览器 —— 组合即生产形态。
|
||||||
* AIMD 控制器(每 60s 决策,30min 滚动窗口;加退避即时、加速每 5 分钟一步):
|
* AIMD 控制器(每 60s 决策;硬约束看 30min 窗,软信号看 10min 短窗):
|
||||||
* 成功率<95% → 速率×0.5;<98% → ×0.7;验证码≥3/窗口 → ×0.7;
|
* 硬约束即时退避:成功率<95%(失败≥3)×0.5;<98%(失败≥2)×0.7;
|
||||||
* 内部门壳尝试占比>15% → ×0.85(抢先退避,藏不住的门壳墙来临前减速);
|
* 软信号按事件段(episode)迟滞,每段波只砍一次(波退去才重置):
|
||||||
* 池空>5/窗口 → ×0.85;成功率≥99.5% 且门壳占比<2% 且无池空/验证码 → ×1.08。
|
* 验证码占比>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(真实并发让溢出自然流向浏览器路径)。
|
* 速率区间 [3, 400] req/min;并发 worker 默认 12(真实并发让溢出自然流向浏览器路径)。
|
||||||
* 判定(客户端,JSON 出口):ok = 200 且 organic>0;gate = 200 且 organic=0 且
|
* 判定(客户端,JSON 出口):ok = 200 且 organic>0;gate = 200 且 organic=0 且
|
||||||
* attempts 含 gate;empty = 200 且 organic=0 无门壳尝试;错误按响应 code 分类
|
* attempts 含 gate;empty = 200 且 organic=0 无门壳尝试;错误按响应 code 分类
|
||||||
@@ -104,43 +108,69 @@ const decisions: Record<string, unknown>[] = [];
|
|||||||
const fpMap = new Map<string, number>();
|
const fpMap = new Map<string, number>();
|
||||||
|
|
||||||
/** # 函数
|
/** # 函数
|
||||||
* 窗口聚合求和。
|
* 窗口聚合求和(参数 n:取最近 n 个分钟桶;不传 = 全窗口)。
|
||||||
*/
|
*/
|
||||||
function sumWindow(): Counter {
|
function sumWindow(n = WINDOW_MIN): Counter {
|
||||||
const s = newCounter();
|
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];
|
for (const k of Object.keys(s) as (keyof Counter)[]) s[k] += c[k];
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** # 成员变量 软信号事件段状态(迟滞:进段砍一次,退段重置;防同一段波逐分钟连砍) **/
|
||||||
|
let captchaWave = false;
|
||||||
|
let gateWave = false;
|
||||||
|
let poolEmptyWave = false;
|
||||||
|
|
||||||
/** # 函数
|
/** # 函数
|
||||||
* 控制器决策(每 60s 调用):按窗口成功率/内部压力调整速率,返回决策记录或 null。
|
* 控制器决策(每 60s 调用):按窗口成功率/内部压力调整速率,返回决策记录或 null。
|
||||||
*/
|
*/
|
||||||
function controlTick(): Record<string, unknown> | null {
|
function controlTick(): Record<string, unknown> | null {
|
||||||
const w = sumWindow();
|
const wH = sumWindow(30); // 硬约束窗口:成功率即约束本身
|
||||||
const tot = w.sent;
|
const tot = wH.sent;
|
||||||
if (tot < 10) return null; // 信号不足,保持
|
if (tot < 10) return null; // 信号不足,保持
|
||||||
const success = w.ok / tot;
|
const wS = sumWindow(10); // 软信号短窗:波动退去快,防旧波滞留连砍(dp1 教训)
|
||||||
const gateRatio = w.attemptsTotal > 0 ? w.gateAttempts / w.attemptsTotal : 0;
|
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;
|
const from = rate;
|
||||||
let to = from;
|
let to = from;
|
||||||
let reason = "hold";
|
let reason = "hold";
|
||||||
// 最少失败数门槛:低速率下单次偶发失败不该触发退避(3 发 1 失败 = 67%,2 发 1 失败 = 50%)
|
// 硬约束即时退避,最少失败数门槛:低速率下单次偶发失败不该触发(3 发 1 失败 = 67%)
|
||||||
const fails = tot - w.ok;
|
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`; }
|
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 (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 {
|
||||||
else if (gateRatio > 0.15) { to = from * 0.85; reason = `门壳尝试占比 ${(gateRatio * 100).toFixed(1)}%>15% ×0.85`; }
|
// 软信号:事件段(episode)迟滞 —— 进段砍一次,退段重置;段内不重复砍
|
||||||
else if (w.poolEmpty > 5) { to = from * 0.85; reason = `池空 ${w.poolEmpty} ×0.85`; }
|
if (capPctS > 0.005 && !captchaWave) {
|
||||||
else if (success >= 0.995 && gateRatio < 0.02 && w.poolEmpty === 0 && w.captcha === 0) {
|
captchaWave = true;
|
||||||
// 加速步:每 RAMP_EVERY_MIN 个「干净节拍」至多一次(窗口滞后于用量,防预算烧穿前无信号)
|
to = capPctS > 0.02 ? from * 0.5 : from * 0.7;
|
||||||
rampSeq++;
|
reason = `验证码占比 ${(capPctS * 100).toFixed(2)}% ${capPctS > 0.02 ? "×0.5" : "×0.7"}`;
|
||||||
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 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;
|
to = Math.round(Math.min(RATE_MAX, Math.max(RATE_MIN, to)) * 10) / 10;
|
||||||
rate = to;
|
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);
|
decisions.push(rec);
|
||||||
if (decisions.length > 500) decisions.splice(0, decisions.length - 500);
|
if (decisions.length > 500) decisions.splice(0, decisions.length - 500);
|
||||||
if (to !== from) console.log(`[${rec.ts}] 速率 ${rec.from} → ${rec.to} req/min(${reason})`);
|
if (to !== from) console.log(`[${rec.ts}] 速率 ${rec.from} → ${rec.to} req/min(${reason})`);
|
||||||
|
|||||||
Reference in New Issue
Block a user