[
https://issues.apache.org/jira/browse/YUNIKORN-3356?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Dale Richardson updated YUNIKORN-3356:
--------------------------------------
Summary: Split the shim's Kubernetes clients by concern (writes / informers
/ events) (was: Split the shim's Kubernetes clients by concern (writes /
informers / events) with per-concern rate limits and User-Agents)
> 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 starve informer watch re-establishment 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* (and later *leader election* if it is ever added) —
> each with its own rate limiter, sensible per-concern defaults, and a distinct
> User-Agent.
> 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 ({{{}client-go 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 watch re-establishment on the same token
> bucket* (clientset 2). Mass-failure scenarios emit events in bursts precisely
> when informers need tokens to reconnect and relist — the worst possible
> coupling (see YUNIKORN-3355 for how much recovery correctness depends on
> timely informer delivery). Severity scales inversely with the configured QPS:
> at the 1000/1000 default a mass-failure event burst (thousands of events)
> delays informer re-establishment by seconds; at operator-lowered values (e.g.
> 50, kube-scheduler parity) the same burst starves the watch path for minutes.
> Splitting removes the coupling at every setting.
> # {*}Events compete with nothing they should{*}: events are droppable by
> design (the events/v1 broadcaster already has a bounded 1000-entry queue with
> drop-on-full), yet today they can only be limited by limiting everything else
> too.
> # 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
> ||Client||Serves||Limiter||Config||
> |writes|Bind, Create/Delete, status updates, volume binder|token
> bucket|{{kubernetes.qps}} / {{kubernetes.burst}} — {*}semantics preserved{*}:
> this is the knob that caps how fast YuniKorn acts on the cluster (binds are
> ~1:1 with QPS tokens; measured 52 vs 1,170 binds/s at 50 vs 1000)|
> |informers|all SharedInformerFactory watch/list traffic|*none* (unlimited)|—
> steady-state request rate is intrinsically tiny (watches are long-lived; the
> limiter charges per request, not per event); relist bursts are better
> governed server-side by APF than by a client limiter that would slow recovery|
> |events|events/v1 broadcaster sink|token bucket|new {{kubernetes.eventQPS}} /
> {{kubernetes.eventBurst}} (suggested default 200/400; kubelet precedent
> {{{}eventRecordQPS{}}}). Bounded loss under storm is acceptable and now
> *possible without capping binds*|
> |bootstrap|2 startup ConfigMap GETs|n/a|fold into the writes client; the
> dedicated 5/10-QPS clientset is vestigial|
> Each client sets a distinct {{UserAgent}} ({{{}yunikorn-scheduler/writes{}}},
> {{{}/informers{}}}, {{{}/events{}}}) for apiserver-side attribution and
> cleaner {{rest_client_*}} metrics if/when client-go metrics are registered.
> h2. Interactions
> * {*}Config semantics{*}: this issue narrows {{kubernetes.qps}} from "per
> clientset, effectively ×2" to "write path only". Release notes must state
> both the old effective behaviour and the new meaning. The documentation work
> for QPS guidance should land as part of this change (one table: client /
> purpose / knob / default) rather than separately.
> * {*}Hot-reload{*}: with per-concern defaults there is little reason left to
> make QPS hot-reloadable; recommend dropping that idea rather than building
> config-rebuild machinery for multiple clients. Server-side APF is the correct
> dynamic control.
> * {*}APF (future, optional phase 2){*}: APF distinguishes flows by
> {*}user{*}, so split clients on one ServiceAccount still share one queue
> position. Running the events client under a separate identity would allow
> events to be placed in a cheap priority level while the write path keeps a
> protected one. Not required for this issue, but the client boundary drawn
> here is what makes it possible later — worth keeping in mind during review.
> * {*}Write dispatcher (KEP-5229-style, future){*}: a bounded write-dispatch
> worker pool would sit naturally on the writes client; clean separation here
> simplifies that later work.
> h2. Risks / compatibility
> * Connection count rises by ~2 HTTP/2 connections — negligible.
> * Operators who relied (knowingly or not) on the 2× effective ceiling will
> see the informer path become unlimited and the write path unchanged; net
> behaviour change in normal operation is nil, but it should be called out.
> * Event delivery under sustained storm becomes explicitly bounded (today it
> is implicitly bounded by the broadcaster's 1000-entry drop-on-full queue
> anyway).
> 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 watch re-establishment 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]