Skip to content

QIAN数据:门控遗漏定律与设计效应DEFF——定时任务跑了998次只失败4次,就敢说系统可靠吗? ​

风险科技 2026年9月16日 · 预计阅读 12 分钟

本文由 AI 辅助创作,数据实测与公式推导由作者完成。

(以下系统为作者本人的自动化调度环境,正文已做脱敏:不含作业名、路径与业务数据。)

上个月我给自己的自动化调度系统做了一次体检。翻开执行台账,998 次任务执行,失败 4 次,成功率 99.6%。

这个数字贴在报告里很好看。但同一份记录,换个数法,会得到一个让人不太舒服的答案——出过问题的作业占了 13.8%,是刚才那个数字的 34.4 倍。

两个数字来自同一份台账,都没算错。问题在于,我们平时报的是哪一种,以及报出来的那个数字究竟约束了什么。

先把结论摆出来:这份台账的 998 次执行、失败 4 次,按设计效应折算,有效样本只有约 23 个——名义样本量虚高了 43 倍。要判断"验证够不够",得看三个数:测了多少次(N)、其中多少是真正独立的(DEFF)、这个测法能检出多低的故障率(r)。

关键词:门控遗漏定律,设计效应DEFF,验证充分性,Clopper-Pearson,最优验证次数,定时任务监控

这张图是全文的路线图:从"998 次执行、失败 4 次"这个原始报法出发,结论要经过五重折损才轮到"证据"二字。后面每一节拆掉其中一格。


01 事件:一份"看起来很稳"的台账 ​

先把这份台账摊开。它记录了一段时间内所有定时任务的执行情况:每次执行一行,含作业标识、状态、开始时间。

python
import json
snap = json.load(open('data/ledger_snapshot.json', encoding='utf-8'))
rows = [(e['job'], e['status']) for e in snap['executions']]

print('执行总数  :', len(rows))
print('不同作业数:', len(set(r[0] for r in rows)))
print('失败次数  :', sum(1 for r in rows if r[1] != 'completed'))
执行总数  : 998
不同作业数: 29
失败次数  : 4

按执行次数算,失败率是 4/998=0.40%。这是最常见的报法,也是仪表盘默认给你的口径。

但"998 次执行"并不是 998 个独立的事件。它们只来自 29 个作业,而且分布极不均匀:

python
sizes = sorted((sum(1 for r in rows if r[0] == j)
                for j in set(r[0] for r in rows)), reverse=True)
print('执行次数分布(前5):', sizes[:5])
print('最大占比        : %.1f%%' % (sizes[0] / len(rows) * 100))
执行次数分布(前5): [903, 8, 7, 7, 7]
最大占比        : 90.5%

榜首那个作业一个人跑了 903 次,占全部执行的 90.5%;其余 28 个作业里,多数在整个窗口内只跑了不到 10 次。

横轴是 29 个作业按执行次数降序排,纵轴取了对数——不用对数轴的话,第二名往后会被压成一条线。红色那根就是榜首,903 次。金色虚线是全组均值 34.4 次,它被一个极端值拉得远高于多数作业的真实水平。

这就带出第一个问题:"执行次数"和"验证过的对象数"是两回事。

02 追问:为什么 99.6% 这个数字不约束任何东西 ​

换个口径,按作业(也就是按"被验证的独立对象")来数:

python
jobs = set(r[0] for r in rows)
bad = [j for j in jobs
       if any(r[1] != 'completed' for r in rows if r[0] == j)]
print('作业数        :', len(jobs))
print('出过问题的作业:', len(bad))
print('口径1 失败率  : %.4f%%' % (4 / len(rows) * 100))
print('口径2 失败率  : %.2f%%' % (len(bad) / len(jobs) * 100))
作业数        : 29
出过问题的作业: 4
口径1 失败率  : 0.4008%
口径2 失败率  : 13.79%

同一个系统,同一个月,0.40% 和 13.79%。

要理解这个差距,得先承认一件事:那 903 次执行并不提供 903 份独立信息。同一个作业,用同样的脚本、读同样的数据源、跑在同样的环境里,它第 900 次成功对"这个作业是否可靠"这个问题贡献的信息,和第 2 次成功几乎是重复的。

