负载削减保护系统
负载削减是在全部接收工作会威胁服务时,主动拒绝其中一部分。目标是保留容量,让仍有机会完成的工作继续进行,而不是保证每个获准请求都足够快、都足够重要。拒绝本身也会消耗资源,因此在哪个阶段做准入判断,以及拒绝路径有多贵,与阈值大小同样重要。 保护还能产生价值的容量 过载队列既占内存,也增加等待时间。客户端可能在任务被工作线程取走之前就已经放弃,服务器却继续为无人等待的答案花费资源。更长的队列可以吸收某些突发,但在需求长期超过处理能力时,它不会创造额外容量,只会推迟暴露问题。 Google SRE 讨论了队列限制、资源压力信号、选择性拒绝和降低响应质量。削减与降级相关,但并不相同:拒绝一个请求,不等于完成一个较便宜的版本。后者必须有清楚的产品契约,说明剩下的结果仍有什么价值,不能只是悄悄少做一部分工作。 不同保护机制也可能针对不同组件。Envoy 过载管理器保护代理自身资源,而它的熔断机制主要保护上游服务。依赖熔断器不会自动解决本地内存、连接或文件描述符耗尽,所以不能只看到请求被快速拒绝,就认定整个入口已经免于过载。 选择有依据的政策 按优先级拒绝,可以保护关键业务路径,但 HTTP 方法或是否容易重试,本身并不能决定重要性。一次读取可能非常关键,一次写入也可能只是允许丢弃的遥测。应使用可信的应用分类,而不是任由调用方把自己的所有请求都声明成最高优先级。 统一准入上限或按比例拒绝,也可以是合理政策,不能因为没有业务优先级就一概认为它们错误。需要明确公平性与容量之间的取舍,包括低优先级工作是否可能长期得不到执行,以及不同租户是否共用同一个限制。拒绝顺序应该服务于真实契约,而不是凭方法名称猜测。 请求数量、字节、执行时间和 CPU 成本是不同预算。只限制三个任务,如果每个任务都可以保留任意大的正文,仍然不能约束内存。应选择与受保护资源对应的信号,并通过真实请求组合验证行为,不能把单一计数上限解释成对所有资源的统一保证。 限制准入,而不只是线程数量 线程池的工作线程上限约束同时执行的任务数,不一定限制已经接受、正在排队的工作。只在工作线程内部获取信号量,会让它前面的队列继续增长。应在昂贵工作提交之前做有界准入,并在任务生命周期真正结束时归还容量,而不是等到执行阶段才开始计数。 名称类似的代理限制,也需要精确理解。NGINX limit_conn 按文档规定的连接规则计数;在 HTTP/2、HTTP/3 中,每个并发请求都被单独计入。不能默认某个“连接数上限”始终只是在统计 TCP socket,否则复用连接下的真实请求并发可能与预想完全不同。 下面的实验同时约束排队和运行中的任务。被拒提交不会创建 future;完成回调会在成功、失败或执行前取消后归还名额,而向已经关闭的执行器提交失败,也会归还尚未使用的名额。这样准入预算对应完整生命周期,而不是只覆盖正常结束的路径。 坏掉的样子 如果已经解析巨大正文、打开数据库连接或执行了业务写入,才开始拒绝,可能已经来不及保护目标资源。昂贵的日志和错误响应生成,也可能让拒绝路径本身过载。应一起观察拒绝成本、获准吞吐量、排队时长、活动工作量与资源压力,而不是仅统计拒绝了多少次。 HTTP 503 可以表达服务暂时不可用,429 则表示调用方在某种政策下请求过多。Retry-After 只是等待信号,不证明重放写操作安全,也不保证之后一定存在容量。立即重试可能把刚削减的负载原样加回来,因此拒绝与客户端行为必须放在同一条链路中理解。 不要仅因为调用方停止等待,就提前回收仍在运行任务的名额。等待 future 结果超时,不会终止工作线程。提前归还会让新任务与实际上尚未结束的旧任务重叠,破坏原先承诺的容量上限;资源记账应以工作真实结束为依据。 快速验证 保存为 shedding_check.py,用 Python 3.10 以上版本执行 python3 shedding_check.py。实验已在 Python 3.14.4 上运行,只使用标准库。真实线程等待测试协调事件,不访问外部服务,也不制造生产负载。前后两种配置接收的是同样十二个任务的突发,用来比较获准与等待数量。 ...