概念定义
尾延迟常来自方差而非普遍慢:某个副本恰好撞上坏事件。对冲让副本互相比快,单个掉队者不再决定响应时间。
工作机制
先发主请求,按目标分布的某个分位数武装定时器;超时即向另一副本发副本请求;先回者胜,掉队者被丢弃或尽力取消。有界对冲限制可对冲的请求比例,让额外负载保持少数派。
使用场景
对复制存储的读、跨区域缓存查找、掉队者会毁掉整页的延迟关键扇出。配合逐请求截止时间传播,让对冲尊重调用方预算。
常见误区
“对冲总降延迟”的表述要打折:对冲过早等于每层翻倍负载、近乎零收益,而且新增排队本身会抬高它本想削减的尾巴。对冲必须打到不同副本——打回同一个过载池只会放大肇因队列。取消必须真正停止下游工作,否则对冲在看不见的地方持续燃烧 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;
}