Skip to content

[perf][client-attempt] 归因并降低 Retry / Endpoint Admission / Circuit Breaker attempt-state 成本 #131

Description

@SunSi12138

目标

验证 Client 在启用 Retry、Endpoint Admission、Circuit Breaker 后产生的每逻辑调用 / 每 attempt 固定成本,并只对有证据的 state/ownership 做最小优化。

历史正式 evidence:

Fixed default               ~952 B/op
Retry first success        ~1512 B/op   (+560)
AlwaysAcceptAdmission      ~1176 B/op   (+224)
ClosedCircuitBreaker       ~1176 B/op   (+224)

这些数字和当前对象模型高度相关,但不能直接把差值等同于某个 class。执行时必须重新读取当前 dev HEAD、重新做 allocation stack,最终 macro 使用 #122

本 issue 要回答:

为了表达 retry selection、attempt outcome、admission lease/report、response-observed 与 elapsed,当前是否让“第一 attempt 就成功”的正常调用承担了过重对象状态?


依赖

#81 可能删除/重命名重复 active counter;本 issue 不得提前把 counter 语义写死。可以先测 object/state,production merge 前必须 rebase/re-audit #81 结论。


当前 dev 事实

Retry path

InvokeUnaryWithRetryAsync 当前会为一个 logical retry call 创建:

var selection = new EndpointRetrySelectionState();

然后每次 attempt:

var outcome = new AttemptOutcomeState(this, method);

即使第一 attempt 成功,这些状态也已经创建。

Endpoint admission / breaker without retry

默认 Unary core 在 _endpointAdmissionPolicy != null 时也会创建:

new AttemptOutcomeState(this, method)

因此 AlwaysAcceptAdmission / ClosedCircuitBreaker 都会付 attempt-state 成本。

AttemptOutcomeState 当前职责很多

它同时保存:

  • client/method;
  • attempt start timestamp;
  • endpoint start timestamp;
  • completion reason;
  • response observed;
  • local error code;
  • admitted endpoint candidate;
  • admission token;
  • admission lease/report flags;
  • grant/reject sequence;
  • retry-after;
  • endpoint/connection diagnostic identity;
  • PendingCall completion observer role。

这说明它不是可以直接删除的“DTO”。真正问题是:

所有 feature combination 是否都需要同一套最重状态?


第一性原理

1. 只为启用能力付成本

理想模型:

retry only
    -> 只付 retry真正需要的状态

admission only
    -> 只付 admission/report需要的状态

retry + admission
    -> 付组合状态

不应该因为一个大 class方便实现,让 retry-only 为 admission token/sequence/report 字段付完整对象成本。

2. Logical retry state 与 endpoint admission lease 是不同生命周期

Retry selection mask跨 attempt;Attempt outcome通常只对应一个 physical attempt;PendingCall completion observer可能比调用栈活得更久。

不要把它们为了“少对象”强行合成一个更难证明的大状态机。

3. Stale callback 是最重要风险

如果 attempt state进入 pool:

旧 PendingCall callback
    -> state 已归还并被新 attempt Rent
    -> callback污染新 attempt

这是不可接受的 ABA/lifecycle bug。

因此 pooling 只有在 terminal ownership和 generation明确后才允许。

4. Retry first success 是最重要 control

正常健康系统最常见:

Retry configured
Attempt 1 succeeds

如果启用 retry 就固定 +500B,即使永远不真正 retry,也值得优化。

5. Policy semantics 高于 allocation

不能为了少对象改变:

  • endpoint exclusion;
  • admission token/report exactly once;
  • RetryAfter;
  • response-observed;
  • circuit breaker success/failure统计;
  • same attempt endpoint identity;
  • retry decision error code。

Step 0:baseline

git checkout dev
git pull --ff-only
git rev-parse HEAD
dotnet --info

运行 Release build/unit/integration,特别覆盖 retry/circuit breaker/endpoint admission tests。


Step 1:建立 feature matrix microbenchmark

