负载削减是在全部接收工作会威胁服务时,主动拒绝其中一部分。目标是保留容量,让仍有机会完成的工作继续进行,而不是保证每个获准请求都足够快、都足够重要。拒绝本身也会消耗资源,因此在哪个阶段做准入判断,以及拒绝路径有多贵,与阈值大小同样重要。

保护还能产生价值的容量

过载队列既占内存,也增加等待时间。客户端可能在任务被工作线程取走之前就已经放弃,服务器却继续为无人等待的答案花费资源。更长的队列可以吸收某些突发,但在需求长期超过处理能力时,它不会创造额外容量,只会推迟暴露问题。

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 上运行,只使用标准库。真实线程等待测试协调事件,不访问外部服务,也不制造生产负载。前后两种配置接收的是同样十二个任务的突发,用来比较获准与等待数量。

from concurrent.futures import ThreadPoolExecutor
from threading import Barrier, BoundedSemaphore, Event, Lock


def exercise(gated):
    slots = BoundedSemaphore(3)  # Bounds queued plus running work, not just threads.
    started, release, lock = Barrier(3), Event(), Lock()
    running, began = set(), []

    def held(job):
        with lock:
            running.add(job)
            began.append(job)
        try:
            if job < 2:
                started.wait(timeout=5)
            assert release.wait(timeout=5), "test coordinator did not release workers"
            return job
        finally:
            with lock:
                running.remove(job)

    with ThreadPoolExecutor(max_workers=2) as pool:
        def submit(function, *args):
            if not gated:
                return pool.submit(function, *args)
            if not slots.acquire(blocking=False):
                return None  # Explicit local admission refusal; no future was enqueued.
            try:
                future = pool.submit(function, *args)
            except RuntimeError:
                slots.release()
                raise
            # Completion, failure, and cancellation before execution all return the slot.
            future.add_done_callback(lambda _: slots.release())
            return future

        futures = [submit(held, job) for job in range(12)]
        accepted = [future for future in futures if future is not None]
        replacement = None
        try:
            started.wait(timeout=5)
            with lock:
                assert running == {0, 1} and set(began) == {0, 1}
            assert all(not future.done() for future in accepted)
            if gated:
                assert len(accepted) == 3 and futures[3:] == [None] * 9
                assert futures[2].cancel()  # Both workers are held, so job 2 has not started.
                replacement = submit(held, 12)
                assert replacement is not None
                assert submit(held, 13) is None
                print("gated burst: 2 running + 1 queued, 9 initial refusals; cancellation returns a slot")
            else:
                assert len(accepted) == 12
                print("ungated burst: 2 workers accept 12 unfinished tasks; the other 10 still wait")
        finally:
            release.set()
        completed = [future.result(timeout=5) for future in accepted if not future.cancelled()]
        if replacement is not None:
            completed.append(replacement.result(timeout=5))
        assert sorted(completed) == ([0, 1, 12] if gated else list(range(12)))

        if gated:
            def fail():
                raise ValueError("injected task failure")

            failed = submit(fail)
            assert failed is not None
            try:
                failed.result(timeout=5)
            except ValueError as error:
                assert str(error) == "injected task failure"
            else:
                raise AssertionError("the failed task must preserve its failure")

    if gated:
        # The executor is now closed; a failed submission must not consume a slot.
        try:
            submit(lambda: None)
        except RuntimeError:
            pass
        else:
            raise AssertionError("submission to a closed executor must fail")
        acquired = [slots.acquire(blocking=False) for _ in range(3)]
        try:
            assert acquired == [True, True, True]
            assert not slots.acquire(blocking=False)
        finally:
            for success in acquired:
                if success:
                    slots.release()
        print("completion, task failure, cancellation, and closed-executor submission leave all 3 slots reusable")


exercise(False)
exercise(True)

只有两个工作线程、没有准入门槛时,十二个任务都仍未完成:两个正在运行,十个在等待。增加三个名额后,只接收三个任务,其他九个在提交之前被拒绝。这里获准的是两个运行任务加一个排队任务,不是同时运行三个线程,断言明确区分了两种数量。

取消还在排队的任务,会归还一个名额供替代任务使用,紧接着再提交一个则仍会被拒。协调事件释放线程后,预期任务完成。注入的任务异常和执行器关闭后的提交失败,都保留各自错误;最后通过公开信号量操作确认,三个名额全部可以再次使用。

实验验证的是准入记账与任务生命周期,不是 HTTP 响应、字节限制、优先级调度、生产延迟或自动资源压力控制器。NGINX、Envoy 只作为语义来源被引用,没有在本例中部署或执行。把该机制用于真实服务之前,仍需针对自己的保护对象验证容量与失败路径。

延伸阅读

来源

  • Google SRE:处理过载,关键性、资源信号、拒绝成本与客户端行为,已核验 2026-09-08。
  • Google SRE:处理级联故障,队列上限、负载削减、截止期限与降级,已核验 2026-09-08。
  • Envoy 过载管理器,本地资源保护与上游熔断的区别,已核验 2026-09-08。
  • NGINX 连接限制,请求计数、HTTP/2–HTTP/3 语义与拒绝状态,已核验 2026-09-08。
  • Python futures,任务提交、结果等待超时、取消与完成回调,已核验 2026-09-08。
  • Python 线程工具,非阻塞有界信号量准入与协调原语,已核验 2026-09-08。
  • RFC 9110,服务不可用、Retry-After 与重试安全语义,已核验 2026-09-08。
  • RFC 6585,第四节单独定义 429 与服务器决定的计数政策,已核验 2026-09-08。