代理IP故障演练怎么做:注入怎么分档、恢复怎么判定

0 阅读8分钟

结论先行:演练的目的是找洞,不是证明没洞。绝大多数演练之所以没有价值,不是因为做得不认真,而是因为设计上就在"构造能通过的场景"——单点故障、全断注入、只判"能不能用"。这三件事凑在一起,几乎必然得出"系统健壮"的结论,而这个结论对真实故障毫无预测力。

本文给出一套组合场景设计方法、分档注入的可控实现,以及三维恢复判定的度量方式。

一、一个"六项全过"却依旧翻车的案例

年度演练六个场景:出口整体不可用、上游超时、凭据失效、并发打满、限流触发、队列积压。六项全部通过,报告判定"具备容灾能力"。

四个月后真实故障:上游接口劣化与某地址段被封同时发生,系统没有切换,队列积压六小时。

复盘发现的机制冲突很典型:切换逻辑判断"上游慢,换出口没用"所以不切;而那个被封的地址段因为健康探测仍返回 200,被判定"可用"继续留在池里被打。两个判断各自正确,合起来是错的。

这类问题只能通过组合场景暴露。

二、场景设计:组合优先于穷举

穷举所有故障类型是不现实的,也没必要。真正值得测的是处理逻辑可能冲突的组合。

判据很直接:看两个机制会不会对同一个信号做出不同判断。

机制 A机制 B冲突点
健康探测(连通即可用)成功率监控(业务才算数)出口能连但业务失败
熔断(整体停)重试(单请求再试)熔断判断该停,重试仍在打
自动切换(换出口)限流退避(等一会)一个要动,一个要静
队列缓冲(堆积容错)超时丢弃(快速失败)一个攒,一个扔

每一行都是值得测的组合。四组覆盖的深度远超二十个单点场景。

三、注入要能停在中间档

全断注入最容易处理,信号极端明确。真实故障往往渐变:从轻微变慢到偶尔超时到三成失败。

中间这段"半死不活"的时间最难,因为判断逻辑会在阈值附近反复横跳。一个可用的注入器需要支持按比例注错:

import random
import time
from contextlib import contextmanager


@contextmanager
def inject_failure(rate: float, duration_sec: int, tag: str = "drill"):
    """按比例注入失败,用于中间档演练。

    rate: 0.0~1.0 的失败比例
    duration_sec: 持续时间,到期自动回收
    """
    state = {"active": True, "injected": 0, "passed": 0}

    def guard() -> bool:
        """在真实请求前调用,返回 True 表示这次请求应被注入失败"""
        if not state["active"]:
            return False
        if random.random() < rate:
            state["injected"] += 1
            return True
        state["passed"] += 1
        return False

    start = time.time()
    try:
        yield guard, state
    finally:
        # 必须无条件回收,即使 yield 块内抛异常
        state["active"] = False
        elapsed = time.time() - start
        print(f"[{tag}] 注入结束 rate={rate:.0%} "
              f"实际={state['injected']}/{state['injected']+state['passed']} "
              f"耗时={elapsed:.0f}s")


# 用法:分档跑三轮
for r in (0.2, 0.4, 0.6):
    with inject_failure(rate=r, duration_sec=120) as (guard, st):
        time.sleep(120)          # 实际业务循环

三个设计点值得注意。

一是 finally 里无条件回收。 这是最容易出事的地方——如果 yield 块里抛异常而没有 finally,注入会永久生效,演练直接变真故障。用上下文管理器强制保证回收。

二是记录实际注入比例。 请求量小的时候,random.random() < rate 的实际分布会偏离目标值。记录实际比例才能判断这轮演练有没有意义——如果目标 20% 实际只有 8%,那这轮结论不可信。

三是分档而不是一次到底。 20%、40%、60% 各跑一轮,观察系统在每一档的行为差异。很多时候系统在 20% 时表现正常、40% 时开始切换、60% 时陷入横跳——这个转折点才是最有价值的信息。

四、恢复判定:三个维度,缺一不可

"恢复"必须拆开定义,否则判定必然宽松。

维度定义计算方式
时间注入到指标回基线回基线时刻 - 注入时刻
完整恢复后数据不缺完成任务数 ÷ 应完成数
稳定恢复后不反复30 分钟内劣化次数
from dataclasses import dataclass


