Course Builder个人学习工作台 · 本地模式
DDIA 第二版:从数据模型到分布式系统设计

02 · 把快和可靠写成可验证的要求

目标:写出包含负载条件、测量口径和失败边界的服务目标。

平均快不代表每个用户都快

活动报名的延迟不是一个常数。请求需要经过网络、排队、执行,最后返回。服务时间只覆盖处理本身;响应时间还包括排队等等待。吞吐接近容量时,新增请求可能主要在队列中等待。提高超时时间并没有创造处理能力;超时后的密集重试还会扩大负载。

百分位描述分布。例如 p99 为 800 毫秒,意味着按指定统计方法,约 99% 的观测值不超过这个值。计算必须说明样本、窗口、失败请求如何处理。只统计成功且很快的请求,会把真实问题从仪表盘上删除。多个实例的 p99 也不能简单取平均,应该合并可聚合的分布信息。

我们的候选目标是:“在每秒 200 次报名、热点活动占 40%、持续 10 分钟的负载下,99% 的有效请求在 800 毫秒内成功或明确拒绝;服务错误单独统计。”验证容量需要负载实验,本课只检查统计口径。

手算再运行

保存 requirements.py。先估计均值和 p99 是否会相差很大。这里采用 nearest-rank:排序后取第 ceil(p × N) 个样本。

from math import ceil
from statistics import mean

def percentile(values, p):
    assert values and 0 < p <= 1
    ordered = sorted(values)
    return ordered[ceil(p * len(ordered)) - 1]

response_ms = [20] * 98 + [800, 1200]
print(f"mean={mean(response_ms):.1f} ms")
print(f"p99={percentile(response_ms, 0.99)} ms")
monthly_requests = 1_000_000
allowed_bad = monthly_requests * (1 - 0.999)
print(f"error_budget={allowed_bad:.0f} requests")
assert percentile(response_ms, 0.99) == 800
uv run --python 3.12 --no-project requirements.py

预期输出:

mean=39.6 ms
p99=800 ms
error_budget=1000 requests

98 个快请求把均值拉低了。最后一行是以请求为分母的预算,不等于某个固定停机分钟数。SLI 是测量指标,SLO 是目标,SLA 是与用户约定的服务条款;本课设计的是工程目标。

引导修改:① 把慢请求变成 10 个 800 毫秒,快请求变成 90 个;② 运行;③ 均值应为 98,p99 仍为 800。相同 p99 不代表整体体验相同,至少同时看吞吐、错误率和多个百分位。

可靠性是一组故障承诺

容错是让组件故障不演变成服务失败。三个运行相同错误程序的副本会一起出错,因此硬件冗余不能替代软件验证和分批变更。可维护性还包括可观测性、清楚的接口、可回滚变更和可操作的恢复流程。

RPO 描述故障后允许丢失的数据范围,RTO 描述恢复服务所需时间的目标。例如“已确认报名不丢失,单机故障后 60 秒内恢复写入”远比“可靠”明确,也会要求我们审视确认写入的时机。未来第 11、19 课会回到这些要求。

独立练习:为查询列表写一个响应时间目标,再为误删报名写一个恢复目标。说明日均流量为什么不足以证明它们可实现。

可检查的示例

列表:峰值每秒 1000 次查询、每次返回 20 条、缓存命中率 90% 的条件下,滚动 10 分钟内 p95 不超过 300 毫秒;错误率单列,统计入口处所有请求。误删:恢复到误删前最后一个可用时间点,目标数据损失不超过 5 分钟,2 小时内完成恢复与核对。后者是练习设定,不是产品保证。

日均流量隐藏了热点和突发;恢复目标需要实际备份、日志留存与恢复演练支持。如果同一事故还破坏了备份,必须另设故障边界。

若 p99 得到 1196 一类结果,检查是否用了插值分位数;它可能是另一种合法定义,但与本例口径不同。若压测看似稳定而用户仍慢,检查是否排除了超时样本,以及客户端是否等响应后才发下一次请求,掩盖了排队。

在 design.md 增加负载形状、两个 SLO、RPO/RTO 和需要实测的条件,避免把练习数字写成已验证容量。

参考资料