必须把能力拆开:

Default
RetryConfigured_FirstSuccess
RetryConfigured_FirstFailsSecondSucceeds
AdmissionAlwaysAccept
AdmissionAlwaysReject
CircuitBreakerClosed
CircuitBreakerOpen
Retry+Admission_FirstSuccess
Retry+Admission_RetryOnce
WaitForReady_NoWait

同一个 tiny Unary codec/transport fixture,避免 payload差异。

记录:

  • B/logical call;
  • B/attempt;
  • objects/call;
  • ns/call;
  • CPU;
  • contention。

Step 2:allocation stack 必须拆出具体 owner

至少确认:

EndpointRetrySelectionState
AttemptOutcomeState
SharpLinkEndpointOutcome
SharpLinkRetryContext / decision(若有 allocation)
Activity/telemetry attempt scope
PendingCall observer相关 state
async retry state machine
Task.Delay/retry timer only when actual retry

尤其要分清:

  • first-success固定成本;
  • actual second-attempt才出现的成本;
  • admission policy内部成本;
  • SharpLink wrapper成本。

不能把用户 policy 自己分配的对象归到 Runtime。


Step 3:Candidate A — EndpointRetrySelectionState 改为局部 value state

只有 profiler确认它每 logical call稳定 allocation才做。

当前 selection 主要保存:

snapshot identity
excluded mask

它跨 attempts使用,但只属于 InvokeUnaryWithRetryAsync 的局部生命周期。

候选:

private struct EndpointRetrySelectionState
{
    object? Snapshot;
    ulong ExcludedMask;
}

然后内部选择调用明确使用 ref,防止 struct copy丢失 exclusion mutation。

硬约束

  • 每个 attempt看到前一 attempt exclusion;
  • snapshot generation变化时 mask reset;
  • 全部 endpoint excluded时现有 reset语义保持;
  • 不把 ref逃逸到 async callback;
  • 不把 mutable struct复制进 closure/boxed interface。

如果为了避免 copy需要大量侵入 API,且只省一个小对象,No-Go。


Step 4:Candidate B — 按 feature 拆 attempt state

这是优先于 pooling 的候选。

B1 Retry-only

_endpointAdmissionPolicy == null 时,retry真正需要的信息应审计为:

attempt elapsed
connection/endpoint identity(仅诊断/策略需要时)
completion reason
response observed
error code

不需要:

admission endpoint/token
admission grant/reject sequence
retry-after from admission
report flags

候选可以是更小的 internal retry attempt tracker,甚至部分 value state;但只要它注册为 PendingCall completion observer并跨 await,就要有稳定 owner。

B2 Admission-only

没有 retry时,主要职责是:

acquire token
remember endpoint
report outcome exactly once

不应为了未来 retry保存完整 retry state。

B3 Combined

retry + admission继续用完整 state,先不要为了统一强行复杂化。

设计要求

不同 state不能复制三套 terminal classification。建议抽一个纯函数

PendingCallCompletion -> EndpointOutcomeKind/ErrorCode

机制共享,mutable lifecycle不共享。


Step 5:Candidate C — lazy diagnostic identity

当前 EndpointId / ConnectionId 是 string reference字段。先 profiler确认是否导致 allocation;引用本身不代表 allocation。

如果 connection.Session.Id / endpoint diagnostics会在每 attempt创建 string,则可以考虑:

  • 仅 retry policy真正需要 context时materialize;
  • 或让 internal outcome暂存稳定 numeric/generation references,再在报告/日志边界格式化。

但公共 SharpLinkRetryContext / SharpLinkEndpointOutcome 契约若要求 string,不能静默删除。

没有 allocation stack证据不要动。


Step 6:Candidate D — bounded pooling(最后候选)

只有 B 拆分后仍存在显著 class allocation,并且 end-to-end收益空间足够,才考虑 pool。

Pool eligibility

