架构图写着“p99 小于 200 ms”,监控却只收成功请求的延迟;十个请求里有一个超时,图表仍然可能全部变绿。可观测性首先是为设计结论提供反例渠道,其次才是把组件指标画成仪表盘。

本篇沿内容分享的发布、读取和异步通知路径定义测量契约。讨论对象是候选后端方案,不涉及监控产品选型或真实生产成本报价。关键不变量是撤销后不能返回内容;服务目标是读取三十天有效请求成功率 99.9%、服务端 p99 小于 200 ms、通知入队后 99% 在五秒内完成。目标和实验观察分别记录。

五类指标对应五种反驳

Google SRE 的监控章节提出延迟、流量、错误和饱和度四类信号,并区分成功与失败延迟。本题还需要业务正确性:返回了已撤销内容的请求可能是 HTTP 200,低延迟、高可用,却仍违反设计。

设计主张 测量位置与分母 能推翻它的观测
读取足够快 网关收到请求至写完响应,超时单列 p99 超目标或超时比例上升
通知不会持续积压 入队至最终处理,包含仍未完成者 最老任务年龄增长
故障不被重试放大 同一逻辑请求下游尝试总数 尝试数增长而成功数不增
撤销后不可见 撤销确认后的受控新读 返回正文或可访问旧 URL
扩容值得成本 每个成功业务动作的分摊成本 总成本上升但有效吞吐不变

读取失败不能从成功率分母里删除。非法路径、已失效分享的预期拒绝和负载保护拒绝分别计数;负载保护若针对原本应成功的有效业务请求,仍属于可用性损失。内部“服务已收到请求”的 SLI 也不能替代客户端 DNS、TLS 和接入失败的体验,需要外部探针补充。

接口为 GET /shares/{id},沿 request_id 关联网关与存储调用;内部事件采用 RequestFinished(route, outcome, elapsed_ms, attempts, bytes)。指标标签只留归一化路由、地域和有限结果集,具体 request_id 放日志或 trace。数据模型必须有逻辑请求与尝试两个层次,否则一次超时后重试三次会被当作三名用户的访问。

为什么平均延迟和采样 trace 都不能独立验收

固定一百次请求,其中九十五次 100 ms,最后五次 2000 ms 并超时。完整样本按 nearest-rank 算 p99 是 2000 ms;只看成功请求得到 100 ms。如果每十条保留一条,样本恰好取 id=0、10、…、90,得到的 p99 仍为 100 ms。这不是随机采样必然失真,而是展示确定性采样可能与周期性故障相关。

基于请求结束的计数器与直方图应尽量覆盖全部请求;trace 则用于解释个案和调用关系。高错误率时可以保留更多错误 trace,但带偏采样不能直接拿来计算总体错误率。采用 tail sampling 还会等待 trace 完整到达,采集器内存、丢包和超长 trace 都会影响可见性。验证 SLO 时应从独立计数来源检查采集覆盖率。

直方图桶也不是精确原始数据。若 100 ms 至 1 s 之间只有一个桶,200 ms 阈值附近就不能获得足够分辨率。多实例聚合应合并桶计数再估计百分位,而不是平均各实例 p99。若用户体验取决于下载结束,网关仅量“写入内核缓冲区”还可能低估慢客户端;测量端点必须随结论一起出现。

flowchart LR
    U[外部探针与真实用户] -->|读取分享| G[网关]
    G -->|查状态与正文| D[(权威库)]
    G -.->|全量结果计数与延迟桶| M[指标汇总]
    G -.->|按策略采样调用链| T[Trace 存储]
    D -.->|审计版本| A[正确性核对器]
    A -->|撤销后新读探针| G
    M -->|窗口 SLI 与成本| R[设计评审]
    T -->|失败个案| R

积压要带年龄,成本要带成功动作