统计上这类"重复观测"有精确的度量,在下一节给出。但这里先要处理一个更基础的问题——当失败次数是 0 的时候,我们连一个像样的区间都给不出来。

03 方法(一):零失败时,常规区间估计直接崩溃 ​

假设我们对某个作业跑了 N 次,一次都没失败。想回答:"它的真实故障率上界是多少?"

最常见的做法是算 p^±zp^(1−p^)/N。但零失败时 p^=0,代入得:

p^±zp^(1−p^)N=0±0=0

区间宽度变成零,结论是"真实故障率等于 0"。这不是精度高,这是估计量在这个点上失效了:宽度正比于 p^(1−p^),而 N 次零失败恰好把这个乘积压成了 0。

零失败场景必须用精确法。N 次试验、0 次失败时,真实故障率的单边 (1−α) 置信上界由 Beta 分布分位数给出:

pupper=1−α1/N

这个式子不依赖正态近似,是精确解。代入几个真实数字:

python
for N in [10, 30, 100, 300, 1000, 3000, 10000]:
    print('N=%6d   α=0.05 上界=%.4f%%' %
          (N, (1 - 0.05 ** (1 / N)) * 100))
N=    10   α=0.05 上界=25.8866%
N=    30   α=0.05 上界=9.5034%
N=   100   α=0.05 上界=2.9513%
N=   300   α=0.05 上界=0.9936%
N=  1000   α=0.05 上界=0.2991%
N=  3000   α=0.05 上界=0.0998%
N= 10000   α=0.05 上界=0.0300%

读法:跑 30 次零失败,你只能声称真实故障率低于 9.5%。想压到 1% 以下,需要大约 300 次零失败;想压到 0.1% 以下,要接近 3000 次。

红线是精确上界随 N 的衰减,金线是 α=0.01 的口径。贴底那条锈色点线才是重点:正态近似在这个场景下恒为 0——0 次失败让 p^=0,区间的宽度也跟着变成 0,于是它"很有把握地"告诉你真实率是 0。三条竖标注分别是 N=30、100、1000 对应的上界。

这就是"一次都没出错"这句话的准确分量。它远没有听上去那么强。

(顺带一提,贝叶斯做法在零失败时给出几乎相同的结果:Beta(1,1) 先验下后验为 Beta(1,N+1),N=1000 时的上界是 0.2988%,与频率派的 0.2991% 只差万分之一。两种范式在零失败场景下会合。)

04 方法(二):门控遗漏定律 ​

第二节留下的问题是:N 次验证"全部漏掉某个故障"的概率到底是多少。

设某个故障在单次验证中现身(被检出)的概率为 r。如果各次验证相互独立,那么单次漏掉它的概率是 1−r,N 次全部漏掉就是:

P(N 次全未命中)=(1−r)N

这个式子不需要任何近似,是独立性下概率乘积的直接结果。用蒙特卡洛对照验证:

python
import numpy as np
rng = np.random.default_rng(42)
for r, N in [(0.10, 10), (0.05, 30), (0.02, 50), (0.01, 100)]:
    exact = (1 - r) ** N
    mc = 1 - (rng.random((200000, N)) < r).any(axis=1).mean()
    print('r=%.3f N=%3d  解析=%.6f  模拟=%.6f' % (r, N, exact, mc))
r=0.100 N= 10  解析=0.348678  模拟=0.348780
r=0.050 N= 30  解析=0.214639  模拟=0.215060
r=0.020 N= 50  解析=0.364170  模拟=0.365215
r=0.010 N=100  解析=0.366032  模拟=0.366480

解析值与 20 万次模拟的偏差在千分之一量级,公式成立。

真正有用的是把它反过来解。要求漏掉概率不超过 α:

(1−r)N≤α⟹N≥ln⁡αln⁡(1−r)≈r≪1ln⁡(1/α)r

小 r 时的近似说明了一件事:所需样本量与故障率成反比。故障率缩小 10 倍,要抓它就得把样本量放大 10 倍。这是"用样本量换检出率"的线性代价,没有便宜可占。

