Skip to content

错误预算与过载接纳推演 ​

前置:理解请求成功/失败、延迟阈值、比例和统计窗口;Python 3.10+ 用于请求预算计算。教学范围为请求型SLI/错误预算;SRE Workbook2018;Python 3.10+。核对日期2026-10-17。示例未执行,本轮没有安装依赖、生成协议代码、启动服务或浏览器预览。完整源码和输入文件见下方「同源完整文件」;推演结论只适用于写明的模型。

完整错误预算计算器 ​

完整错误预算计算器读取离线请求日志,只统计eligible=true的事件,用整数ppm表示目标避免浮点舍入。坏事件同时包含失败和超时,二者重合只记一次。程序不探测任何服务,也不代表日志来源完整可信。

错误预算预期 ​

错误预算预期在目标990000ppm时,每100个有效请求预算1个坏事件。若实际2个则剩余额度-1,表示预算超支;无流量返回比例null且预算0。负延迟与缺失布尔字段应拒绝,不能让无效日志悄悄改善SLI。

预算与延迟练习 ​

预算与延迟练习给出成功100毫秒、成功301毫秒和失败20毫秒三条有效记录,目标900000ppm。计算好事件与预算,判断预算是否超支;把门槛从300调到500会改变定义,历史比较必须注明新旧口径,不能直接宣称服务改善。

过载接纳完整推演 ​

过载接纳完整推演使用admission-input.json:桶容量2、每秒补2令牌、并发上限1。a在0毫秒消耗1令牌并运行到500,剩1000 credit;同刻b被并发上限拒绝,仍剩1000。到500毫秒a完成,桶补满为2000,c消耗2令牌并运行到600,剩0。600毫秒d到达时并发名额已释放,但仅补到200 credit,被速率限制拒绝;1000毫秒e到达时有1000 credit,接纳后归零,运行到1100。1100毫秒f成本3超过桶容量,直接拒绝,credit为200。预期接纳a/c/e,完成时间500/600/1100;这些结果仅为逐步手算,示例未执行。

边界题:把e的到达改为999毫秒会怎样?从600到999只补798,加原200共998 credit,少于一个令牌,因此拒绝;改回1000恰好接纳。把并发上限改成2时b在0毫秒也能接纳且扣走另一个令牌,到500只补1000,c需要2000就会被rate_limit拒绝,不能沿用原预期。时间倒退、零成本、重复请求ID与非正时长为无效输入,退出2;成本大于桶容量是合法请求但不可接纳的决策。模型无等待队列,拒绝既不占并发也不预订未来令牌,恢复是修改输入另算,不能把无限重试当限流恢复策略。

同源完整文件 ​

以下README含前置、输入、读者自跑步骤、预期结果、失败、恢复与清理;只做静态核对,没有执行。

model.py ​

python
import json
import sys

def budget(events, target_ppm=990000, latency_ms=300):
    if type(target_ppm) is not int or not 0 <= target_ppm <= 1000000:
        raise ValueError('target_ppm out of range')
    total = good = 0
    for event in events:
        if type(event.get('eligible')) is not bool or type(event.get('ok')) is not bool:
            raise ValueError('eligible and ok must be booleans')
        latency = event.get('latency_ms')
        if type(latency) is not int or latency < 0:
            raise ValueError('nonnegative integer latency required')
        if event['eligible']:
            total += 1
            good += int(event['ok'] and latency <= latency_ms)
    allowed = total * (1000000 - target_ppm) // 1000000
    bad = total - good
    return {'total': total, 'good': good, 'bad': bad,
            'ratio': good / total if total else None,
            'allowed_bad': allowed, 'remaining': allowed - bad}

if __name__ == '__main__':
    try:
        data = json.load(sys.stdin)
        print(json.dumps(budget(data['events'], data['target_ppm'])))
    except (ValueError, TypeError, KeyError) as error:
        print(str(error), file=sys.stderr)
        sys.exit(2)

admission.py与具体输入 ​

python
"""Offline token bucket plus concurrency admission. No sleeping or requests."""
import json
import sys


def positive(value, name):
    if type(value) is not int or value <= 0:
        raise ValueError(name + ' must be a positive integer')
    return value


