← 返回目录

第 2 卷

第 2 / 4 卷日期:

复制代码

1//对每条历史流量回放(复用ch18引擎),统计覆盖率/精确率/误伤率*
2*lettotalBad =0 //样本里该拦(异常)的总数*
3*lettotalGood =0 //样本里正常的总数*
4*letcaughtBad =0 //异常、且被候选命中→拦对的*
5*letcaughtGood =0 //正常、却被候选命中→误伤的*
6*for(const{ req, isBad }oftraffic) {*
7* // isBad:这条请求是不是异常;matched:这条候选命没命中它*
8* constmatched =ruleMatches(compiled,sanitizeRequest(req)) // compiled:com
9* if(isBad) {*
10* totalBad++*
11* if(matched) caughtBad++*
12* }else{*
13* totalGood++*
14* if(matched) caughtGood++*
15* }*
16*}*
17*constcoverage = totalBad ===0?0: caughtBad / totalBad //覆盖率=拦住的异常
18*constprecision = caughtBad + caughtGood ===0?0: caughtBad / (caughtBad + ca
19*constfpr = totalGood ===0?0: caughtGood / totalGood //误伤率=误伤的正常/

例子中这四个计数器的值是 totalBad=9、totalGood=10、caughtBad=2、 caughtGood=0,上面三个式子的分子分⺟,就是它们两两相除的结果。

阴影测试:不影响真实环境

从历史回放直接跳到真实拦截,仍然有些冒险:历史样本是固定的,可能没覆盖真实流量的分布,直接上线就要当场承担误伤代价。有一种经典的过渡式阴影测试(shadow),把规则挂

上真实流量跑,但结果不影响真实环境:我们这一讲是“只记录、不拦截”,也有的做法是复制一份镜像环境来真正拦截。

因为不碰真实环境,所以不会误伤真实请求。它要回答的问题和模拟测试是同一个:拦得好不好(覆盖 / 精确 / 误伤),只是把“历史样本”换成“活的真实流量”,看这三个数在真实流量

上还成不成立。

么)、正常样本数(误伤率分⺟)都照常积累。阴影测试结束时,手里已经有一套“这条规则在真实流量上会怎么表现”的信号,作为下一步进灰度的依据。

灰度测试:小范围真实拦截

阴影测试确认“它会拦什么”之后,才进入灰度测试(canary),小范围真实拦截,验证误伤率。灰度期有两条自动退路:

自动回退:误伤率超阈值,规则就回退、退出拦截;正常样本不足(minGoodTotal,默认 20)时暂不回退,防止仅凭一条噪声样本就把规则误判为回退。回退不可自动恢复,想

重新启用只能由人复核后解除(reinstate)。

老化:连续无命中达到阈值就退场,避免规则库膨胀,看的是连续无命中,不是累计命中,否则命中过一次就永远不会老化。

命中达标(本讲配置为命中 3 次)就转 active(正式全量拦截);自主级别 2(第 19 讲)

且确认通过的候选,直接从这里起步。

这两条退路写在 transition 里,每次请求到来就自动判一次(精简自源码,observe 先更新命中 / 误伤 / 漏拦 / 正常样本这些信号):

复制代码 1private transition(): void {

2if(isTerminal(this._stage))return//终态不再转移
3//回退与转正式只在真实拦截的灰度/全量期判定;阴影测试只观察,不因模拟误伤就回退
4if(this._stage ==='canary'||this._stage ==='active') {
5constrate=this._signal.goodTotal ===0?0:this._signal.falsePositives /
6constsufficient=this._stage ==='active'||this._signal.goodTotal >= (thi
7if(rate >this.cfg.fpThreshold && sufficient) {//误伤率超阈值
8this.setStage('rolled_back','误伤率超阈值')
9return
10}
11if(this._stage ==='canary'&&this._signal.hits >= (this.cfg.promoteHits ??
12this.setStage('active','灰度达标')

18}

阶段变更一律走转移表(loop.ts 的 TRANSITIONS):能从哪走到哪是显式写出来的,非法转移直接抛错,而不是静默生效:

复制代码

1constTRANSITIONS:Record<stage,readonlystage[]> = {
2proposed: ['shadow','canary','aged_out'],
3shadow: ['canary','aged_out'],
4canary: ['active','rolled_back','aged_out'],
5active: ['rolled_back','aged_out'],
6rolled_back: [], //终态:不可复活
7aged_out: [], //终态
8}</stage,readonlystage[]>

规则怎样持续进化:闭环

自动回退兜住了“误伤”(拦了正常请求)这一头,误伤率超阈值就当场回退止损;而“漏拦”

(该拦的没拦住)这一头却只被记下,还没接回进化循环,那些漏掉的异常请求不会自动进入下一轮提案。闭环就是把这两头都接上,这一节从漏拦这头切入:

长出候选 → 评测 → 确认 → 上线,上线后撞上真实流量、漏掉新的异常请求;漏掉的喂回证据库 → 下一轮提案,长出覆盖它的候选,再评测、再确认、再上线,又出现新的漏

拦……回到起点,一轮轮转下去。

全站分卷索引 · 4 卷
  1. 1第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