Skip to content

[Server Architecture][Admission][Compression] 统一请求资源准入、两阶段 Call Reservation 与 Decode Execution #273

Description

@SunSi12138

背景 / 为什么需要这个架构 issue

关联:

#244 的本质不是单纯“让 rejected request 更快”,而是:

服务器已经没有 call capacity、请求最终必然 reject 时,远端仍能让 request loop 支付 decompression CPU、decoded buffer 与 memory-bandwidth 成本。

#261 已验证:将真正 decompression 移到 capacity admission 之后,可以让 capacity-rejected compressed Request 达到 Decompress = 0、decoded payload rent = 0,并显著降低 overload 路径 CPU/op。但 #261 也暴露出现有架构限制:

RequestLoop 同时承担 I/O/control frame consumption 与 dispatch
+
ISharpLinkCompressionProvider.Decompress 是同步 CPU API
+
call capacity 只有 acquire/release,没有 reservation/activation 分层
+
remote Cancel 要在 post-capacity decompression 期间保持可见

当前局部解法因此演化为:

TryAcquireCall
-> retain/copy compressed frame
-> per-request ThreadPool handoff
-> worker synchronous decompress
-> reader loop 继续消费 Cancel

该方案正确性已被大量测试覆盖,但最终 benchmark 显示 remaining accepted-path cost 与 handoff 强相关:64 KiB cancellable compressed case 约 +7%8% CPU/op、约 -4%-5% QPS;no-handoff compressed control 基本持平;small payload 的固定调度成本比例更明显。

因此本 issue 的目标不是继续优化 #261 的 work-item pool,而是从架构上把 “谁有资格消耗 server resource”“昂贵 decode 在哪里执行” 建模清楚,让安全性和性能来自同一个资源治理模型。


核心设计目标

1. 统一资源准入,而不是把 compression 特判塞进现有 Admission Controller

需要引入 server-owned 的基础资源治理层,例如概念上的:

                 Request facts
                     │
                     ▼
        ┌──────────────────────────┐
        │      ResourceGovernor    │
        │                          │
        │ call reservation         │
        │ decode concurrency       │
        │ decode byte budget       │
        │ retained byte budget     │
        │ stream buffer budget     │
        └────────────┬─────────────┘
                     │
                RequestPermit

Advanced Admission / policy admission 与 ResourceGovernor 是不同层:

  • ResourceGovernor:始终存在,保护 server 的硬资源边界;不能因为高级 admission disabled 就绕过。
  • AdmissionProgram / policy admission:可选、可动态更新,负责业务策略、global/contract/method/partition/rate/queue 等规则。

明确禁止继续保留这种耦合:

_admissionController != null
    => deferred decode
_admissionController == null
    => eager decode

Compression 的安全 decode 时机不能由“是否启用高级 admission policy”决定。

2. Call lifecycle 拆成 Reserve -> Activate

当前 TryAcquireCall() 同时承担“未来处理资格”与“已经进入 active call 生命周期”。建议拆为:

TryReserveCall()
    ↓
CallReservation
    ↓
decode / prepare
    ↓
ActivateCall()
    ↓
service invoke

硬不变量:

active calls + reserved calls <= configured call capacity

这样 compressed Request 在 decode 前只需要一个轻量 reservation:

cheap validation
-> policy admission if configured
-> TryReserveCall
   -> fail: cheap reject, Decompress = 0
   -> success: server has committed resource budget
-> decode
-> ActivateCall
-> invoke

这直接满足 #244 的安全目标,而不要求仅为了“已经是 active call”建立完整 async handoff/call-state machinery。

3. 资源成本不只是一枚 call slot

Compressed RPC 至少涉及:

  • call reservation;
  • concurrent decode CPU work;
  • compressed retained bytes;
  • decoded bytes in flight;
  • pre-admission/client-stream stream-buffer reservation。

概念上可以形成:

RequestCost
  CallSlots
  DecodeOriginalBytes
  RetainedCompressedBytes
  StreamBufferBytes
  DecodeClass / ProviderClass

