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