工作机制
头部采样在请求开始时决定,通常用 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;
}
}