不要求最终 public API 直接暴露 RequestCost,但内部架构必须能证明:

没有对应 permit,就不能开始对应昂贵工作。

4. RequestLoop 明确成为 I/O / control-plane loop

RequestLoop 的长期职责应优先限定为 bounded cheap work:

  • frame parse;
  • compression envelope validation;
  • routing/deadline/metadata cheap facts;
  • Cancel / StreamData / FlowControl / lifecycle frame consumption;
  • resource/policy admission;
  • ownership handoff。

不应让任意大 payload 的 CPU-bound decompression 无条件长期占住 reader loop。

同时,也不要求所有 compressed request 都无条件 per-request offload;decode execution 应根据成本和 cancellation semantics 决定。


必须建立的架构不变量

Resource invariants

  1. No call reservation -> no decompression.
  2. No decode permit -> no provider decode.
  3. No retained-byte permit -> no long-lived compressed frame retention.
  4. No decoded-byte permit -> no large decoded buffer rent.
  5. capacity/policy rejected compressed Request:Decompress = 0,decoded bytes rented = 0。
  6. resource permit 的 acquisition / transfer / release 必须 exactly once。
  7. Server Stop/Drain/connection close/cancellation/decode failure 都不能泄漏 reservation。

Dynamic-generation invariants

  1. 一个 Request 对需要动态化的 policy generation 只 capture 一次;await/decode/queue 后不重新读取 current generation 改写其语义。
  2. 动态 Admission enable/disable 不能改变基础 resource-safety invariant。 Disable policy admission 只表示后续请求 bypass 该 policy,不表示可以绕过 call/decode/memory resource governor。
  3. ResourceGovernor/StateKernel 应是 server-owned stable runtime state;AdmissionProgram generation 可以动态替换,但不能通过 new controller -> swap 重置真实 active/queued/decode accounting。
  4. retired policy generation 与 outstanding RequestPermit/AdmissionLease 必须有明确 transfer/drain/reclaim 规则。

#262 / #264 Dynamic Runtime Configuration 的关系

本 issue 不是 #264 的替代品,而是 #264 需要共享的底层 server resource architecture。

建议职责边界:

本 issue 负责

#264 负责

  • AdmissionProgram immutable policy generation;
  • runtime enable / disable / replace;
  • concurrency/queue/rate/partition policy dynamic semantics;
  • state-preserving update / retire / drain;
  • policy generation capture 与 publication;
  • dynamic control API。

两者共享 / 必须协同

Request frame
    ↓
cheap RequestFacts
    ↓
capture AdmissionProgram generation(若 enabled)
    ↓
policy decision / cost classification
    ↓
ResourceGovernor reserve
    ↓
RequestPermit
    ↓
decode / activate / invoke

重要:#264 当前文档中“Admission enabled 决定 ValidateEnvelope vs DecodeInboundPayload”的耦合,在本架构落地后应被删除。动态 policy generation 可以参与 routing/admission/cost classification,但 compression 是否允许做昂贵 decode 由 RequestPermit/resource reservation 决定,而不是由 _admissionController != null 决定。

#264 的 policy/state split 与本 issue 的 ResourceGovernor/RequestPermit 应复用同一 state ownership 思路,避免出现:

DynamicAdmissionController 自己计 active/queue
+
ResourceGovernor 又计 call/decode
+
两边独立 generation / 生命周期

导致双重 accounting 或状态漂移。

建议实施顺序上:


Compression execution model:不要预设“async = 更好”

当前 ISharpLinkCompressionProvider 是同步接口:

SharpLinkCompressionResult Decompress(
    ReadOnlySequence<byte> input,
    IBufferWriter<byte> output,
    int maxOutputBytes,
    CancellationToken cancellationToken = default);

内置 Brotli 已经使用 stateful BrotliDecoder 分块 decode,并在循环中检查 cancellation。因此当前代码具备 incremental/cooperative decode 的技术基础,但还没有正式 execution abstraction。

