← 中文资源

简体中文版本

直方图分位桶

把观测值记入累积直方图桶,并在跨越目标的分位桶内做线性插值。

阅读英文原文

功能说明

该示例把一组观测值归入预定义边界的累积桶,并据此估算分位数。观测值记入第一个上界不小于它的桶,随后把各桶累加成累积计数。求分位时先用 q 乘以总数得到目标秩,找到首个累积计数达到该秩的桶,再在桶的上下界之间线性插值。

关键代码

边界先排序,避免调用方传入乱序导致归类错误。归类循环找首个使样本不大于边界的下标,正无穷桶即最后一个下标。累积化把每个桶变成不大于其边界的样本数,与 Prometheus 的桶语义一致。分位函数在目标所在桶内按“桶内已越过目标的比例”插值,桶计数为零时直接返回上界,避免除零。

使用注意

插值给出的分位是桶内均匀分布的近似,边界过疏会让估算粗糙,边界应按延迟分布合理设置。最末桶若为开区间,超出最大边界的样本被归入上界为“前界两倍”的兜底桶,真实系统应显式设置足够大的末界。分位数无法跨实例平均——平均值可聚合,分位不可——应发布为桶而非已算好的分位值。

代码示例

export interface Histogram { readonly bounds: readonly number[]; readonly counts: readonly number[]; readonly total: number; }

export function observe(bounds: readonly number[], samples: readonly number[]): Histogram {
  const sorted = [...bounds].sort((a, b) => a - b);
  const cumulative = new Array<number>(sorted.length + 1).fill(0);
  for (const sample of samples) {
    let i = 0;
    while (i < sorted.length && sample > (sorted[i] as number)) i += 1;
    cumulative[i] += 1;
  }
  for (let i = 1; i < cumulative.length; i++) cumulative[i] += cumulative[i - 1] as number;
  return { bounds: sorted, counts: cumulative, total: samples.length };
}

export function quantile(hist: Histogram, q: number): number {
  if (q < 0 || q > 1) throw new RangeError('q must be in [0, 1]');
  const target = q * hist.total;
  let prev = 0;
  for (let i = 0; i < hist.counts.length; i++) {
    const count = hist.counts[i] as number;
    if (count >= target) {
      const upper = i < hist.bounds.length ? (hist.bounds[i] as number) : prev * 2;
      const lower = i === 0 ? 0 : (hist.bounds[i - 1] as number);
      const bucketCount = count - prev;
      if (bucketCount === 0) return upper;
      return lower + ((target - prev) / bucketCount) * (upper - lower);
    }
    prev = count;
  }
  return hist.bounds[hist.bounds.length - 1] ?? 0;
}