python
import math
for alpha in [0.10, 0.05, 0.01]:
    row = [math.ceil(math.log(alpha) / math.log(1 - r))
           for r in [0.05, 0.02, 0.01, 0.005, 0.001]]
    print('α=%.2f →' % alpha, row)
α=0.10 → [45, 114, 230, 460, 2302]
α=0.05 → [59, 149, 299, 598, 2995]
α=0.01 → [90, 228, 459, 919, 4603]

对应到真实尺度:想知道一个发生率 0.1% 的故障存不存在,95% 把握下需要约 2995 次验证。发生率 1%,需要约 299 次。

五条曲线是不同真实故障率下"跑 N 次一次都没撞上"的概率。注意它们在 N 小时全都贴着 1——故障率 1% 的曲线跑到第 30 次仍有 74% 的概率完全没见过它。金色安全区是 5% 遗漏线。

换成双对数坐标,倒数律就变成直线:横轴故障率缩小 10 倍,纵轴所需样本就抬高 10 倍。两个圆点标的是 r=1% 与 r=0.1% 在 α=0.05 口径下的解。

05 拆解(一):重复不是更多样本——设计效应 ​

现在处理第二节那个 34.4 倍的差距。它有个精确的名字:设计效应(Design Effect)。

在簇抽样里,如果观测被分成若干簇(这里是"作业"),簇内有相关性 ρ,那么样本均值的方差会被放大:

Var(y¯)=σ2n[1+(m−1)ρ]=σ2n⋅DEFF

其中 m 是簇的大小。有效样本量因此是 neff=n/DEFF。

簇大小相同时这就是全部。但我们的簇长极不均匀——一个 903,多数不到 10——这时要换成 Kish 给出的不等簇长形式:

DEFF=1+(CVm2⋅m¯+m¯−1)ρ

把真实数字代进去:

python
m, cv = 34.4137931034, 4.8548707   # 真实簇长均值与变异系数(精确值)
for rho in [0.01, 0.02, 0.05, 0.10]:
    simple = 1 + (m - 1) * rho
    kish = 1 + (cv ** 2 * m + m - 1) * rho
    print('ρ=%.2f  简化=%.4f  Kish=%.2f  低估%.1f倍' %
          (rho, simple, kish, kish / simple))
ρ=0.01  简化=1.3341  Kish=9.45   低估7.1倍
ρ=0.02  简化=1.6683  Kish=17.89  低估10.7倍
ρ=0.05  简化=2.6707  Kish=43.23  低估16.2倍
ρ=0.10  简化=4.3414  Kish=85.45  低估19.7倍

这是本文我自己踩的坑,值得如实记下来:我一开始用了等簇长那个简化式。按它算,ρ=0.05 时 DEFF 只有 2.67,有效样本 374——看起来还行。

直到我意识到真实簇长的变异系数是 4.85(标准差 167.07 对均值 34.41)。换成 Kish 式之后:DEFF = 43.23,有效样本从 1000 掉到 23。

也就是说,那份"998 次执行"的台账,按独立信息量折算,大约相当于 23 个独立作业。差距 43 倍。

红色是 Kish 式,蓝色是等簇长简化式,两线之间的填充就是被我一开始漏掉的量。ρ 取 0.05 时,简化式给 2.67,Kish 式给 43.23——差 16 倍。这个差距全部来自真实簇长的变异系数 4.85:一个作业 903 次、多数不到 10 次,簇长越不均,简化式的低估越狠。

这个公式我用严格构造验证过一遍——beta-二项模型下,簇概率 pj∼Beta(a,b) 时簇内相关恰好是 ρ=1/(a+b+1):

python
import numpy as np
rng = np.random.default_rng(42)
k, m = 5000, 10          # 5000 簇×10 观测; k=400 时误差可达 15%
for rho in [0.02, 0.05, 0.10, 0.20]:
    ab = 1 / rho - 1                       # Beta(0.05ab, 0.95ab)
    p_cl = rng.beta(0.05 * ab, 0.95 * ab, k)
    obs = rng.random((k, m)) < p_cl[:, None]
    cm = obs.mean(axis=1); pbar = obs.mean()
    deff_emp = cm.var(ddof=1) / (pbar * (1 - pbar) / m)
    print('ρ=%.2f  理论DEFF=%.2f  实测=%.2f' %
          (rho, 1 + (m - 1) * rho, deff_emp))
