[ 
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: 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.

 

  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? (Writes: no, but an opt-in policy cap is retained. Informers: no, and 
no knob is offered. Events: yes — source-shedding of discardable traffic is 
cheaper than server-side rejection.)

|| 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
>            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: 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.
>  



--
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