需要分别研究以下路线。

A. Inline incremental decode

对小 payload / 低估算 decode cost:

RequestPermit
-> inline decode
-> ActivateCall

优势:

  • 不产生 scheduler handoff;
  • 不需要 retain/copy 仅为了跨 async;
  • small compressed accepted path 更接近 baseline;
  • cancellation/deadline 可以在 decode quantum 边界检查。

B. Cooperative decode quantum

把一次完整 decode 进一步明确为 bounded quantum:

decode N KiB
-> cancellation / lifecycle check
-> optionally yield / reschedule
-> next quantum

目标不是制造 async 关键字,而是控制 request-loop / executor 单次不可抢占 CPU 时间。

需要评估:

  • quantum size;
  • provider state ownership;
  • output writer ownership;
  • yield 是否真的改善 Cancel latency;
  • 对 1 KiB / 64 KiB / 1 MiB 的 CPU/P99 影响。

C. Bounded persistent DecodeExecutor

如果大型 remote-cancellable decode 不适合 reader loop,则建立正式 executor,而不是 per-request ThreadPool.QueueUserWorkItem

connection queues
      ↓
fair / bounded scheduling
      ↓
small fixed set of persistent decode workers

必须具备:

  • bounded queue;
  • bounded decode concurrency;
  • per-connection fairness / anti-monopoly;
  • Stop/Drain supervision;
  • worker exception observation;
  • no unbounded detached tasks;
  • RequestPermit 与 worker ownership 明确 transfer。

D. Optional genuine async provider capability

可以研究新的 provider SPI 是否需要 async capability,但不能把:

ValueTask DecompressAsync(...)

本身当作解决方案。

对于内存中的 Brotli,主要成本是 CPU,不是等待 I/O。若 provider 的 DecompressAsync 内部只是 Task.Run(Decompress),那只是把 #261 的 ThreadPool handoff 从 server 层移动到 provider 层。

只有 provider 真正具备异步/外部执行能力(例如 dedicated native engine / accelerator / remote execution),async capability 才有独立价值。

因此建议 provider SPI 最终允许:

  • fast synchronous/incremental decode;
  • optional async execution capability;
  • 但不要强迫普通 CPU codec 实现 fake async。

推荐 Decode 策略:adaptive,而不是一刀切

候选策略:

if request is not remote-cancellable:
    inline incremental decode
else if estimated decode cost <= InlineDecodeBudget:
    inline bounded decode
else:
    DecodeExecutor.Enqueue(permit, payload)

估算可以先基于:

  • originalLength;
  • compressedLength;
  • provider/profile;
  • server load / decode credits;
  • cancellation semantics。

不要在第一版过度智能化。最初甚至可以只按 originalLength 阈值做 deterministic prototype,再用 benchmark 决定是否值得增加 provider-specific cost model。

目标语义:

remote Cancel 允许被一个明确 bounded 的小 decode quantum 延迟;大/昂贵 decode 不阻塞 control-plane consumption。

这比“所有 wire-cancellable compressed request 都必须 per-request handoff”更符合性能/复杂度平衡。


Cancellation / Deadline 语义需要重新审计

当前默认 request timeout 会使普通 unary request 具有 wire Cancellable 语义,因此 #261 中很多看似普通的 compressed unary 也进入 handoff path。

本架构需要明确区分概念:

HasDeadline
AcceptsRemoteCancellation
ServiceSupportsCooperativeCancellation

它们是否必须继续由同一个 wire flag 表达,需要独立协议/兼容性分析;本 issue 不应偷改 Protocol v2。

但 server 内部至少不能把“有 deadline”自动等同于“decode 期间必须依赖 reader loop 立即收到 remote Cancel 才能取消”。Server-side deadline / Stop / connection-close cancellation 可以来自独立 cancellation source。