ρ=0.02  理论DEFF=1.18  实测=1.21
ρ=0.05  理论DEFF=1.45  实测=1.43
ρ=0.10  理论DEFF=1.90  实测=1.89
ρ=0.20  理论DEFF=2.80  实测=2.88

实测与理论在 3% 以内吻合,公式可用。(注:簇数取 400 时误差可达 15%,这里用 5000 簇把蒙特卡洛噪声压下来。)

06 拆解(二):台账自己也有盲区 ​

到这里,前面所有讨论都建立在一个隐含假设上:执行台账是完整的。

我去核对了事故台账,结果是另一记闷棍。

python
ex_w = (min(e[2] for e in EX), max(e[2] for e in EX))
in_w = [i for i in INC if ex_w[0] <= i[2] <= ex_w[1]]
print('执行台账窗口:', ex_w[0], '→', ex_w[1])
print('事故台账窗口:', min(i[2] for i in INC), '→', max(i[2] for i in INC))
print('事故落在执行窗口内: %d/%d' % (len(in_w), len(INC)))
执行台账窗口: 2026-09-09T17:40 → 2026-09-16T00:10
事故台账窗口: 2026-08-28T21:04 → 2026-09-12T22:00
事故落在执行窗口内: 2/8

事故台账里 8 条记录,只有 2 条落在执行台账的时间窗内。剩下 6 条——包括同一个作业连续三次限流失败——执行台账里根本看不到。

这背后是留存窗口差异造成的系统性遗漏,与巧合无关:两张表的起点不同。如果我只信执行台账,会得出"系统整体健康"的结论;而事实上,另一张表里躺着 6 条它看不见的事故。

这对"验证"这件事的打击比前两节更彻底。前面说的是"样本量不够",这里说的是取样范围本身就有洞——你再怎么增加 N,也增加不到台账窗口之外去。

顺带看一眼重复性。8 条事故里,同一个作业重复发生的有 5 条(一个 3 次、一个 2 次)。另一份独立日志里,748 条错误记录中有 216 条来自同一个反复出现的错误,占 29%。

重复的东西看起来像"多",但它们是同一个信息的回声。

07 方法(三):把漏检概率换算成钱,解出最优验证次数 ​

到此为止的结论偏悲观:样本量要求高,有效样本还被严重打折。那是不是应该无限加大验证力度?

不是。验证本身有成本,而漏检的损失随 N 指数衰减——这是一个标准的权衡问题。

设单次验证成本 cs,单次漏检代价 cf,该故障年发生频次 A。总成本:

C(N)=N⋅cs⏟样本成本, 线性+(1−r)N⋅cf⋅A⏟期望漏检代价, 指数衰减

对 N 求导并令其为零:

dCdN=cs+ln⁡(1−r)(1−r)NcfA=0

解得最优验证次数(注意 ln⁡(1−r)<0):

N∗=ln(cs−ln⁡(1−r)⋅cf⋅A)ln⁡(1−r)

取一组可感参数(单次验证 10 元、单次漏检 100 万、年发生 10 次):

python
import math
c_s, c_f, A = 10.0, 1_000_000.0, 10.0
f = lambda N, r: N * c_s + (1 - r) ** N * c_f * A
for r in [0.05, 0.01, 0.005, 0.001]:
    ln1 = math.log(1 - r)
    Ns = math.log(c_s / (-ln1 * c_f * A)) / ln1
    print('r=%.3f  N*=%6.0f  C(N*)=%9.0f  C(1000)=%10.0f'
          % (r, Ns, f(Ns, r), f(1000, r)))
