4. 基于 Prometheus 的 SLI 采集:使用 Histogram 和 Summary 记录请求延迟,使用 Counter 记录错误计数,指标命名规范与标签设计

SLI(Service Level Indicator)是 SLO 的量化基石。Prometheus 通过四种核心指标类型(Counter、Gauge、Histogram、Summary)来采集 SLI 数据。本章聚焦于 请求延迟 和 错误计数 两个最关键的 SLI,讲解如何正确使用 Histogram/Summary 记录延迟分布,使用 Counter 记录错误次数,并遵循 Prometheus 官方命名规范与标签设计原则。

4.1 核心 SLI 指标类型选择

SLI 维度 推荐指标类型 核心用途 典型指标名
请求延迟 Histogram(首选)或 Summary 记录延迟分布,计算 p50/p90/p99 分位数 http_request_duration_seconds
错误计数 Counter 累计错误请求次数,计算错误率 http_requests_total(含 status 标签)
请求总量 Counter 累计总请求次数,作为错误率分母 http_requests_total
最佳实践: 对于延迟 SLI,优先使用 Histogram 而非 Summary。Histogram 允许在 PromQL 层面聚合多个实例的分位数,而 Summary 的分位数只能在客户端计算,无法跨实例聚合。

4.2 使用 Histogram 记录请求延迟

Histogram 将观测值放入预定义的桶(bucket)中,同时记录总计数和总和。以下是一个典型的 HTTP 请求延迟 Histogram 采集示例:

// 定义 Histogram 指标
http_request_duration_seconds = Histogram(
    'http_request_duration_seconds',
    'HTTP request latency in seconds',
    ['method', 'path', 'status'],
    buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
)

// 在请求处理结束时记录延迟
def handle_request(method, path):
    start = time.time()
    try:
        # 处理请求逻辑
        response = process_request(method, path)
        status = response.status_code
    except Exception as e:
        status = 500
    finally:
        duration = time.time() - start
        http_request_duration_seconds.labels(
            method=method, path=path, status=status
        ).observe(duration)

桶(Bucket)设计原则:

  • 桶的范围应覆盖预期的延迟分布,通常从 5ms 到 10s。
  • 在 SLO 目标值附近(如 200ms、500ms)增加桶密度,提高分位数精度。
  • Prometheus 自动添加 +Inf 桶,确保所有观测值都被计数。

4.3 使用 Summary 记录请求延迟(备选方案)

当无法在服务端聚合分位数,或需要精确的滑动窗口分位数时,可以使用 Summary。但注意 Summary 的分位数是客户端计算的,无法跨进程聚合。

// 定义 Summary 指标
http_request_duration_summary = Summary(
    'http_request_duration_summary',
    'HTTP request latency summary in seconds',
    ['method', 'path', 'status'],
    quantiles=(0.5, 0.9, 0.99)
)

// 记录延迟
http_request_duration_summary.labels(
    method='GET', path='/api/users', status='200'
).observe(0.123)
注意: Summary 的 quantiles 参数是固定的,无法在查询时动态调整。如果未来需要新的分位数(如 p95),必须修改代码并重启服务。Histogram 则无此限制。

4.4 使用 Counter 记录错误计数

错误计数通常作为 http_requests_total 的一个标签维度,而不是单独定义一个指标。这样可以通过 PromQL 的 rate() 和 sum() 灵活计算错误率。

// 定义 Counter 指标
http_requests_total = Counter(
    'http_requests_total',
    'Total number of HTTP requests',
    ['method', 'path', 'status']
)

// 在请求结束时递增计数
def handle_request(method, path):
    status = process_request(method, path)
    http_requests_total.labels(
        method=method, path=path, status=status
    ).inc()

错误率计算 PromQL 示例:

# 5 分钟内错误率(status 以 5xx 开头)
rate(http_requests_total{status=~"5.."}[5m])
/
rate(http_requests_total[5m])

4.5 指标命名规范与标签设计

Prometheus 官方命名规范要求指标名遵循 namespace_subsystem_unit_suffix 格式。标签设计应遵循“少而精”原则,避免基数爆炸。