def simulate(data):
    capacity = positive(data['capacity'], 'capacity')
    rate = positive(data['tokens_per_second'], 'tokens_per_second')
    limit = positive(data['concurrency'], 'concurrency')
    # One token equals 1000 credits, so elapsed milliseconds refill exactly.
    credits, last_ms = capacity * 1000, 0
    active, completed, seen, trace = {}, [], set(), []
    for request in data['requests']:
        now, cost, duration = request['at_ms'], request['cost'], request['duration_ms']
        if type(now) is not int or now < last_ms:
            raise ValueError('nondecreasing nonnegative integer timestamps required')
        positive(cost, 'cost')
        positive(duration, 'duration_ms')
        name = request['id']
        if not isinstance(name, str) or not name or name in seen:
            raise ValueError('unique nonempty request ids required')
        seen.add(name)
        finished = sorted((end, key) for key, end in active.items() if end <= now)
        for end, key in finished:
            completed.append(dict(id=key, at_ms=end))
            del active[key]
        credits = min(capacity * 1000, credits + (now - last_ms) * rate)
        last_ms = now
        if cost > capacity:
            reason = 'cost_exceeds_capacity'
        elif len(active) >= limit:
            reason = 'concurrency_limit'
        elif credits < cost * 1000:
            reason = 'rate_limit'
        else:
            reason = 'accepted'
            credits -= cost * 1000
            active[name] = now + duration
        trace.append(dict(id=name, at_ms=now, decision=reason, credits=credits,
                          active=sorted(active), finished_now=[key for _, key in finished]))
    # Completion records are known from admitted durations; no real clock is advanced.
    completed.extend(dict(id=key, at_ms=end) for end, key in sorted((end, key) for key, end in active.items()))
    return dict(trace=trace, completions=completed, pending_at_last_arrival=sorted(active),
                credits_at_last_arrival=credits)


if __name__ == '__main__':
    try:
        print(json.dumps(simulate(json.load(sys.stdin))))
    except (ValueError, TypeError, KeyError) as error:
        print(str(error), file=sys.stderr)
        sys.exit(2)
json
{
  "capacity": 2,
  "tokens_per_second": 2,
  "concurrency": 1,
  "requests": [
    {"id": "a", "at_ms": 0, "cost": 1, "duration_ms": 500},
    {"id": "b", "at_ms": 0, "cost": 1, "duration_ms": 100},
    {"id": "c", "at_ms": 500, "cost": 2, "duration_ms": 100},
    {"id": "d", "at_ms": 600, "cost": 1, "duration_ms": 100},
    {"id": "e", "at_ms": 1000, "cost": 1, "duration_ms": 100},
    {"id": "f", "at_ms": 1100, "cost": 3, "duration_ms": 100}
  ]
}

requirements.txt ​

text
# Python 3.10+ standard library only; no third-party dependency.

README.md ​

md
# 可用性与故障设计:完整示例

示例未执行。核对日期2026-10-17。教学版本族:请求型SLI/错误预算;SRE Workbook2018;Python 3.10+。

## 文件与依赖

文件:model.py、admission.py、admission-input.json、requirements.txt。两个程序均仅依赖Python3.10+标准库,完整源码、输入和依赖声明在实践正文同源展示。

## 输入与前置

前置:先确定纳入统计的请求、成功条件和延迟阈值,再计算比例。使用 Python 3.10+ 标准库,不需要监控服务或压测工具。预算模型输入包含 target_ppm 与 events;具体输入和预期见下节,准入模型另用后文 admission-input.json。

读者将来自跑(本轮未执行):在本目录以Python 3.10+读取input.json,PowerShell示意`Get-Content -Raw input.json | python model.py`;Bash示意`python3 model.py < input.json`。把下面输入手工保存为UTF-8文件,输出按预期逐项比较。无需安装依赖,没有服务或网络调用;程序只读标准输入并写标准输出,不改原文件。失败信息写标准错误且退出2。恢复是修正输入后重读,清理只删除自己创建的input.json与输出文件,勿递归删除工作目录。

## 预期结果、失败与恢复