@dataclass
class VerdictBox:
    inject_ts: float
    recover_ts: float
    done: int
    expected: int
    relapse: int

    @property
    def seconds(self) -> float:
        return self.recover_ts - self.inject_ts

    @property
    def completeness(self) -> float:
        return self.done / self.expected if self.expected else 0.0

    def judge(self, max_sec: float = 300,
              min_completeness: float = 0.99,
              max_relapse: int = 0) -> tuple[bool, list[str]]:
        reasons = []
        if self.seconds > max_sec:
            reasons.append(f"恢复耗时 {self.seconds:.0f}s 超阈值 {max_sec:.0f}s")
        if self.completeness < min_completeness:
            reasons.append(f"数据完整率 {self.completeness:.1%} 低于 {min_completeness:.0%}")
        if self.relapse > max_relapse:
            reasons.append(f"恢复后劣化 {self.relapse} 次")
        return (not reasons), reasons


v = VerdictBox(inject_ts=0, recover_ts=2400, done=667, expected=1000, relapse=2)
ok, why = v.judge()
print(ok, why)
# False ['恢复耗时 2400s 超阈值 300s', '数据完整率 66.7% 低于 99%', '恢复后劣化 2 次']

这个例子是一次真实演练的结果:系统"恢复了",但四十分钟、丢了三分之一、还反复了两次。按"能用就算恢复"的标准是及格,按业务视角是完全不合格。

五、撤回验证:演练的前置条件

有一个风险必须单独说:注入撤不回来,演练就变成真故障。

这类事故不罕见——某个配置改了没记、某个开关忘了关、某个比例注入的标记没清掉。

防护措施两条:

其一,每次注入都是上下文管理器。 用 with 语句保证 finally 里回收,不依赖人工关闭。

其二,撤回动作在演练前验证过。 演练前先做一次 5 秒的短注入 + 撤回,确认撤回真的有效。验证不通过的注入不做。

其三,注入有全局开关。 一个独立的 kill switch,任何人在任何时候都能一键停掉所有注入。这个开关本身要能在演练前验证。

六、演练类型分层:四种各管一段

类型频率范围参与作用
桌面推演每季度全流程无注入全员校准认知
单点注入每月单组件值班+运维保持手感
组合注入每半年多组件交叉核心团队找设计缺陷
无预告每年不预设场景仅组织者知情验证真实响应

四种不是替代关系,是覆盖不同层面。缺了组合注入就找不到机制冲突,缺了无预告就不知道真实响应能力,缺了桌面推演则大家对流程的理解会逐渐漂移。

七、观测:四个数判断演练价值

指标含义健康值
待修复项演练产出每次 2-5 个
注入到发现监控覆盖度越短越好
注入到恢复切换效率按 SLA
数据完整率状态管理≥ 99%

首个指标最容易被忽略,因为它不是技术指标而是"产出指标"。但它恰恰是判断演练有没有价值的核心——待修复项为零通常意味着场景太友好,而不是系统太健壮。

八、三个反直觉的结论

结论一:演练失败比演练成功有价值。

演练中暴露远好于真实故障中暴露。报告里全是"符合预期"的演练,通常等于没做。

结论二:最该测的不是最严重的故障,是最模糊的故障。

全断谁都处理得了。真正难的是"上游慢了三成"这种半死不活的状态——它既不够严重到触发熔断,又足够严重到拖垮队列。

结论三:撤回验证比注入设计更重要。

注入设计错了,顶多测不到东西;撤回失败了,演练直接变成生产事故。后者代价高一个数量级。

常见问题

Q:多久演练一次合适?

分层:桌面每季度、单点每月、组合每半年、无预告每年。类型要全,频率次之。

Q:演练会不会影响线上业务?

要控制影响范围:低峰期、限定比例、备好撤回。完全不影响线上的演练通常也测不出真问题。

Q:怎么选值得组合的场景?

看两个机制会不会对同一信号做不同判断。判断冲突的地方就是薄弱点,也最值得测。

Q:注入比例怎么定?

分档跑:20%、40%、60% 各一轮。观察系统在哪一档开始切换、哪一档开始横跳。这个转折点比"能不能扛住全断"有价值得多。

Q:演练全通过算正常吗?

算可疑。真实系统总有薄弱点,全通过通常说明测得太浅或场景太友好。

Q:注入卡住变成真故障怎么办?

这是最需要防的风险。要求每次注入都用上下文管理器保证回收,且撤回动作在演练前验证过。再加一个全局 kill switch。

Q:无预告演练要注意什么?

提前对齐知情边界:谁知道、影响什么范围、出问题谁兜底。边界不清的无预告演练弊大于利。

Q:演练和告警怎么配合?

演练是验证告警的手段。注入一次劣化,看告警在什么时间点发出、发了几条、消息是否够判断——这三项就是告警体系的体检报告。

本文部分内容包含 AI 辅助创作。

#代理IP #故障演练 #稳定性