CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
可观测性入门/课时/指标与 SLI
RawBlame
课时目录12 节

可观测性入门

  1. 01可观测性三支柱
  2. 02指标与 SLI
  3. 03结构化日志
  4. 04分布式链路追踪
  5. 05插桩与埋点
  6. 06监控与告警
  7. 07遥测与 OpenTelemetry
  8. 08Spring 可观测性
  9. 09SLO 与错误预算
  10. 10仪表盘设计
  11. 11故障排查流程
  12. 12综合练习

指标与 SLI

Counter/Gauge/Histogram;衔接 prom。

30 分钟

指标与 SLI

这一课你将能做到

  1. 区分 Counter / Gauge / Histogram / Summary 四种指标类型,说清各自适合测什么、用错会怎样
  2. 为判题接口写出 3 条 SLI(可用率、延迟、错误率),每条都有明确的分子和分母
  3. 用 PromQL 写出 QPS、错误率、P99 延迟查询,并解释高基数标签为什么会拖垮时序库

为什么要学

没有指标,告警和 SLO 都无从谈起;选错指标类型,数字会「看起来有、算起来错」。典型翻车现场:把 Counter 的累计值当瞬时 QPS 直接配阈值告警,结果服务重启归零后告警狂叫又自动恢复;只看平均延迟觉得「系统很健康」,实际 5% 的用户判题结果等了 60 秒。

指标(Metrics) 把系统行为压缩成可聚合的数值时序;SLI(Service Level Indicator,服务水平指标) 是其中「对用户真正重要」的那几条。面试里「你们系统怎么度量健康」「P99 和平均值的区别」「标签基数怎么回事」都属于本课射程,是必须背到脱口而出的程度。

先建立图像

类型行为(人话)典型用途常见坑
Counter只增不减的累计计数器请求总数、错误次数、判题提交量拿裸累计值当「当前速率」
Gauge可升可降的瞬时值队列长度、连接池占用、在线 worker 数对它用 rate()(通常不对)
Histogram把观测值丢进一组桶延迟、响应体大小 → 算 P95/P99桶边界和业务延迟量级不符
Summary客户端算好分位数少见;一般改用 Histogram分位数无法跨实例再聚合
一次判题提交(原始事件)
   ├─ Counter:   judge_submissions_total{result="accepted"} += 1
   ├─ Gauge:     judge_queue_depth = 42(Kafka 待判题消息数,瞬时值)
   └─ Histogram: judge_duration_seconds 观察本次判题耗时

SLI 不是「再采一套新指标」,而是从已有信号里选出用户最在意的定义:
  可用率 = 非 5xx 请求数 / 总请求数
  错误率 = 5xx 请求数 / 总请求数
  延迟   = P99 是否低于约定阈值(如提交接口 < 500ms)

跟着做

步骤 1:给判题接口选定三条 SLI

针对 POST /api/judge/submissions,在记事本写下:

SLI-1 可用率 = status!~"5.." 的请求数 / 总请求数(滚动 5 分钟窗口)
SLI-2 延迟   = http_server_requests_seconds 的 P99 < 500ms
SLI-3 错误率 = status=~"5.." 的请求数 / 总请求数

关键要求:每条 SLI 都有明确的分子和分母。写不出分子分母的「指标」只是图表,不是 SLI。

步骤 2:用 Micrometer 埋一个业务 Counter 和 Timer

在判题提交服务里:

@Service
public class SubmissionService {

    private final MeterRegistry registry;

    public SubmissionService(MeterRegistry registry) {
        this.registry = registry;
    }

    public SubmissionDto submit(SubmitRequest req) {
        // Counter:判题提交总量,标签只用低基数的 language
        registry.counter("judge_submissions_total",
                "language", req.getLanguage())   // java/python/cpp,种类有限
            .increment();

        // Timer:记录判题创建耗时(Timer 底层就是 Histogram)
        return Timer.builder("judge_submission_create")
            .register(registry)
            .record(() -> doSubmit(req));
    }
}

启动后访问 http://localhost:8080/actuator/prometheus,搜索 judge_submissions_total 应能看到按 language 分开的计数行。

步骤 3:在 Prometheus 里写三条核心 PromQL

-- QPS(注意:对 Counter 必须用 rate,不能用裸值)
sum(rate(http_server_requests_seconds_count{uri="/api/judge/submissions"}[5m]))