输入 {"target_ppm":900000,"events":[{"eligible":true,"ok":true,"latency_ms":100},{"eligible":true,"ok":true,"latency_ms":301},{"eligible":true,"ok":false,"latency_ms":20}]};预期total3/good1/bad2/allowed_bad0/remaining-2,ratio约1/3。空events的ratio为null。

失败要保留输入和错误,先与正文的不变量及边界核对。没有真实服务或性能验证,不以这些模型输出证明生产可用性。

## 独立练习

按同一三条日志计算300与500毫秒两种门槛下的结果,并判断预算是否足够。

参考答案:300毫秒时好1坏2;500毫秒时好2坏1。有效总数3且允许比例10%,允许坏事件floor(0.3)=0,两口径都超支。500毫秒使达标数增加是统计定义放宽,不是已证实的延迟下降。

## 完整令牌桶与并发接纳

示例未执行。admission.py只计算给定事件,不睡眠、不发请求、不探测服务。机制参考[Go维护者rate包](https://pkg.go.dev/golang.org/x/time/rate)的单桶速率/突发定义;并发限制是本课附加策略,非RFC2697双桶三色实现。核对日期2026-10-17。

capacity为令牌容量、tokens_per_second为每秒补充率、concurrency为在途上限;三者为正整数。requests按非递减at_ms排列,id唯一、cost和duration_ms为正整数。同刻按输入顺序处理,结束时间等于到达时刻的旧请求先完成;拒绝不消耗令牌、不入队。1令牌=1000 credit,用毫秒补充整数,返回trace中每步decision、credits、active和finished_now,以及所有已接纳请求的推导完成时刻。最终pending_at_last_arrival表示最后到达时仍在途的请求,completions包含按已知duration推导的后续完成;它们不是相同时间切面。

读者未来可在本目录自跑,命令只是文本,本轮未执行:

```text
PowerShell: Get-Content -Raw admission-input.json | python admission.py
Bash: python3 admission.py < admission-input.json
```

输入固定容量2、补充率2/秒、并发1。预期a接纳后credit1000;b同刻因concurrency_limit拒绝且credit1000;500毫秒回收a、补满2000后c消耗2令牌,credit0;600毫秒回收c后d仅有200 credit,被rate_limit拒绝;1000毫秒e恰有1000,接纳后credit0;1100毫秒回收e,f成本3超过容量,被cost_exceeds_capacity拒绝,credit200。完成列表为a500、c600、e1100,最终在途为空。

边界练习:把e到达改为999毫秒。答案:此前600毫秒credit200,补充399×2=798,总998,少2 credit,拒绝;1000毫秒才恰够。若把并发设为2,b也在0接纳,500毫秒只有1令牌,c成本2就被速率拒绝,后续应重新逐步算,不能复用原结果。空requests合法,最终credit2000且无在途;时间倒退、重复ID、零成本为无效输入,退出2。成本超过桶容量是明确拒绝的合法场景,不是等待更久就能通过。

容量练习:平均40毫秒占用一个worker,到达120/秒,目标利用理想能力不超过75%,至少需ceil(120/(25×0.75))=7个worker;8个worker时理想200/秒,按75%接纳150/秒。只计平均且没有测量资源、方差或依赖瓶颈,不能据此承诺生产时延。安全余量50/秒与距当前流量30/秒是不同差值。

修正输入后可重新手推或由读者未来自跑;没有服务/数据需要回滚,清理仅删除自己另存的输出文件。不要把接纳决策算例称为真实压测或已验证容量。

先作答,再核对 ​

按同一三条日志计算300与500毫秒两种门槛下的结果,并判断预算是否足够。

逐步参考解答

300毫秒时好1坏2;500毫秒时好2坏1。有效总数3且允许比例10%,允许坏事件floor(0.3)=0,两口径都超支。500毫秒使达标数增加是统计定义放宽,不是已证实的延迟下降。

沿时间线核对接纳结果 ​

同一时刻先完成再到达是这里的重要规则:500ms 时 a 已完成,c 才能使用唯一并发名额。600ms 时 d 虽有名额,却只有0.2令牌。把“没接纳”细分成成本、并发和速率原因,能避免将所有拒绝误判为同一种容量问题。

查看图表代码

来源与适用边界 ​

继续阅读