需要形成明确 ADR:

  • small bounded inline decode 可以允许多少 remote-Cancel observation latency;
  • large decode 何时必须 offload;
  • deadline 与 remote Cancel 的 execution requirement 是否完全相同;
  • 如果 wire semantics 无法拆分,内部仍如何避免不必要 handoff。

Phase 0 — Architecture spike / 数据先行

在正式生产重构前,用 #261 已有 benchmark/test harness 做同宿主对比,至少 prototype:

A. #261 per-request ThreadPool handoff baseline
B. ReserveCall + inline incremental decode
C. ReserveCall + bounded cooperative decode quantum
D. ReserveCall + persistent bounded DecodeExecutor

如果准备新增 provider async SPI,再加:

E. optional async-provider prototype

测试矩阵至少:

payload: 1 KiB / 64 KiB / 1 MiB
compression ratio: high / low
remote cancellable: off / on
capacity: available / full
advanced admission: off / immediate / queued
concurrency: 1 / 16 / 128(按 runner 能力调整)

指标:

  • QPS;
  • CPU/op;
  • P50/P99;
  • B/op;
  • decompression calls/rejected request;
  • decoded bytes rented/rejected request;
  • retained compressed bytes in flight;
  • decoded bytes in flight;
  • decode queue depth;
  • worker/scheduler delay;
  • remote Cancel observation latency;
  • Stop/Drain latency。

Phase 0 输出一个简短 ADR/issue comment,明确选择哪种 execution model,再开始大规模 production plumbing。


建议实施阶段

Phase 1 — Decouple resource safety from optional Admission

  • compressed Request cheap envelope validation 不再依赖 _admissionController != null
  • 建立 RequestFacts / routing facts 的单次解析边界;
  • 保持 uncompressed direct/zero-copy fast path;
  • 不在本阶段引入最终 async executor。

Phase 2 — Introduce RequestPermit + CallReservation

  • TryReserveCall / ActivateCall
  • server + per-connection capacity 统一 reservation accounting;
  • early reject / corruption / cancel / close / stop exactly-once release;
  • active + reserved invariant deterministic tests;
  • 不改变 wire semantics。

Phase 3 — Add decode/memory resource budgets

  • decode concurrency credits;
  • decoded bytes in flight;
  • retained compressed bytes;
  • pre-admission stream buffer budget 与 RequestPermit ownership 对齐;
  • 明确 per-server / per-connection 两级预算关系。

Phase 4 — Implement selected decode execution model

根据 Phase 0 结果选择:

  • inline + adaptive threshold;
  • cooperative quantum;
  • persistent DecodeExecutor;
  • optional async provider capability。

不要默认把所有方案都实现到生产代码。