规范维度 规则 正确示例 错误示例
指标名基础格式 namespace_subsystem_unit_suffix http_request_duration_seconds httpLatency
单位后缀 使用 秒 作为时间单位,明确标注 _seconds _seconds, _bytes, _total _ms, _milliseconds
Counter 后缀 Counter 类型指标必须以 _total 结尾 http_requests_total http_requests
标签基数控制 标签值组合数 < 10000,避免高基数标签如 user_id method, path, status user_id, session_id, ip
标签命名 小写字母 + 下划线,语义清晰 http_method, status_code HTTPMethod, sc
标签设计黄金法则: 每个标签的值集合应该是 有限且可枚举 的。例如 method 只有 GET/POST/PUT/DELETE 等少数值;path 应归一化为路由模板(如 /api/users/:id),而不是原始请求路径。

4.6 SLI 采集架构图

下图展示了基于 Prometheus 的 SLI 采集整体架构,从应用埋点到指标存储与查询:

SLI 采集架构:从应用到 Prometheus 应用服务 Histogram / Counter 客户端埋点 /metrics 端点 HTTP 暴露指标 Prometheus Server 拉取 / 存储 / 查询 Grafana / Alertmanager 可视化 / 告警 关键流程说明: 1. 应用服务通过客户端库(如 Prometheus Client)定义 Histogram/Counter 指标。 2. 指标通过 /metrics 端点以文本格式暴露,Prometheus Server 定期拉取。 3. Prometheus 存储指标后,Grafana 用于可视化 SLI 趋势,Alertmanager 用于触发 SLO 告警。

4.7 完整采集示例:结合 Histogram 与 Counter

以下是一个完整的 Python Flask 应用示例,同时采集请求延迟 Histogram 和请求计数 Counter:

from prometheus_client import Histogram, Counter, generate_latest
from flask import Flask, request, Response
import time

app = Flask(__name__)

# 定义指标
REQUEST_DURATION = Histogram(
    'http_request_duration_seconds',
    'HTTP request latency in seconds',
    ['method', 'endpoint', 'status'],
    buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
)

REQUEST_COUNT = Counter(
    'http_requests_total',
    'Total number of HTTP requests',
    ['method', 'endpoint', 'status']
)

@app.before_request
def before_request():
    request.start_time = time.time()

@app.after_request
def after_request(response):
    duration = time.time() - request.start_time
    REQUEST_DURATION.labels(
        method=request.method,
        endpoint=request.path,
        status=response.status_code
    ).observe(duration)
    REQUEST_COUNT.labels(
        method=request.method,
        endpoint=request.path,
        status=response.status_code
    ).inc()
    return response

@app.route('/metrics')
def metrics():
    return Response(generate_latest(), mimetype='text/plain')

@app.route('/api/users')
def get_users():
    return {'users': ['alice', 'bob']}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

4.8 标签设计实战:避免高基数陷阱

标签设计不当会导致 Prometheus 存储爆炸和查询性能下降。以下对比展示了正确与错误的标签设计:

场景 错误标签设计 正确标签设计 原因
HTTP 路径 path="/api/users/123" endpoint="/api/users/:id" 原始路径包含用户 ID,基数无限
HTTP 方法 method="GET"(正确) method="GET" 方法本身基数低,无需修改
HTTP 状态码 status="200"(正确) status="200" 状态码有限,直接使用即可
用户标识 user_id="alice" 不使用 user_id 标签 用户 ID 基数随用户增长,导致高基数
请求来源 IP client_ip="10.0.0.1" 不使用 client_ip 标签 IP 地址基数高,且变化频繁
标签基数检查: 在生产环境中,建议使用 promtool tsdb analyze 命令分析 TSDB 的标签基数。如果某个标签的取值数量超过 10000,应立即优化。

4.9 Histogram 与 Summary 对比总结

对比维度 Histogram Summary
分位数计算位置 服务端(PromQL 计算) 客户端(应用内计算)
跨实例聚合 支持(通过 histogram_quantile) 不支持(只能查看单个实例)
分位数灵活性 查询时动态指定(如 p50/p90/p99) 客户端固定,无法动态调整
存储开销 较高(每个桶一个时间序列) 较低(仅存储分位数、总和、计数)
适用场景 需要聚合多个实例的分位数,或需要灵活查询 无法在服务端聚合,或需要精确滑动窗口分位数
推荐: 在大多数 SLO 场景中,使用 Histogram 采集延迟 SLI。它提供了更好的灵活性和聚合能力,是 Prometheus 官方推荐的方式。