对象必须:

  • internal;
  • 用户拿不到引用;
  • owner唯一;
  • terminal明确;
  • reset字段可枚举;
  • retained references可完全清零;
  • 有 hard retention cap。

Generation

必须有:

lease generation

PendingCall observer注册时保存 generation/token。

callback进入时必须验证仍是同一 lifecycle;旧 generation只能 no-op,不能写新 state。

不允许

  • 无界 ConcurrentBag
  • 无版本 intrusive lock-free stack;
  • Return时仍有 PendingCall callback可能发生;
  • pool保留 Method/Client/Endpoint/Exception/ALC强引用。

如果 generation让所有 callback API都变复杂,只为省很小 allocation,应撤回。


Retry correctness matrix

必须保持:

  1. non-idempotent不 retry;
  2. first success exactly one attempt;
  3. unavailable/connection closed retry;
  4. remote business error不误 retry;
  5. response observed后按当前 policy判断;
  6. max attempts exact;
  7. exponential backoff;
  8. jitter范围;
  9. deadline阻止下一 attempt;
  10. caller cancellation;
  11. admission RetryAfter与policy delay取正确值;
  12. endpoint exclusion在同 snapshot内生效;
  13. topology generation变化正确 reset exclusion。

Admission / breaker correctness

  1. policy TryAcquire throw -> FailedPrecondition;
  2. negative RetryAfter被拒绝;
  3. rejected endpoint不建立 PendingCall;
  4. grant token只 report一次;
  5. success report;
  6. remote error report;
  7. send failure report;
  8. connection closed report;
  9. deadline/cancel report;
  10. CircuitBreaker Open/HalfOpen/Closed状态不变;
  11. retry attempt Add experimental shared-memory transport #1 report完成后 #2才能独立 acquire/report;
  12. local failure before Pending register也正确完成 admission lifecycle。

Deterministic races

至少:

  • Pending response与cancel同时;
  • response与disconnect;
  • response与deadline;
  • policy grant后、Pending register前 connection draining;
  • Pending terminal callback与 retry loop catch并发;
  • old callback发生在 state Return之后(pool candidate必须覆盖);
  • same endpoint generation replacement;
  • retry wait期间 Stop;
  • RetryAfter delay与deadline同刻。

所有 counter/report最终 exactly once。


正式 macro matrix

#122 后,同机交替 >=5 轮:

Default
Retry_FirstSuccess
Retry_OneFailure
Admission_Accept
Breaker_Closed
Breaker_Open
Retry+Admission_FirstSuccess
Retry+Admission_OneFailure

concurrency:1 / 32 / 128 / 512

TCP + SharedMemory。

记录:

  • QPS;
  • B/op;
  • CPU/op;
  • P99/P99.9;
  • attempts/call;
  • retry count;
  • policy acquire/report count;
  • failures。

Go / merge gates

Retry first-success

这是主要目标:

  • retry附加 allocation降低 >=30%;
  • 或 full RPC CPU/QPS稳定改善 >=3%;
  • actual retry行为不回退 >3%。

Admission/Breaker accept path

  • feature附加 allocation降低 >=25%;
  • 或 full RPC CPU/QPS稳定改善 >=3%。

Pool candidate额外门槛

如果不用 pool就已经获得大部分收益,不引入 pool。

pool必须至少比非pool candidate再带来:

  • =15% feature附加 allocation下降;或

  • =3% target workload CPU改善。

否则复杂度不值。

Default feature-off path B/op必须完全不增加。


Commit / rollback

1. bench: attribute client attempt state costs
2. experiment: make retry selection value-owned
3. experiment: split retry-only and admission attempt state
4. experiment: pool attempt state with generation   // 最后才做
5. test: harden retry/admission lifecycle races

每项独立可回滚。

立即 revert,如果:

  • endpoint exclusion错误;
  • retry次数变化;
  • report double/missing;
  • breaker状态错误;
  • stale callback污染新 attempt;
  • deadline/cancel语义变化;
  • retained Client/Method/ALC reference;
  • target workload收益不足。

完成定义

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