← 中文资源

简体中文版本

对冲请求

对冲请求在延迟阈值后向另一副本发出重复请求、取先到者,用双倍负载的代价削减尾延迟。

阅读英文原文

概念定义

尾延迟常来自方差而非普遍慢:某个副本恰好撞上坏事件。对冲让副本互相比快,单个掉队者不再决定响应时间。

工作机制

先发主请求,按目标分布的某个分位数武装定时器;超时即向另一副本发副本请求;先回者胜,掉队者被丢弃或尽力取消。有界对冲限制可对冲的请求比例,让额外负载保持少数派。

使用场景

对复制存储的读、跨区域缓存查找、掉队者会毁掉整页的延迟关键扇出。配合逐请求截止时间传播,让对冲尊重调用方预算。

常见误区

“对冲总降延迟”的表述要打折:对冲过早等于每层翻倍负载、近乎零收益,而且新增排队本身会抬高它本想削减的尾巴。对冲必须打到不同副本——打回同一个过载池只会放大肇因队列。取消必须真正停止下游工作,否则对冲在看不见的地方持续燃烧 CPU;与重试叠加使用时,还要防住故障期叠出指数级请求树。

代码示例

export interface HedgedRequestOptions {
  delayMs: number;
  maxAttempts?: number;
  signal?: AbortSignal;
}

export async function hedgedRequest<T>(
  attempt: (signal: AbortSignal, attemptIndex: number) => Promise<T>,
  options: HedgedRequestOptions,
): Promise<T> {
  if (typeof attempt !== "function") throw new TypeError("attempt must be a function");
  if (!Number.isFinite(options.delayMs) || options.delayMs < 0) {
    throw new RangeError("delayMs must be a non-negative finite number");
  }
  const maxAttempts = options.maxAttempts ?? 2;
  if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
    throw new RangeError("maxAttempts must be a positive integer");
  }
  const external = options.signal;
  if (external?.aborted) throw external.reason;

  return new Promise<T>((resolve, reject) => {
    const controllers = new Set<AbortController>();
    const timers = new Set<ReturnType<typeof setTimeout>>();
    const failures: unknown[] = [];
    let launched = 0;
    let settled = 0;
    let finished = false;

    const cleanup = (): void => {
      for (const timer of timers) clearTimeout(timer);
      timers.clear();
      external?.removeEventListener("abort", onExternalAbort);
      for (const controller of controllers) controller.abort();
      controllers.clear();
    };

    const finish = (callback: () => void): void => {
      if (finished) return;
      finished = true;
      cleanup();
      callback();
    };

    const onExternalAbort = (): void => {
      finish(() => reject(external?.reason ?? new Error("hedged request aborted")));
    };

    const launch = (): void => {
      if (finished || launched >= maxAttempts) return;
      const index = launched;
      launched += 1;
      const controller = new AbortController();
      controllers.add(controller);
      if (external?.aborted) controller.abort(external.reason);

      Promise.resolve()
        .then(() => attempt(controller.signal, index))
        .then(
          (value) => finish(() => resolve(value)),
          (error) => {
            if (finished) return;
            failures.push(error);
            settled += 1;
            if (settled === launched && launched === maxAttempts) {
              finish(() => reject(new AggregateError(failures, "all hedged attempts failed")));
            }
          },
        );

      if (index + 1 < maxAttempts) {
        const timer = setTimeout(() => {
          timers.delete(timer);
          launch();
        }, options.delayMs);
        timers.add(timer);
      }
    };

    external?.addEventListener("abort", onExternalAbort, { once: true });
    launch();
  });
}
export function retryDelay(header, { now = Date.now(), maxDelayMs = 60000, fallbackMs = 1000 } = {}) {
  if (header === undefined || header === null || String(header).trim() === '') return fallbackMs;
  const raw = String(header).trim();
  if (/^\d+$/.test(raw)) {
    return Math.min(Number(raw) * 1000, maxDelayMs);
  }
  const when = Date.parse(raw);
  if (Number.isNaN(when)) return fallbackMs;
  return Math.min(Math.max(when - now, 0), maxDelayMs);
}

export async function fetchWithRetry(url, options = {}, maxAttempts = 3) {
  let lastError;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fetch(url, options);
    if (res.status !== 429 && res.status < 500) return res;
    if (attempt === maxAttempts) return res;
    lastError = res;
    const delay = retryDelay(res.headers.get('retry-after'));
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
  throw lastError;
}