← 中文资源

简体中文版本

延迟分位数与直方图

延迟分位数从直方图汇总分布——p50、p99、p999——聚合方式、桶边界与协调性遗漏修正共同决定这些数字是否可信。

阅读英文原文

概念定义

延迟数据集是分布。分位数把分布讲给人和 SLO 听;直方图让分位数可以跨时间跨空间计算,而不必保存每个样本。

工作机制

样本落入对数编码的桶,从微秒到分钟恒定相对误差。分位数从累计计数读出;聚合按合并桶计数进行,跨节点保持正确。协调性遗漏修正补偿“测量循环因慢响应而没发出去的那些请求”,把被循环自己藏掉的尾部找回来。

使用场景

一切服务延迟 SLO、CI 性能闸门、容量看板。导出直方图指标而不是预计算分位数,下游聚合才始终有效;窗口尺寸要与用户体验的时间尺度匹配。

常见误区

分位数不可加也不可平均——全局 p99 必须来自合并直方图,平均各节点 p99 会漏报尾部。桶边界必须对齐阈值,否则 p99 落在过宽桶里靠插值瞎猜。低流量下的超高 分位数是采样伪影,要设最小样本数;小时聚合会藏住分钟级突发,窗口大小必须匹配用户体验的时间尺度。

代码示例

package main

import (
	"fmt"
	"math"
)

type Histogram struct {
	min    float64
	max    float64
	counts []int
	total  int
}

func NewHistogram(minimum, maximum float64, buckets int) *Histogram {
	if buckets <= 0 || maximum <= minimum {
		panic("invalid histogram bounds")
	}
	return &Histogram{
		min:    minimum,
		max:    maximum,
		counts: make([]int, buckets),
	}
}

func (histogram *Histogram) Observe(value float64) {
	if value < histogram.min {
		value = histogram.min
	}
	if value > histogram.max {
		value = histogram.max
	}
	width := (histogram.max - histogram.min) / float64(len(histogram.counts))
	index := int((value - histogram.min) / width)
	if index == len(histogram.counts) {
		index--
	}
	histogram.counts[index]++
	histogram.total++
}

func (histogram *Histogram) Percentile(percentile float64) float64 {
	if histogram.total == 0 {
		return math.NaN()
	}
	rank := int(math.Ceil(percentile / 100 * float64(histogram.total)))
	if rank < 1 {
		rank = 1
	}
	seen := 0
	width := (histogram.max - histogram.min) / float64(len(histogram.counts))
	for index, count := range histogram.counts {
		if seen+count >= rank {
			within := float64(rank-seen) / float64(count)
			return histogram.min + (float64(index)+within)*width
		}
		seen += count
	}
	return histogram.max
}

func main() {
	histogram := NewHistogram(0, 1000, 10)
	for _, value := range []float64{10, 20, 30, 40, 50, 60, 70, 80, 90, 1000} {
		histogram.Observe(value)
	}
	p50 := histogram.Percentile(50)
	p90 := histogram.Percentile(90)
	p99 := histogram.Percentile(99)
	fmt.Printf("p50=%.0f p90=%.0f p99=%.0f\n", p50, p90, p99)
	if !(p50 < p90 && p90 < p99) || p99 != 1000 {
		panic("unexpected percentile estimates")
	}
}
import promClient from 'prom-client';
import express from 'express';

const registry = new promClient.Registry();
promClient.collectDefaultMetrics({ register: registry, prefix: 'svc_' });

const ordersTotal = new promClient.Counter({
  name: 'orders_created_total',
  help: 'Total orders created',
  labelNames: ['tenant', 'channel'],
  registers: [registry],
});

const requestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Request latency distribution',
  buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5],
  labelNames: ['route', 'method'],
  registers: [registry],
});

async function handleCheckout(req, res) {
  const end = requestDuration.startTimer({ route: '/checkout', method: 'POST' });
  try {
    ordersTotal.inc({ tenant: 'acme', channel: 'web' });
    res.json({ ok: true });
  } finally {
    end();
  }
}

const app = express();
app.post('/checkout', handleCheckout);
app.get('/metrics', async (req, res) => {
  res.set('content-type', registry.contentType);
  res.end(await registry.metrics());
});

app.listen(3000);