队列长 1200 条是否严重,取决于每秒处理能力和业务期限。假设输入 120 task/s、输出 100 task/s,六十秒后多积压 1200 条;只看消费者 CPU 50% 并不能排除下游限额成为瓶颈。若恢复到输入 80、输出 100,净消减仅 20 task/s,需要另外六十秒消化积压。此时“服务端处理一次只用十毫秒”也不能说明通知在五秒内送达。

至少保留入队时间、首次开始时间、最后尝试时间和终态时间。未完成项的年龄分布要单独展示,避免只统计已完成任务导致幸存者偏差。死信队列不是成功终态;业务是否允许放弃必须单列。恢复成功的证据应同时包括流入流出差、最老年龄回落,以及正确性核对没有重复副作用。

教学流量按每天一千万次访问、400 B 原始诊断记录、七天保留:10000000×400×7=28 GB,两份则 56 GB。假定仅原始存储单价 0.02 成本单位/(GB·月),单份对应 0.56 成本单位/月;这不是云厂商报价,索引、查询、复制与采集计算另计。若全保留 2 KiB trace,日原始量为 20.48 GB;采样 1% 后约 0.2048 GB/日,但不能用节省的存储来证明 SLO 可观测性没有损失。

日均约 115.74 request/s,十倍峰值 1157.4 request/s;200 ms 平均在途预算约 231.5 个,前提是稳定系统且这里使用平均延迟。p99 不能直接代入 Little 定律当平均值。若峰值持续积压,此时还需显式计算排队,不再满足简单稳态解释。

成本除以请求总数容易被失败重试“改善”。设一天成本 100 单位、100 万逻辑请求成功,单位成功动作成本是 0.0001。若重试导致总尝试两百万但成功数仍一百万,按尝试数分摊看似减半,实际用户收益没有变化。成本归因采用租户和功能级汇总,高基数的单请求详细账本可放离线存储,不放每个监控标签。

sequenceDiagram
    participant C as 客户端
    participant G as 网关
    participant D as 存储
    participant M as 指标
    C->>G: 一个逻辑请求 R1
    G->>D: attempt 1
    D--xG: 超时
    G->>D: attempt 2,剩余预算内
    D-->>G: 成功
    G-->>C: 返回结果
    G->>M: logical_total 加 1
    G->>M: attempts_total 加 2
    Note over G,M: 若最终超时,也计入有效请求分母

告警和回退怎样关联到设计

初版直接记录全量计数、有限桶直方图和少量 trace,不引入自动根因推断。多窗口错误预算消耗可触发告警,但安全与正确性事件不能等错误预算用完:一次确认后的撤销泄露就应进入独立处理路径。反之,CPU 短暂升高而关键业务没有受影响,未必值得立即叫醒值班人员。

采集故障也需要自己的信号:心跳缺失、样本计数骤降、导出队列饱和、时钟跳变。观察链路停止不能解释为业务错误率归零。回退条件可以写为“新版本有效请求错误率持续超过基线且原始样本确认来自该版本”,但自动回退前要核对数据库迁移兼容性;错误指标不能替代状态兼容判断。

运行 python3 examples/system-design/labs/32/run.py,输出完整 p99=2000 ms、偏置样本 p99=100 ms、错误率 5%、正确性违反一条、六十秒积压 1200 条。它是固定账本与算术模型,不包含真实监控采集器、trace 后端或压测;原始输出、版本、命令及源码哈希在 examples/system-design/evidence/32/。负例的价值是让“只采成功请求也能验收”这一主张被明确推翻。

面试追问:若用户说很慢但服务端 p99 正常,应增加哪个测量位置;若通知完成延迟正常但最老任务一直增长,哪些样本被遗漏;若降低采样率省了费用,什么指标仍必须全量保留。答案应落实到分母、窗口和数据路径。

[PATTERN] 每个设计承诺配一个能使其失败的观测,再估算观测本身的成本。收集更多数据不是目标,保留能区分正确与错误的证据才有价值。

实验附件与导航

可运行实验源码 · 本次原始结果

系列导读;容量和数据承诺分别沿用系列的方法,本文数字为独立教学假设。