r=0.050  N*=   211  C(N*)=     2309  C(1000)=     10000
r=0.010  N*=   917  C(N*)=    10164  C(1000)=     10432
r=0.005  N*=  1700  C(N*)=    18992  C(1000)=     76540
r=0.001  N*=  6905  C(N*)=    79043  C(1000)=   3686954

两个可操作的读数:

其一,最优次数随故障率下降而上升,但只按对数增长。 故障率从 5% 降到 0.1%(50 倍),N∗ 从 211 升到 6905(约 33 倍)——而如果按第 04 节那条"样本量与故障率成反比"的直觉去堆,很容易堆到过量的量级。

其二,过头和不足都要付钱。 看最后一行:真实故障率 0.1% 时,按 N∗=6905 优化,总代价约 7.9 万;而机械地跑到 10000 次,代价是 368.7 万——多出来的 360 万全花在边际收益极低的验证上。

三条曲线是不同故障率下的总成本,圆点标出各自的 N∗。左侧陡降段是漏检代价在主导,右侧缓升段是样本成本在主导,谷底就是最优解。N∗ 随故障率下降而右移,但只按对数移动——所以"低概率故障要测到天荒地老"这个直觉是错的。

08 结论:验证要盯"漏了什么",不是盯"测了多少" ​

把这条链串起来。

第一,零失败不构成证据,除非 N 足够大,而且必须用精确上界。 30 次零失败只支持"低于 9.5%";要声称低于 0.3%,得接近 1000 次。用正态近似会得出"真实率 = 0"的荒谬结论。

第二,名义样本量会被重复和簇结构严重虚高。 998 次执行在本例中折算成约 23 个独立作业的信息量,虚高 43 倍。数执行次数,等于把同一个信息数了很多遍。

第三,"成功率"这个问题本身取决于你数什么。 同一份台账,0.40% 和 13.79% 都对,但只有后者约束了"这个系统有多少环节曾经不可靠"。

第四,取样窗口的洞,加样本量补不上。 执行台账看不见 75% 的历史事故——这跟 N 的大小无关,属于覆盖范围的问题。

第五,验证力度有最优解。 N∗ 存在且可计算,无脑堆样本既贵又不见得更安全。

回到开头那个 99.6%。它没有说谎,它只是回答了一个很窄的问题:这段时间里跑出去的任务,大部分都跑完了。它没有回答、也无法回答的问题是:这套系统里有多少个环节,其实已经出过问题但没被记录。

下次看到"我们测过了,没问题",可以问三个数:测了多少次(N)、这里面有多少是真正独立的(DEFF)、以及这个测法能检出多低的故障率(r)。这三个数通常只有一个会被主动告诉你。

落地成判定:三个数一起看才有结论。左边是验证不足的信号——只说次数不提独立样本数、拿零失败推断真实率为零、窗口短于系统历史、故障率 0.1% 却只跑 1000 次(N∗ 应是 6905)。右边是过度验证的信号——已经超过 N∗ 还在堆样本、把同一作业重复跑当独立验证。两边都要花钱,方向相反。


数据说明: 执行台账 998 条、事故台账 8 条,均来自作者本人的自动化调度系统(SQLite 本地库),已脱敏处理:不含作业名称、文件路径、业务数据与账号信息。样本量与故障率反解、Clopper-Pearson 精确上界、设计效应、最优验证次数均为公式直接计算,可用文中代码复现。蒙特卡洛对照使用 np.random.seed(42),20 万次重复。

代码环境: Python 3.11,NumPy / SciPy / Matplotlib。所有数字来自 gen_figures.py 实际执行输出,未做任何手工调整。


  • Clopper, C. J., & Pearson, E. S. (1934). The use of confidence or fiducial limits illustrated in the case of the binomial. Biometrika, 26(4), 404–413. — 零失败精确上界 1−α1/N 的原始来源,本文第 03 节直接使用
  • Kish, L. (1965). Survey Sampling. Wiley. — 设计效应与不等簇长下的 DEFF 修正形式,本文第 05 节 Kish 式的出处
  • Cochran, W. G. (1977). Sampling Techniques (3rd ed.). Wiley. — 簇抽样下样本均值方差的分解,DEFF 的一般推导

关注公众号:QIAN数据