错误预算与过载接纳推演
前置:理解请求成功/失败、延迟阈值、比例和统计窗口;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
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与具体输入
"""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){
"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
# Python 3.10+ standard library only; no third-party dependency.README.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令牌。把“没接纳”细分成成本、并发和速率原因,能避免将所有拒绝误判为同一种容量问题。
查看图表代码
来源与适用边界
- 英文官方、作者或规范原文:本专题机制依据;按上述固定版本族阅读。
- 中文官方或作者资料:用于术语和讲解交叉核对,冲突以原始规范为准。
继续阅读
- 目标与故障隔离
- 目标与故障隔离