Phase 5 — Integrate Dynamic Admission (#264)

Phase 6 — Stress / performance / documentation


Correctness acceptance suite

优先复用/迁移 #261 已验证的测试,不要重新从零构造:

Capacity / cheap reject

  • capacity full + compressed unary:Decompress = 0。
  • capacity full + compressed one-way:Decompress = 0。
  • rejected decoded buffer bytes = 0。
  • accepted compressed unary/one-way:exactly one decode。
  • advanced admission enabled/disabled/queued 都满足相同 resource invariant。

Envelope / corruption

  • invalid compression envelope pre-reservation reject。
  • malformed routing/metadata 保持当前 connection/RPC error semantics。
  • corrupt body 在 reservation 后 decode failure,permit exactly once release。
  • provider throw / consumed/written mismatch / max original length mapping 保持。

Cancellation / lifecycle

  • remote Cancel before reservation。
  • remote Cancel during inline small decode。
  • remote Cancel during executor decode。
  • deadline during decode。
  • connection close after reservation/before decode。
  • Stop/Drain during decode。
  • decode failure × Cancel race。

Streaming

  • client-streaming / duplex setup reservation cleanup。
  • rejected pre-admission stream drain。
  • one-way stream rejection no leak。
  • stream byte budget 与 RequestPermit exactly-once transfer/release。

Dynamic configuration

  • Request capture old AdmissionProgram -> config update -> old request 不混用新 generation。
  • admission disable 后新 request bypass policy,但仍受 ResourceGovernor。
  • concurrency shrink 时 outstanding reservation/active call accounting 不被重置。
  • update + decode + Cancel + Stop race。
  • retired generation 不因 RequestPermit 留存产生 use-after-dispose / unbounded retention。

Stress

  • 100k capacity-rejected compressed requests:active/reserved call = 0,decoded/retained/stream budget = 0。
  • repeated dynamic enable/disable/update + compressed traffic 无泄漏。
  • bounded executor queue/concurrency under overload。

性能门禁

本重构的目标不是“安全性允许长期牺牲正常路径性能”,而是建立可以同时保护 overload 与 accepted fast path 的架构。

Uncompressed disabled/default path

  • 无新增 per-call allocation。
  • 无 request-path global lock。
  • 无 decode/executor branch work beyond必要的 predictable resource fast-path。
  • 相对 baseline 稳定吞吐/CPU/P99 回退应在 benchmark noise 范围;持续 >3%~5% 必须分析并给出架构理由。

Small compressed accepted path

  • 不因 cancellation semantics 无条件支付 per-request ThreadPool handoff。
  • 1 KiB / 64 KiB no-contention case 应尽量接近 no-handoff baseline。
  • B/op 不明显回退。

Large/remote-cancellable compressed path

  • decode concurrency bounded。
  • reader/control-plane 不被大 decode 长时间饿死。
  • remote Cancel latency 有明确上界/测量证据。
  • executor 不产生无界 queue / ThreadPool starvation。

Rejected path


明确禁止的实现

  • 不再用 _admissionController != null 决定 compression eager/deferred decode。
  • 不为了支持 dynamic admission 建第二套独立 call/decode accounting。
  • 不把 ResourceGovernor 做成每请求全局锁。
  • 不通过 process-global static registry 保存 permits/generations。
  • 不把 ValueTask DecompressAsync + 内部 Task.Run 当作 async architecture 完成。
  • 不让所有 small compressed requests 无条件 ThreadPool handoff。
  • 不为了跨 async 把整个 payload copy 到普通 byte[];retained ownership 必须受 budget 管理。
  • 不在 config update 时重置 active/decode/queue resource state。
  • 不在 ordinary dynamic Admission Disable 时释放/绕过基础 server resource safety。
  • 不在一个超大 PR 中同时完成 ResourceGovernor、所有 dynamic rate limiter、provider API redesign、wire protocol redesign。

Out of scope

  • Protocol v2 wire format redesign / compression renegotiation;如确需拆分 deadline/remote-cancel wire semantics,另开协议 issue。
  • Client endpoint admission / circuit breaker 动态化。
  • 所有 Runtime Configuration public API 的统一设计(仍由 [Runtime Configuration] 动态配置 / Hot Reload 长期跟踪 #262 后续统一评估)。
  • GPU/native accelerator 等具体 provider 实现;只定义必要 execution capability 边界。
  • 为了本 issue 重写全部 server dispatch。

建议 PR 拆分

不要重复 #261 的大分支模式。建议每一步可单独验证/回滚:

  1. test: preserve issue-244 resource amplification evidence
  2. refactor(server): decouple compressed envelope handling from optional admission
  3. refactor(server): add call reservation and request permit lifecycle
  4. feat(server): add decode and byte resource budgets
  5. perf(server): prototype and select decode execution strategy
  6. feat(server): implement bounded/adaptive decode execution
  7. refactor(server): integrate dynamic admission generation with resource governor
  8. test: add dynamic/compression/cancellation resource stress
  9. perf: record accepted/rejected/resource-governor matrix
  10. docs: document server request resource admission architecture

如果 Phase 0 证明 provider SPI 必须变化,再单独开/拆 provider API PR,不把 public SPI redesign 偷塞进基础 reservation PR。


Definition of Done

Checks

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions