← 中文资源

简体中文版本

遥测采样

遥测采样不保留每个事件,而是按比例或规则保留有代表性的链路、日志或指标子集,在控制成本的同时维持可用的可观测性。

阅读英文原文

工作机制

头部采样在请求开始时决定,通常用 trace ID 哈希对照采样率,使所有服务做出一致决定并保持链路完整。尾部采样在收集器缓冲整条链路,看到结果后再决定,因此可以保留全部错误和慢请求,丢弃常规流量。指标通常通过加权聚合而非采样。

适用场景与取舍

当遥测数据量超出预算或后端容量时,尤其是高吞吐链路和 debug 日志,采样是合适手段;账单、审计和安全记录必须完整,不能采样。与盲目丢弃相比,有文档的采样策略会保留已知重要事件并记录采样率。代价是稀有事件可能消失,仪表盘需要按采样率加权,否则会低估;尾部采样还需要缓冲和足够的收集器容量。

具体示例

结算服务在 SDK 层采样百分之一的链路,同时在收集器保留所有包含错误的链路。延迟飙升时,工程师仍能完整看到失败请求,并通过 trace ID 关联到采用相同采样决策的日志。

常见误解与关联概念

采样不是聚合,百分之一的样本若不加权就无法给出准确错误总数;只保留错误也不够,还需要成功流量的基线。采样不能替代告警,检测应基于完整计数器而非采样后的 span。遥测管道集中执行采样,分布式追踪依赖一致的采样决策,日志级别决定哪些记录进入采样范围。

代码示例

export type EventMap = Record<string, unknown>;

export class TypedEmitter<TEvents extends EventMap> {
  private readonly listeners = new Map<keyof TEvents, Set<(payload: never) => void>>();
  private readonly maxListeners: number;

  constructor(options: { maxListeners?: number } = {}) {
    this.maxListeners = options.maxListeners ?? 10;
    if (!Number.isInteger(this.maxListeners) || this.maxListeners < 1) {
      throw new RangeError("maxListeners must be a positive integer");
    }
  }

  on<TKey extends keyof TEvents>(
    event: TKey,
    listener: (payload: TEvents[TKey]) => void,
  ): () => void {
    let set = this.listeners.get(event);
    if (!set) {
      set = new Set();
      this.listeners.set(event, set);
    }
    if (set.size >= this.maxListeners) {
      throw new Error(`listener limit ${this.maxListeners} exceeded for event ${String(event)}`);
    }
    const wrapped = listener as (payload: never) => void;
    set.add(wrapped);
    return () => this.off(event, listener);
  }

  off<TKey extends keyof TEvents>(
    event: TKey,
    listener: (payload: TEvents[TKey]) => void,
  ): void {
    const set = this.listeners.get(event);
    if (!set) return;
    set.delete(listener as (payload: never) => void);
    if (set.size === 0) this.listeners.delete(event);
  }

  emit<TKey extends keyof TEvents>(event: TKey, payload: TEvents[TKey]): void {
    const set = this.listeners.get(event);
    if (!set) return;
    // snapshot so removal during dispatch does not skip listeners
    for (const listener of [...set]) listener(payload as never);
  }

  listenerCount(event: keyof TEvents): number {
    return this.listeners.get(event)?.size ?? 0;
  }
}
export class LogSampler {
  constructor({ rate = 0.01, alwaysKeep = () => false } = {}) {
    this.rate = rate;
    this.alwaysKeep = alwaysKeep;
    this.seen = new Set();
  }

  shouldEmit(signature) {
    if (this.alwaysKeep(signature)) return true;
    if (!this.seen.has(signature)) {
      this.seen.add(signature);
      return true;
    }
    return Math.random() < this.rate;
  }

  emit(signature, write) {
    if (!this.shouldEmit(signature)) return false;
    write({ signature, sampledRate: this.rate });
    return true;
  }
}