保护你接受的工作
过载时接受所有请求并不能服务所有人,只会让所有人变慢。队列增长、内存上升、超时触发,客户端重试又带来更多负载。减载通过限制在途或等待的请求数量打破循环。被接受的请求保持延迟目标,被拒绝的请求得到快速、明确的响应,从而可以退避。
最简单的形式是并发上限。达到上限时,新工作立即被拒绝,而不是排入无界队列:
export function createAdmission(maxInFlight: number) {
let inFlight = 0;
return async function admit<T>(work: () => Promise<T>): Promise<T> {
if (inFlight >= maxInFlight) {
const error = new Error('overloaded');
(error as Error & { status?: number }).status = 503;
throw error;
}
inFlight += 1;
try {
return await work();
} finally {
inFlight -= 1;
}
};
}
这段代码假设单进程。多实例部署时,每个实例应用自己的限制,网关可以再施加全局限制。正确的上限来自在延迟目标下测得的并发数,而不是猜测。先从服务实际能使用的 worker 或数据库连接数开始,再通过负载测试调整。
选择信号
并发数是好的默认值,因为它直接限制资源使用。队列长度可以发现工作在运行前的等待。CPU 或事件循环延迟能发现并发数看不出的饱和,尤其是 CPU 密集型处理器。内存压力可以在进程被杀之前触发减载。信号应带滞回,单个尖峰不应让策略反复开关。
优先级类别很重要。结账请求和夜间报表不应平等竞争。可以为交互路径预留容量,在交互类别接近上限时拒绝或延迟批处理。优先级必须来自认证身份,而不是客户端提供的头。
用正确的响应拒绝
临时过载使用带 Retry-After 的 503 Service Unavailable;特定客户端超过配额时使用 429 Too Many Requests。区别很重要:429 表示调用方速率问题,503 表示服务器容量问题。返回简短、机器可读的响应体,并记录带原因的减载计数。
客户端必须带抖动指数退避重试。立即重试会把短暂过载变成持续过载。非幂等请求需要幂等键,避免在部分工作完成后重试造成重复副作用。
权衡与验证
减载让部分请求失败,以换取其余请求成功。这是产品决策,不只是基础设施决策。定义哪些类别可以减载、用户看到什么、系统如何恢复。用超过容量的负载测试策略:被接受请求的 p95 应保持目标内,被拒绝请求快速返回,负载下降后无需重启即可恢复。同时监控减载率、在途数、队列深度和延迟;延迟尖峰时减载率接近零,说明策略采样太晚。
何时使用
当服务有硬并发限制,且延迟 SLO 必须在流量尖峰中存活时,使用减载。优点是有界资源使用;缺点是部分调用方按设计收到错误。与限流相比,减载响应的是服务器当前饱和度,而不是客户端长期配额;与自动扩缩容相比,它在毫秒级生效。两者互补而非竞争。