[ 
https://issues.apache.org/jira/browse/YUNIKORN-3356?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Dale Richardson updated YUNIKORN-3356:
--------------------------------------
    Description: 
h2. Summary

The shim builds two general-purpose clientsets whose groupings follow code 
structure rather than traffic type: one serves both the informers and the 
events sink, the other serves writes, the volume binder, and predicate lookups. 
Because each clientset gets its own rate limiter from the same 
{{kubernetes.qps}} setting, (a) unrelated traffic classes share failure domains 
— an event burst can delay informer relists during recovery — and (b) the 
effective request ceiling is 2× the configured value, which is unlikely to be 
what operators expect.

Proposal: restructure into purpose-built clients — {*}writes/binds{*}, 
{*}informers{*}, *events* - each with its own rate limiter, sensible 
per-concern defaults, and a distinct User-Agent.

Scope note: this ticket is entirely client-side (shim code + config). It ships 
no FlowSchema/PriorityLevelConfiguration objects and requires none - all 
server-side APF work is proposed separately in a later proposal. APF is 
referenced below only as rationale for where client-side limits are and are not 
appropriate.
h2. Current state (master, {{pkg/client}} / {{{}pkg/shim{}}})
 * Bootstrap client ({{{}pkg/client/kubeclient.go:43{}}}, 
{{{}interfaces.go:58{}}}, called from {{{}bootstrap.go:30{}}}): two ConfigMap 
GETs at startup. Sets no QPS, so no clientset-level limiter is created and it 
runs on client-go's rest-layer defaults (5 QPS / 10 burst).
 * Clientset 2 ({{{}pkg/shim/scheduler.go:67{}}}): serves *both* the 
cluster-wide SharedInformerFactory ({{{}scheduler.go:70{}}}) *and* the 
events/v1 broadcaster sink ({{{}scheduler.go:84-85{}}}).
 * Clientset 3 ({{{}pkg/client/apifactory.go:93{}}}): serves *all writes* 
(Bind/Create/Delete/UpdateStatus), the namespaced ConfigMap informer factory, 
the volume binder ({{{}apifactory.go:122{}}}), and the predicate framework 
handle.

Consequences:
 # *{{kubernetes.qps}} is a per-clientset limit, so the effective ceiling is 2× 
the configured value.* Each {{NewKubeClient}} call builds a fresh 
{{rest.Config}} ({{{}kubeclient.go:55-69{}}}) and client-go creates a new token 
bucket per clientset ({{{}kubernetes/clientset.go{}}}, 
{{{}NewForConfigAndClient{}}}). An operator setting {{kubernetes.qps: 500}} 
expecting to cap the scheduler at 500 req/s actually allows ~1,000. Any further 
client splitting silently multiplies this again unless the config surface is 
redesigned — so this restructuring and the config semantics must land together.
 # *An event flood competes with informer relists on the same token bucket* 
(clientset 2). Watch calls themselves bypass the client rate limiter entirely 
({{{}rest/request.go:763-764{}}} — watches are deliberately not throttled), but 
the *relist / initial LIST* after a watch break does pay a token — and the 
events/v1 broadcaster spawns a goroutine per event 
({{{}tools/events/event_broadcaster.go:170-173{}}}), so a mass-failure burst 
can park thousands of concurrent token waits ahead of a recovery relist on the 
shared FIFO bucket. Order-of-magnitude impact (assuming a burst of a few 
thousand events with the bucket drained): seconds of relist delay at the 
1000/1000 default, minutes at operator-lowered values (e.g. 50). Recovery 
correctness is sensitive to relist timeliness (see YUNIKORN-3355). Splitting 
removes the coupling at every setting.
 # {*}Events compete with nothing they should{*}: events are droppable by 
design, but today the only way to limit them is to limit everything else too. 
Note the broadcaster's bounded 1000-entry drop-on-full queue bounds only the 
broadcast side; in-flight sink recordings are per-event goroutines with *no* 
bound — a client-side event bucket introduces a real bound that does not exist 
today.
 # No {{UserAgent}} is set on any client (zero occurrences in non-test code), 
so apiserver-side attribution (audit logs, per-client debugging) cannot 
distinguish the shim's write path from its watch path.

h2. Proposed design

Server-side, every request from every client below is governed by APF 
unconditionally (on by default since K8s 1.20, GA 1.29) — mutating requests as 
seat-costed writes, relists as seat-costed LISTs, established watches streaming 
seat-free with their cost charged to writers via the fan-out surcharge. The 
table therefore describes only the *client-side* mechanism, and the design 
question per row is solely: does a client-side limiter add anything APF cannot 
provide?

 
||Client||Serves||Client-side limiter||
|*writes*|Bind, Create/Delete, status updates, volume binder|none by default; 
{{kubernetes.qps}} / {{kubernetes.burst}} retained as opt-in cap|
|*informers*|all SharedInformerFactory watch/list traffic|none; no knob offered|
|*events*|events/v1 broadcaster sink|token bucket: new {{kubernetes.eventQPS}} 
/ {{{}kubernetes.eventBurst{}}}, default 200/400|
|*bootstrap*|two startup ConfigMap GETs|n/a — folded into the writes client|

Per-client rationale:
 * *writes* — values <= 0 (the new default) install an explicit no-op limiter; 
implementation note: this must be 
{{{}flowcontrol.NewFakeAlwaysRateLimiter(){}}}, because leaving 
{{rest.Config.QPS}} at 0 silently applies client-go's 5/10 defaults. Positive 
values install a token bucket for operators with a deliberate policy reason to 
cap the scheduler (fleet limits, fragile downstream admission/audit 
infrastructure). Rationale for default-off: the limiter's only measured 
production effect is harm — 52 binds/s at qps 50, and even the 1000 default 
clips sustained peak (~1,170 binds/s measured only as a burst-window average) — 
while APF provides the actual protection. (Method: 3,000–5,000-pod bind bursts 
on a kind+KWOK rig, 500 nodes; harness offered below.)
 * *informers* — steady-state request rate is intrinsically tiny: watches are 
long-lived and client-go exempts WATCH from the client limiter anyway 
({{{}rest/request.go:763{}}}); the only limitable calls are relists, which 
occur precisely when client-side delay damages recovery (see YUNIKORN-3355). 
There is no legitimate operator policy that wants relists delayed, hence no 
knob.
 * *events* — suggested default 200/400, higher than kubelet's 
{{eventRecordQPS}} 50/100 because a batch scheduler's event volume tracks 
pods/s (roughly one event per scheduled pod); 50 would drop events in normal 
operation at moderate throughput, while 200/400 is lossless in steady state and 
bounds storms. Bounded loss under storm is acceptable and now {*}possible 
without capping binds{*}.
 * *bootstrap* — the dedicated 5/10-QPS clientset is vestigial; its two 
ConfigMap GETs move to the writes client.

Each client sets a distinct {{UserAgent}} ({{{}yunikorn-scheduler/writes{}}}, 
{{{}/informers{}}}, {{{}/events{}}}) for apiserver-side attribution (audit 
logs, APF debug endpoints). ({{{}rest_client_*{}}} metrics are not 
User-Agent-labelled; the client split itself is what would let per-client 
metrics registries be distinguished, if metrics are ever registered.)

Design principle — three rules govern the table above:

# *APF arbitrates everything that is sent.* On every supported Kubernetes 
version, all shim traffic — binds, status updates, relists, events — contends 
under APF, and that is desirable: only the server can allocate its capacity 
fairly against other clients' load, which no amount of client self-restraint 
can influence.
# *Client-side limiting exists only to avoid sending discardable traffic.* 
Events are the sole discardable class: their terminal state under overload is 
"dropped" no matter who decides. The server's cheapest possible answer still 
costs both ends a round trip plus APF classification and queue bookkeeping, 
after which the events/v1 broadcaster abandons the event anyway (server 
rejections — any {{StatusError}}, including 429 — are never retried: "Server 
rejected event (will not retry!)"; its 12-attempt/10s retry loop covers only 
network errors, i.e. outages, where APF cannot act at all). Source-shedding 
reaches the same outcome for free and keeps working when the server is 
unreachable. The companion APF proposal may still route events to a cheap 
priority level (resource-matching FlowSchema rule, {{resources: events}}, same 
ServiceAccount) as defence in depth for whatever the bucket admits — the two 
are complements, not alternatives: APF protects the scheduler from everyone 
else's load; the bucket keeps the scheduler's own discardable work from costing 
anything.
# *Must-complete traffic is never self-throttled by default.* The write-path 
{{kubernetes.qps}} knob survives only as an opt-in policy cap — the measured 
cost of engaging it is 52 vs ~1,170 binds/s — and the rare 
deliberately-APF-disabled cluster ({{--enable-priority-and-fairness=false}}) is 
exactly the operator who can be expected to set an explicit value. Caveat: 
default-unlimited slightly enlarges the blast radius of a hypothetical write 
hot-loop bug; containment is APF plus client-go's backoff, as it effectively is 
today — a limiter permitting 1,000+ binds/s never meaningfully contained it.


h2. Risks / compatibility
 * Connection overhead: ~nil — client-go caches transports keyed on connection 
config, so additional clientsets with identical TLS/dial settings share the 
underlying connection pool.
 * Event delivery under sustained storm becomes explicitly and predictably 
bounded (today the bounds are accidental: the broadcaster's 1000-entry 
broadcast queue plus unbounded in-flight sink goroutines).

h2. Validation
 * Unit: config plumbing (defaults, back-compat of {{{}kubernetes.qps{}}}).
 * e2e: mass-failure event storm concurrent with a bind burst — assert bind 
throughput is unaffected by event-path saturation (today they share a bucket); 
assert informer relist latency is unaffected by an event backlog. A kind+KWOK 
harness that produces both conditions exists from the API-server load 
characterisation work and can be contributed.

 

  was:
h2. Summary

The shim builds two general-purpose clientsets whose groupings follow code 
structure rather than traffic type: one serves both the informers and the 
events sink, the other serves writes, the volume binder, and predicate lookups. 
Because each clientset gets its own rate limiter from the same 
{{kubernetes.qps}} setting, (a) unrelated traffic classes share failure domains 
— an event burst can delay informer relists during recovery — and (b) the 
effective request ceiling is 2× the configured value, which is unlikely to be 
what operators expect.

Proposal: restructure into purpose-built clients — {*}writes/binds{*}, 
{*}informers{*}, *events* - each with its own rate limiter, sensible 
per-concern defaults, and a distinct User-Agent.

Scope note: this ticket is entirely client-side (shim code + config). It ships 
no FlowSchema/PriorityLevelConfiguration objects and requires none - all 
server-side APF work is proposed separately in a later proposal. APF is 
referenced below only as rationale for where client-side limits are and are not 
appropriate.
h2. Current state (master, {{pkg/client}} / {{{}pkg/shim{}}})
 * Bootstrap client ({{{}pkg/client/kubeclient.go:43{}}}, 
{{{}interfaces.go:58{}}}, called from {{{}bootstrap.go:30{}}}): two ConfigMap 
GETs at startup. Sets no QPS, so no clientset-level limiter is created and it 
runs on client-go's rest-layer defaults (5 QPS / 10 burst).
 * Clientset 2 ({{{}pkg/shim/scheduler.go:67{}}}): serves *both* the 
cluster-wide SharedInformerFactory ({{{}scheduler.go:70{}}}) *and* the 
events/v1 broadcaster sink ({{{}scheduler.go:84-85{}}}).
 * Clientset 3 ({{{}pkg/client/apifactory.go:93{}}}): serves *all writes* 
(Bind/Create/Delete/UpdateStatus), the namespaced ConfigMap informer factory, 
the volume binder ({{{}apifactory.go:122{}}}), and the predicate framework 
handle.

Consequences:
 # *{{kubernetes.qps}} is a per-clientset limit, so the effective ceiling is 2× 
the configured value.* Each {{NewKubeClient}} call builds a fresh 
{{rest.Config}} ({{{}kubeclient.go:55-69{}}}) and client-go creates a new token 
bucket per clientset ({{{}kubernetes/clientset.go{}}}, 
{{{}NewForConfigAndClient{}}}). An operator setting {{kubernetes.qps: 500}} 
expecting to cap the scheduler at 500 req/s actually allows ~1,000. Any further 
client splitting silently multiplies this again unless the config surface is 
redesigned — so this restructuring and the config semantics must land together.
 # *An event flood competes with informer relists on the same token bucket* 
(clientset 2). Watch calls themselves bypass the client rate limiter entirely 
({{{}rest/request.go:763-764{}}} — watches are deliberately not throttled), but 
the *relist / initial LIST* after a watch break does pay a token — and the 
events/v1 broadcaster spawns a goroutine per event 
({{{}tools/events/event_broadcaster.go:170-173{}}}), so a mass-failure burst 
can park thousands of concurrent token waits ahead of a recovery relist on the 
shared FIFO bucket. Order-of-magnitude impact (assuming a burst of a few 
thousand events with the bucket drained): seconds of relist delay at the 
1000/1000 default, minutes at operator-lowered values (e.g. 50). Recovery 
correctness is sensitive to relist timeliness (see YUNIKORN-3355). Splitting 
removes the coupling at every setting.
 # {*}Events compete with nothing they should{*}: events are droppable by 
design, but today the only way to limit them is to limit everything else too. 
Note the broadcaster's bounded 1000-entry drop-on-full queue bounds only the 
broadcast side; in-flight sink recordings are per-event goroutines with *no* 
bound — a client-side event bucket introduces a real bound that does not exist 
today.
 # No {{UserAgent}} is set on any client (zero occurrences in non-test code), 
so apiserver-side attribution (audit logs, per-client debugging) cannot 
distinguish the shim's write path from its watch path.

h2. Proposed design

Server-side, every request from every client below is governed by APF 
unconditionally (on by default since K8s 1.20, GA 1.29) — mutating requests as 
seat-costed writes, relists as seat-costed LISTs, established watches streaming 
seat-free with their cost charged to writers via the fan-out surcharge. The 
table therefore describes only the *client-side* mechanism, and the design 
question per row is solely: does a client-side limiter add anything APF cannot 
provide?

 
||Client||Serves||Client-side limiter||
|*writes*|Bind, Create/Delete, status updates, volume binder|none by default; 
{{kubernetes.qps}} / {{kubernetes.burst}} retained as opt-in cap|
|*informers*|all SharedInformerFactory watch/list traffic|none; no knob offered|
|*events*|events/v1 broadcaster sink|token bucket: new {{kubernetes.eventQPS}} 
/ {{{}kubernetes.eventBurst{}}}, default 200/400|
|*bootstrap*|two startup ConfigMap GETs|n/a — folded into the writes client|

Per-client rationale:
 * *writes* — values <= 0 (the new default) install an explicit no-op limiter; 
implementation note: this must be 
{{{}flowcontrol.NewFakeAlwaysRateLimiter(){}}}, because leaving 
{{rest.Config.QPS}} at 0 silently applies client-go's 5/10 defaults. Positive 
values install a token bucket for operators with a deliberate policy reason to 
cap the scheduler (fleet limits, fragile downstream admission/audit 
infrastructure). Rationale for default-off: the limiter's only measured 
production effect is harm — 52 binds/s at qps 50, and even the 1000 default 
clips sustained peak (~1,170 binds/s measured only as a burst-window average) — 
while APF provides the actual protection. (Method: 3,000–5,000-pod bind bursts 
on a kind+KWOK rig, 500 nodes; harness offered below.)
 * *informers* — steady-state request rate is intrinsically tiny: watches are 
long-lived and client-go exempts WATCH from the client limiter anyway 
({{{}rest/request.go:763{}}}); the only limitable calls are relists, which 
occur precisely when client-side delay damages recovery (see YUNIKORN-3355). 
There is no legitimate operator policy that wants relists delayed, hence no 
knob.
 * *events* — suggested default 200/400, higher than kubelet's 
{{eventRecordQPS}} 50/100 because a batch scheduler's event volume tracks 
pods/s (roughly one event per scheduled pod); 50 would drop events in normal 
operation at moderate throughput, while 200/400 is lossless in steady state and 
bounds storms. Bounded loss under storm is acceptable and now {*}possible 
without capping binds{*}.
 * *bootstrap* — the dedicated 5/10-QPS clientset is vestigial; its two 
ConfigMap GETs move to the writes client.

Each client sets a distinct {{UserAgent}} ({{{}yunikorn-scheduler/writes{}}}, 
{{{}/informers{}}}, {{{}/events{}}}) for apiserver-side attribution (audit 
logs, APF debug endpoints). ({{{}rest_client_*{}}} metrics are not 
User-Agent-labelled; the client split itself is what would let per-client 
metrics registries be distinguished, if metrics are ever registered.)

Design principle: server-side APF arbitrates all must-complete traffic (binds, 
status updates). Client-side limiting is used only for *discardable* traffic 
(events), where the terminal state under overload is "dropped" regardless of 
who decides. Shedding at the source is free; sending an event to be rejected 
costs a full round trip and APF seat-time on an already-stressed server, after 
which the events/v1 broadcaster abandons it anyway (server rejections — 
{{StatusError}} — are not retried: "Server rejected event (will not retry!)"; 
the 12-attempt retry loop applies only to network errors). APF *can* route the 
shim's events to a separate cheap priority level via a resource-matching 
FlowSchema rule (same ServiceAccount, {{{}resources: events{}}}), and the 
companion APF proposal may ship one as defence in depth - but it complements 
source-shedding rather than replacing it. The write-path knob is retained as an 
opt-in policy cap, not a default: APF is present on every supported Kubernetes 
version, and the rare deliberately-APF-disabled cluster 
({{{}--enable-priority-and-fairness=false{}}}) is exactly the operator who can 
be expected to set an explicit value. One reviewer-facing caveat: 
default-unlimited slightly enlarges the blast radius of a hypothetical shim bug 
that hot-loops writes - containment for that is APF plus client-go's 
connection-error backoff, which is also true today, since a limiter sized to 
permit 1,000+ binds/s never meaningfully contained it.
h2. Risks / compatibility
 * Connection overhead: ~nil — client-go caches transports keyed on connection 
config, so additional clientsets with identical TLS/dial settings share the 
underlying connection pool.
 * Event delivery under sustained storm becomes explicitly and predictably 
bounded (today the bounds are accidental: the broadcaster's 1000-entry 
broadcast queue plus unbounded in-flight sink goroutines).

h2. Validation
 * Unit: config plumbing (defaults, back-compat of {{{}kubernetes.qps{}}}).
 * e2e: mass-failure event storm concurrent with a bind burst — assert bind 
throughput is unaffected by event-path saturation (today they share a bucket); 
assert informer relist latency is unaffected by an event backlog. A kind+KWOK 
harness that produces both conditions exists from the API-server load 
characterisation work and can be contributed.

 


> Split the shim's Kubernetes clients by concern (writes / informers / events)
> ----------------------------------------------------------------------------
>
>                 Key: YUNIKORN-3356
>                 URL: https://issues.apache.org/jira/browse/YUNIKORN-3356
>             Project: Apache YuniKorn
>          Issue Type: Improvement
>          Components: shim - kubernetes
>            Reporter: Dale Richardson
>            Assignee: Dale Richardson
>            Priority: Major
>
> h2. Summary
> The shim builds two general-purpose clientsets whose groupings follow code 
> structure rather than traffic type: one serves both the informers and the 
> events sink, the other serves writes, the volume binder, and predicate 
> lookups. Because each clientset gets its own rate limiter from the same 
> {{kubernetes.qps}} setting, (a) unrelated traffic classes share failure 
> domains — an event burst can delay informer relists during recovery — and (b) 
> the effective request ceiling is 2× the configured value, which is unlikely 
> to be what operators expect.
> Proposal: restructure into purpose-built clients — {*}writes/binds{*}, 
> {*}informers{*}, *events* - each with its own rate limiter, sensible 
> per-concern defaults, and a distinct User-Agent.
> Scope note: this ticket is entirely client-side (shim code + config). It 
> ships no FlowSchema/PriorityLevelConfiguration objects and requires none - 
> all server-side APF work is proposed separately in a later proposal. APF is 
> referenced below only as rationale for where client-side limits are and are 
> not appropriate.
> h2. Current state (master, {{pkg/client}} / {{{}pkg/shim{}}})
>  * Bootstrap client ({{{}pkg/client/kubeclient.go:43{}}}, 
> {{{}interfaces.go:58{}}}, called from {{{}bootstrap.go:30{}}}): two ConfigMap 
> GETs at startup. Sets no QPS, so no clientset-level limiter is created and it 
> runs on client-go's rest-layer defaults (5 QPS / 10 burst).
>  * Clientset 2 ({{{}pkg/shim/scheduler.go:67{}}}): serves *both* the 
> cluster-wide SharedInformerFactory ({{{}scheduler.go:70{}}}) *and* the 
> events/v1 broadcaster sink ({{{}scheduler.go:84-85{}}}).
>  * Clientset 3 ({{{}pkg/client/apifactory.go:93{}}}): serves *all writes* 
> (Bind/Create/Delete/UpdateStatus), the namespaced ConfigMap informer factory, 
> the volume binder ({{{}apifactory.go:122{}}}), and the predicate framework 
> handle.
> Consequences:
>  # *{{kubernetes.qps}} is a per-clientset limit, so the effective ceiling is 
> 2× the configured value.* Each {{NewKubeClient}} call builds a fresh 
> {{rest.Config}} ({{{}kubeclient.go:55-69{}}}) and client-go creates a new 
> token bucket per clientset ({{{}kubernetes/clientset.go{}}}, 
> {{{}NewForConfigAndClient{}}}). An operator setting {{kubernetes.qps: 500}} 
> expecting to cap the scheduler at 500 req/s actually allows ~1,000. Any 
> further client splitting silently multiplies this again unless the config 
> surface is redesigned — so this restructuring and the config semantics must 
> land together.
>  # *An event flood competes with informer relists on the same token bucket* 
> (clientset 2). Watch calls themselves bypass the client rate limiter entirely 
> ({{{}rest/request.go:763-764{}}} — watches are deliberately not throttled), 
> but the *relist / initial LIST* after a watch break does pay a token — and 
> the events/v1 broadcaster spawns a goroutine per event 
> ({{{}tools/events/event_broadcaster.go:170-173{}}}), so a mass-failure burst 
> can park thousands of concurrent token waits ahead of a recovery relist on 
> the shared FIFO bucket. Order-of-magnitude impact (assuming a burst of a few 
> thousand events with the bucket drained): seconds of relist delay at the 
> 1000/1000 default, minutes at operator-lowered values (e.g. 50). Recovery 
> correctness is sensitive to relist timeliness (see YUNIKORN-3355). Splitting 
> removes the coupling at every setting.
>  # {*}Events compete with nothing they should{*}: events are droppable by 
> design, but today the only way to limit them is to limit everything else too. 
> Note the broadcaster's bounded 1000-entry drop-on-full queue bounds only the 
> broadcast side; in-flight sink recordings are per-event goroutines with *no* 
> bound — a client-side event bucket introduces a real bound that does not 
> exist today.
>  # No {{UserAgent}} is set on any client (zero occurrences in non-test code), 
> so apiserver-side attribution (audit logs, per-client debugging) cannot 
> distinguish the shim's write path from its watch path.
> h2. Proposed design
> Server-side, every request from every client below is governed by APF 
> unconditionally (on by default since K8s 1.20, GA 1.29) — mutating requests 
> as seat-costed writes, relists as seat-costed LISTs, established watches 
> streaming seat-free with their cost charged to writers via the fan-out 
> surcharge. The table therefore describes only the *client-side* mechanism, 
> and the design question per row is solely: does a client-side limiter add 
> anything APF cannot provide?
>  
> ||Client||Serves||Client-side limiter||
> |*writes*|Bind, Create/Delete, status updates, volume binder|none by default; 
> {{kubernetes.qps}} / {{kubernetes.burst}} retained as opt-in cap|
> |*informers*|all SharedInformerFactory watch/list traffic|none; no knob 
> offered|
> |*events*|events/v1 broadcaster sink|token bucket: new 
> {{kubernetes.eventQPS}} / {{{}kubernetes.eventBurst{}}}, default 200/400|
> |*bootstrap*|two startup ConfigMap GETs|n/a — folded into the writes client|
> Per-client rationale:
>  * *writes* — values <= 0 (the new default) install an explicit no-op 
> limiter; implementation note: this must be 
> {{{}flowcontrol.NewFakeAlwaysRateLimiter(){}}}, because leaving 
> {{rest.Config.QPS}} at 0 silently applies client-go's 5/10 defaults. Positive 
> values install a token bucket for operators with a deliberate policy reason 
> to cap the scheduler (fleet limits, fragile downstream admission/audit 
> infrastructure). Rationale for default-off: the limiter's only measured 
> production effect is harm — 52 binds/s at qps 50, and even the 1000 default 
> clips sustained peak (~1,170 binds/s measured only as a burst-window average) 
> — while APF provides the actual protection. (Method: 3,000–5,000-pod bind 
> bursts on a kind+KWOK rig, 500 nodes; harness offered below.)
>  * *informers* — steady-state request rate is intrinsically tiny: watches are 
> long-lived and client-go exempts WATCH from the client limiter anyway 
> ({{{}rest/request.go:763{}}}); the only limitable calls are relists, which 
> occur precisely when client-side delay damages recovery (see YUNIKORN-3355). 
> There is no legitimate operator policy that wants relists delayed, hence no 
> knob.
>  * *events* — suggested default 200/400, higher than kubelet's 
> {{eventRecordQPS}} 50/100 because a batch scheduler's event volume tracks 
> pods/s (roughly one event per scheduled pod); 50 would drop events in normal 
> operation at moderate throughput, while 200/400 is lossless in steady state 
> and bounds storms. Bounded loss under storm is acceptable and now {*}possible 
> without capping binds{*}.
>  * *bootstrap* — the dedicated 5/10-QPS clientset is vestigial; its two 
> ConfigMap GETs move to the writes client.
> Each client sets a distinct {{UserAgent}} ({{{}yunikorn-scheduler/writes{}}}, 
> {{{}/informers{}}}, {{{}/events{}}}) for apiserver-side attribution (audit 
> logs, APF debug endpoints). ({{{}rest_client_*{}}} metrics are not 
> User-Agent-labelled; the client split itself is what would let per-client 
> metrics registries be distinguished, if metrics are ever registered.)
> Design principle — three rules govern the table above:
> # *APF arbitrates everything that is sent.* On every supported Kubernetes 
> version, all shim traffic — binds, status updates, relists, events — contends 
> under APF, and that is desirable: only the server can allocate its capacity 
> fairly against other clients' load, which no amount of client self-restraint 
> can influence.
> # *Client-side limiting exists only to avoid sending discardable traffic.* 
> Events are the sole discardable class: their terminal state under overload is 
> "dropped" no matter who decides. The server's cheapest possible answer still 
> costs both ends a round trip plus APF classification and queue bookkeeping, 
> after which the events/v1 broadcaster abandons the event anyway (server 
> rejections — any {{StatusError}}, including 429 — are never retried: "Server 
> rejected event (will not retry!)"; its 12-attempt/10s retry loop covers only 
> network errors, i.e. outages, where APF cannot act at all). Source-shedding 
> reaches the same outcome for free and keeps working when the server is 
> unreachable. The companion APF proposal may still route events to a cheap 
> priority level (resource-matching FlowSchema rule, {{resources: events}}, 
> same ServiceAccount) as defence in depth for whatever the bucket admits — the 
> two are complements, not alternatives: APF protects the scheduler from 
> everyone else's load; the bucket keeps the scheduler's own discardable work 
> from costing anything.
> # *Must-complete traffic is never self-throttled by default.* The write-path 
> {{kubernetes.qps}} knob survives only as an opt-in policy cap — the measured 
> cost of engaging it is 52 vs ~1,170 binds/s — and the rare 
> deliberately-APF-disabled cluster ({{--enable-priority-and-fairness=false}}) 
> is exactly the operator who can be expected to set an explicit value. Caveat: 
> default-unlimited slightly enlarges the blast radius of a hypothetical write 
> hot-loop bug; containment is APF plus client-go's backoff, as it effectively 
> is today — a limiter permitting 1,000+ binds/s never meaningfully contained 
> it.
> h2. Risks / compatibility
>  * Connection overhead: ~nil — client-go caches transports keyed on 
> connection config, so additional clientsets with identical TLS/dial settings 
> share the underlying connection pool.
>  * Event delivery under sustained storm becomes explicitly and predictably 
> bounded (today the bounds are accidental: the broadcaster's 1000-entry 
> broadcast queue plus unbounded in-flight sink goroutines).
> h2. Validation
>  * Unit: config plumbing (defaults, back-compat of {{{}kubernetes.qps{}}}).
>  * e2e: mass-failure event storm concurrent with a bind burst — assert bind 
> throughput is unaffected by event-path saturation (today they share a 
> bucket); assert informer relist latency is unaffected by an event backlog. A 
> kind+KWOK harness that produces both conditions exists from the API-server 
> load characterisation work and can be contributed.
>  



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to