-- 错误率(5xx 占比)
sum(rate(http_server_requests_seconds_count{uri="/api/judge/submissions", status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{uri="/api/judge/submissions"}[5m]))

-- P99 延迟(对 Histogram 用 histogram_quantile)
histogram_quantile(0.99,
  sum(rate(http_server_requests_seconds_bucket{uri="/api/judge/submissions"}[5m])) by (le)
)

把三条都跑一遍,对照 Grafana 图确认数值合理。rate() 的 [5m] 是回看窗口:窗口太短曲线毛刺大,太长反应迟钝,5m 是常用起点。

步骤 4:做一次「高基数」体检

列出你想加的标签:method、status、uri、language、userId、submissionId、rawUrl、exceptionMessage。

逐个问:这个标签有多少种可能取值?

  • language:3~5 种 → 安全
  • uri:必须用模板 /api/judge/submissions,不能是 /api/judge/submissions/12345
  • userId / submissionId:几万到几百万种 → 禁止
  • exceptionMessage:文本千变万化 → 禁止(这类细节放日志字段)

应得到:一张标签白名单。每个标签组合都会生成一条独立时序,基数爆炸 = 时序数量爆炸 = 内存、存储、查询全崩。

步骤 5:把 SLI 挂到 SLO 入口

写一句话:「若 SLI-1 在 30 天滚动窗口低于 99.9%,则开始消耗错误预算」。错误预算的算法和告警在第 9 课展开,本课只要建立 SLI → SLO 的挂钩意识:先测得准,才谈得上承诺。

讲清楚要点

  1. Counter 是累计值,所以看速率必须用 rate() / increase()——裸值直接比大小是初学者最常见的错误。
  2. Gauge 表示瞬时状态,适合连接池、队列深度——不适合回答「过去 5 分钟错了几次」这类累计问题。
  3. 延迟是分布而不是单个数字,所以要看 P95/P99 而不是平均值——平均值会掩盖长尾:100 个请求 95 个 10ms、5 个 10s,平均值约 510ms 看起来还行,但那 5 个用户已经流失了。
  4. Histogram 的分位数在服务端聚合算,Summary 在客户端算死——跨多实例聚合分位数必须用 Histogram,这是两者选型的唯一硬标准。
  5. SLI 服务用户体验,所以优先选「请求成功与否、花了多久」——JVM 堆、CPU 是因果线索,不是用户承诺本身。
  6. 每个标签组合生成一条新时序,所以标签设计首先是基数设计——白名单制度,细节交给日志和链路。
  7. 多副本各自暴露指标,聚合时按 application 等公共标签 sum——否则你看到的是「某个 Pod 的 QPS」而不是服务的 QPS。

你可能会懵的地方

  • Counter 重启归零,是不是指标坏了? 不是。rate() 内部会检测重置并正确处理。你要关心的是「单位时间增量」,不是裸计数本身。这也是不能直接对 Counter 配「> 10000 告警」的原因之一。

  • Histogram 和 Summary 到底怎么选? 需要在 Prometheus 里跨实例聚合 P99 → Histogram。Summary 的分位数在应用内算好,两个实例的 P99 无法合并成整体 P99。Spring/Micrometer 默认走 Timer → Histogram 语义,跟着默认走就对。

  • rate() 窗口怎么选? 至少取 scrape 间隔的 4 倍(如 15s 间隔用 ≥1m),告警常用 5m。窗口小于间隔会得到锯齿状假数据。

  • SLI 和「有用的指标」有什么区别? SLI 是你愿意写进 SLO、用来做发布决策的那几条。磁盘 inode 很重要,但用户感知不到,通常不是面向用户的 SLI,而是排障线索。

  • 为什么 uri 标签必须用模板? /api/judge/submissions/12345 每个 ID 一条时序,基数随业务量爆炸。Spring 的 MVC 插桩默认已用模板化 URI;如果你看到原始 URL 出现在标签里,多半是自定义了错误的 uri 标签。

错法对照

错法问题正解
用平均延迟当唯一健康标准长尾用户被平均值掩盖至少看 P95/P99 + 错误率
userId、完整 URL 做指标标签时序爆炸,拖垮 Prometheus低基数标签白名单,细节进日志
Counter 裸值配阈值告警重启归零、语义错误先 rate()/increase() 再比阈值
只采机器 CPU,不采 HTTP 指标资源正常但接口已挂服务 RED 指标优先,资源作辅助
给每个内部方法建 Timer指标爆炸、维护噩梦框架自动插桩打底,手动只埋业务决策点

卡点急救

  1. histogram_quantile 查出来是 NaN 或平直不动:检查是否忘了 by (le) 聚合——桶标签 le 必须保留;再确认 Prometheus 版本 ≥ 2.x 且指标确实是 _bucket 结尾的 Histogram。
  2. rate() 曲线出现莫名尖刺/断裂:多半是窗口内发生了 Counter 重置(服务重启、Pod 重建)。属正常现象,告警加 for 持续时间消化。
  3. 指标在 /actuator/prometheus 有,Prometheus 里没有:先看 Status → Targets 是否 UP;再查标签是否写错——Micrometer 的 judge_submissions_total 导出后可能带 _total 后缀,别搜错名。

面试常问

  • P99 和平均延迟有什么区别?为什么看 P99? 背这个版本:平均值把所有请求拉平,长尾被多数快请求稀释;P99 表示 99% 的请求都慢于这个值的反面——即最慢的 1% 用户等待多久。大流量下 1% 就是每天成千上万次坏体验。工程实践:接口延迟看 P95/P99,配合 P50 看整体水位;告警用 P99 + 错误率,不用平均值。

  • Prometheus 四种指标类型区别? Counter 只增累计(请求数),用 rate 看速率;Gauge 瞬时可升降(队列深度、连接数);Histogram 服务端分桶,可跨实例聚合出 P95/P99;Summary 客户端算分位数,不可跨实例聚合,一般优先 Histogram。

  • 什么是高基数问题?怎么避免? 每个标签组合生成一条独立时序。userId 这种百万级取值的标签会让时序数量爆炸,Prometheus 内存和查询都被拖垮。避免:标签白名单制度,取值必须是有界枚举;uri 用路由模板;高基数字段(userId、订单号、异常文本)只进日志和 Trace 属性,不进指标标签。

自测三问

  1. Kafka 判题队列的堆积深度该用 Counter 还是 Gauge?为什么?
  2. 写出你为判题提交接口选的三条 SLI,各自的分子分母是什么?
  3. 为什么不能把 submissionId 加为指标标签?正确的承载位置是哪里?

小结一句话

先选对指标类型,再定义用户视角的 SLI——数字才会服务于告警、SLO 与排障,而不是制造错觉。

延伸

  • Prometheus:Metric types
  • OpenTelemetry:Metrics 概念
  • Micrometer Concepts
  • 下一课《结构化日志》:让每一行日志都能被检索、被关联、被信任。
加载本节练习…
← 上一课可观测性三支柱下一课 →结构化日志