This is an automated email from the ASF dual-hosted git repository.
zaynt4606 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/main by this push:
new e107e2d0c7 [CELEBORN-2466] Support per-zone worker StatefulSets and
autoscaling in the Helm chart
e107e2d0c7 is described below
commit e107e2d0c70e59890b0e428f197947a35cc0b359
Author: strelok89 <[email protected]>
AuthorDate: Tue Oct 13 11:41:24 2026 +0800
[CELEBORN-2466] Support per-zone worker StatefulSets and autoscaling in the
Helm chart
### What changes were proposed in this pull request?
Three values for `charts/celeborn`, all disabled by default.
**`worker.zoneAwareReplication`** renders one worker StatefulSet per zone
instead of a single
one for the whole cluster. Each carries a `celeborn.apache.org/zone` label
as part of its
selector, so a zone's worker count is declared rather than inferred from
scheduling.
`worker.replicas` becomes the total across zones, split `ceil(replicas /
zones)`, with a
per-zone override; `zones[].nodeSelector` merges over `worker.nodeSelector`
and
`zones[].affinity` replaces `worker.affinity`. All zones share the existing
headless service,
so the service and pod monitor keep selecting every zone.
**`worker.zoneAwareReplication.metricsLabel`** passes the zone into
`celeborn.metrics.extraLabels` so the worker stamps a `zone` label on
everything it emits. It
is injected as a system property, since master and worker share one config
map:
`Utils.loadDefaultCelebornProperties` applies the file with `setIfMissing`,
so a system
property takes precedence, and `start-worker.sh` builds
`CELEBORN_JAVA_OPTS` from
`CELEBORN_WORKER_JAVA_OPTS`, so it composes with whatever `worker.env` sets.
**`worker.drain`** adds the `preStop` hook described below, independent of
autoscaling, since
any rollout removes workers too.
**`worker.autoscaling`** creates a KEDA `ScaledObject` per worker
StatefulSet — one per zone
with the above enabled. Two triggers ship, on the two resources whose
exhaustion takes a
worker out of service, both scoped by the `zone` label and both excluding a
decommissioning
worker:
- `diskUsage`, on `DeviceCelebornFreeBytes / DeviceCelebornTotalBytes`,
default 0.70.
- `memoryUsage`, on `DirectMemoryUsageRatio`, default 0.70 — below
`celeborn.worker.directMemoryRatioToPauseReceive` (0.85) so capacity
arrives before pushes
pause.
Both use `metricType: Value`, since a ratio must not be divided across
replicas.
`worker.autoscaling.triggers` appends user triggers, rendered through `tpl`
against the zone's
context.
`worker.drain` adds a `preStop` hook calling `POST /api/v1/workers/exit`
with
`DECOMMISSION`, because Kubernetes scale-in only sends `SIGTERM`, which
never enters the
decommission path — the worker would leave with its shuffle data. The hook
must not fire on a
rolling update or node drain, where waiting for shuffle data to expire
would block every pod
replacement for hours, so it distinguishes them by ordinal: the StatefulSet
controller lowers
`spec.replicas` before deleting pods on a scale-in, so an ordinal at or
above the desired count
is being removed for good, while anything else shuts down gracefully and
returns. A desired
count that cannot be read falls back to a graceful shutdown, so a failed
lookup cannot stall a
rollout. The hook reads the count with the pod's own service account, so
the role gains `get`
on `statefulsets/scale` when the drain is enabled.
### Why are the changes needed?
Workers all live in one StatefulSet at a fixed replica count, which leaves
two gaps.
Their spread across availability zones is uneven. The scheduler decides
where a worker lands,
so pod-level spreading (`worker.affinity`, or
`worker.topologySpreadConstraints` from
CELEBORN-2446) balances it only on a best-effort basis: the balance is
re-derived on every
scheduling decision, holds while the constraint is satisfiable, and
degrades quietly when it is
not — a zone out of capacity for the workers' instance type, or several
nodes replaced at once.
And a zone cannot be scaled at all. Load per zone is genuinely uneven when
applications select
workers by tag (`celeborn.tags.tagsExpr`), so the fleet has to be sized for
the busiest zone
and left that size. A single StatefulSet also cannot be rolled or paused
one zone at a time.
Note that this controls where worker pods run and how many there are. It is
not zone-aware data
placement: which workers a client may use is decided in the master by
worker tags, not by pod
topology.
### Does this PR resolve a correctness bug?
- [ ] Yes
### Does this PR introduce _any_ user-facing change?
- [x] Yes
New chart values: `worker.zoneAwareReplication` and `worker.autoscaling`.
Both default to
disabled and the rendered output with default values is unchanged, so
existing releases are
unaffected until they opt in. `charts/celeborn/README.md` documents both,
including that
enabling zone-aware replication renames the worker StatefulSet and so
recreates every worker —
a maintenance-window change rather than a rolling update.
### How was this patch tested?
- `helm unittest charts/celeborn --file "tests/**/*_test.yaml" --strict` —
121 tests pass, 36
of them new, covering per-zone StatefulSets and selectors, the replica
split and per-zone
override, the node selector merge, the zone metric label and its
composition with existing
extra labels, the per-zone ScaledObjects and their default triggers, the
`preStop` hook with
its environment and scoped RBAC rule, and each `fail` guard.
- `helm lint`, with default values and with
`charts/celeborn/ci/values.yaml`.
- `helm template` with default values, diffed against `main`:
byte-identical.
- `helm template` with three zones and autoscaling enabled: three
StatefulSets with distinct
names and selectors, integer replica counts, one shared `serviceName`,
unchanged service and
pod monitor selectors, and one ScaledObject per zone targeting its own
StatefulSet.
- The trigger queries were run against a live Celeborn deployment's
Prometheus to confirm the
metric names exist and each query returns the single element KEDA
requires.
- `files/worker-drain.sh` was run directly over scale-in, rolling update,
always-decommission,
unreadable replica count, missing pod name, IPv6 bracketing, and an
unreachable exit
endpoint.
Closes #3855 from strelok89/helm-worker-zone-aware-statefulsets.
Authored-by: strelok89 <[email protected]>
Signed-off-by: zhengtao <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Codex <[email protected]>
AI-Model: gpt-6
AI-Contributed/Feature: 0/860
AI-Contributed/UT: 0/852
---
charts/celeborn/README.md | 332 ++++++++++++++++
charts/celeborn/files/worker-drain.sh | 130 +++++++
charts/celeborn/templates/configmap.yaml | 5 +
charts/celeborn/templates/role.yaml | 19 +
charts/celeborn/templates/worker/_helpers.tpl | 71 +++-
charts/celeborn/templates/worker/scaledobject.yaml | 77 ++++
charts/celeborn/templates/worker/statefulset.yaml | 121 +++++-
charts/celeborn/tests/configmap_test.yaml | 18 +
charts/celeborn/tests/role_test.yaml | 52 +++
.../celeborn/tests/worker/scaledobject_test.yaml | 361 ++++++++++++++++++
charts/celeborn/tests/worker/statefulset_test.yaml | 421 +++++++++++++++++++++
charts/celeborn/values.yaml | 105 ++++-
12 files changed, 1705 insertions(+), 7 deletions(-)
diff --git a/charts/celeborn/README.md b/charts/celeborn/README.md
index c6908fe9cb..c679f5c364 100644
--- a/charts/celeborn/README.md
+++ b/charts/celeborn/README.md
@@ -54,6 +54,338 @@ More details in [Helm
Install](https://helm.sh/docs/helm/helm_install/).
The chart can be customized using the following [celeborn
configurations](https://celeborn.apache.org/docs/latest/configuration/#important-configurations).
Specify parameters using `--set key=value[,key=value]` argument to `helm
install`.
+## Zone-aware worker replication
+
+By default the chart deploys all workers in a single statefulset, and which
zone a worker
+lands in is whatever the scheduler decides. Pod-level spreading -
`worker.affinity`, or a
+topology spread constraint where the chart supports one - can balance that,
but the balance is
+re-derived on every scheduling decision: it holds while the constraint is
satisfiable and
+silently degrades when it is not, for example when one zone is out of capacity
for the instance
+type the workers need, or when several nodes are replaced at once.
+
+Setting `worker.zoneAwareReplication.enabled` deploys one worker statefulset
per zone
+instead, which makes zone membership structural rather than emergent:
+
+```yaml
+worker:
+ # Total across all zones; each zone gets ceil(replicas / zones) = 3.
+ replicas: 9
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ nodeSelector:
+ topology.kubernetes.io/zone: us-east-1a
+ - name: us-east-1b
+ nodeSelector:
+ topology.kubernetes.io/zone: us-east-1b
+ - name: us-east-1c
+ nodeSelector:
+ topology.kubernetes.io/zone: us-east-1c
+```
+
+This renders `celeborn-worker-us-east-1a`, `-1b` and `-1c`, each carrying a
+`celeborn.apache.org/zone` label that is part of its statefulset selector.
What it gives you:
+
+- A zone's worker count is declared, not inferred, so it survives concurrent
node churn and
+ a full recreate.
+- Each zone can be rolled, scaled or paused on its own, leaving the other
zones serving.
+- A worker's ordinal-to-zone mapping is stable, so its identity
+ (`<pod>.<service>.<namespace>.svc.<cluster>.local`) implies its zone. That
is what makes
+ zone-pinned worker selection (see below) predictable.
+
+Notes:
+
+- `worker.replicas` is the total across zones; each zone deploys
+ `ceil(worker.replicas / number of zones)`, so 5 replicas over 3 zones is 2
per zone (6
+ total). Set `zones[].replicas` to size a zone explicitly, which is also the
lever for
+ running a zone short while its capacity is constrained.
+- `zones[].nodeSelector` is merged over `worker.nodeSelector` and wins on
conflicting keys;
+ `zones[].affinity` replaces `worker.affinity` for that zone.
+- All zones share one headless service, so the service and the pod monitor
keep selecting
+ workers in every zone. Only the statefulset selectors are zone-scoped.
+- Per-zone `nodeSelector` already separates the zones physically, so a zone
+ `topologySpreadConstraints` entry becomes redundant once this is enabled. A
per-hostname
+ spread or anti-affinity rule is still meaningful within a zone.
+
+### Zone-aware replication is not zone-aware data placement
+
+This only controls where worker pods run. It does not make a client prefer a
worker in its
+own zone: Celeborn allocates slots in the master, and the client-side view of
which workers
+are eligible comes from worker tags (`celeborn.tags.tagsExpr`,
`celeborn.tags.enabled`), not
+from pod topology. To keep shuffle traffic inside one zone, tag each worker
with its zone and
+have each application select its own zone's tag. Note that
+`celeborn.client.reserveSlots.rackaware.enabled` does the opposite on purpose:
it spreads a
+partition's replicas across racks for durability.
+
+### Migrating an existing release
+
+The statefulset names change, so enabling this on a running release replaces
+`<fullname>-worker` with the per-zone statefulsets. Every worker is recreated,
and with it
+its identity, so:
+
+- Worker data on `hostPath` or `emptyDir` volumes does not survive.
+- The master holds stale worker registrations until
`celeborn.master.heartbeat.worker.timeout`
+ elapses.
+- In-flight shuffles fail.
+
+Treat it as a maintenance-window change rather than a rolling update.
+
+## Draining a worker before it is removed
+
+Deleting a worker pod - by scaling in, rolling out, or draining its node -
reaches Celeborn as
+a `SIGTERM`. That never enters the decommission path, so the worker leaves
with its shuffle data
+and any application still reading from it loses that stage. Celeborn's safe
drain is
+`DECOMMISSION`, which waits for the worker's shuffle keys to expire (up to
+`celeborn.worker.decommission.forceExitTimeout`, 6h by default) and is only
reachable through
+the worker's HTTP API.
+
+This is worth having with a fixed replica count, not only with
`worker.autoscaling` - any
+rollout removes workers too.
+
+`worker.drain.enabled` adds a `preStop` hook that calls it. The hook runs
`files/worker-drain.sh`, shipped in the chart's
+config map and mounted at `/opt/celeborn/drain/worker-drain.sh`; it takes
`POD_NAME`,
+`STS_NAME` and `WORKER_HTTP_PORT` from the environment, so the script itself
is the same in
+every zone. The hook must not decommission on a rolling update or a node
+drain, or every pod replacement would block for hours, so it distinguishes the
two: the
+statefulset controller lowers `spec.replicas` *before* deleting pods on a
scale-in, so a pod
+whose ordinal is at or above the desired count is being removed for good and
decommissions,
+while any other pod shuts down gracefully and comes back. If the desired count
cannot be read
+the hook falls back to a graceful shutdown, so a broken lookup cannot stall a
rollout.
+
+This needs three things:
+
+- `rbac.create: true`. The chart adds `get` on `statefulsets/scale` to the
role when the
+ drain is enabled; the hook reads the count with the pod's own service
account.
+- `celeborn.worker.graceful.shutdown.enabled: true`, so the non-scale-in path
actually
+ persists state and recovers on restart.
+- `worker.terminationGracePeriodSeconds` above
`celeborn.worker.decommission.forceExitTimeout`.
+ This is an upper bound, not a wait: a graceful shutdown still finishes in
+ `celeborn.worker.graceful.shutdown.timeout`, so a long grace period does not
slow rollouts
+ down.
+
+The hook calls the worker over plain HTTP and sends no credentials, so it
cannot reach a worker
+with `celeborn.worker.http.ssl.enabled` or
`celeborn.worker.http.auth.supportedSchemes` set. A
+failed exit request falls through to `SIGTERM`, which on a scale-in would
silently skip the
+drain, so the chart refuses to render `worker.drain` together with either
setting under
+`celeborn`. It can only see what is set there: enabling TLS or authentication
some other way,
+such as an extra config file, still bypasses the drain.
+
+It is off by default. Turn it on alongside `worker.autoscaling`, or leave it
off and treat
+scale-in as destructive, only letting it happen when the fleet is idle.
+
+### When the worker's storage does not outlive the pod
+
+Telling a scale-in from a rollout is only worth doing if a rollout can
recover. Graceful
+shutdown persists committed file metadata to
`celeborn.worker.graceful.shutdown.recoverPath` and
+recovers from it when the worker comes back - which needs both that path and
+`celeborn.worker.storage.dirs` to survive the pod. On an `emptyDir`, or on
instance-store disks
+that are wiped when the node is replaced, neither does: the worker returns to
empty disks, there
+is nothing to recover, and the in-flight shuffles it was holding are lost.
+
+`worker.drain.alwaysDecommission: true` drops the distinction and
decommissions on
+every termination, so a worker is never removed while an application still
needs what is on it.
+It also needs no Kubernetes API access, since it never reads the replica
count, and the chart
+leaves the `statefulsets/scale` rule out of the role.
+
+What it costs:
+
+- Rollouts and node drains wait for the drain as well, bounded by
+ `celeborn.worker.decommission.forceExitTimeout` rather than
+ `celeborn.worker.graceful.shutdown.timeout`. With `OrderedReady` a rollout
drains each worker
+ in turn, so budget accordingly.
+- Anything that evicts a pod on a deadline shorter than the drain will still
kill it mid-drain -
+ a cluster autoscaler reclaiming a node, or a spot interruption with its
two-minute notice.
+ Make sure the node pool's own grace period is at least as long as you expect
a drain to take,
+ or accept that those paths behave as they did before.
+
+#### Worked example: local NVMe through an `emptyDir`
+
+A common shape for workers is to skip network storage entirely and write to
the node's own NVMe
+- an `emptyDir` mounted at `celeborn.worker.storage.dirs`, landing on the
instance's local disks.
+On AWS that means instance-store NVMe, and on a node OS that assembles those
disks into one
+array for container storage (Bottlerocket's local-storage setup, for instance)
an `emptyDir`
+lands on that array with no extra wiring. It is a reasonable choice: shuffle
is a
+write-heavy, short-lived, reproducible workload, so there is little reason to
pay for network
+volumes, per-volume throughput ceilings, or PVCs that pin an ordinal to one
zone.
+
+The catch is lifetime, and it is shorter than people expect. An `emptyDir` is
deleted when the
+pod leaves the node - not only when the node goes away. So both
+`celeborn.worker.storage.dirs` and
`celeborn.worker.graceful.shutdown.recoverPath`, if it sits
+under the same mount, disappear on every pod replacement. Graceful shutdown
still writes its
+recovery database, and the replacement worker still starts up and
re-registers, but it comes
+back to empty disks and there is nothing to read the database against.
+
+What that costs while jobs are running: a rolling update, a chart upgrade or a
scale-in removes
+a worker that is holding shuffle data for applications still running. Those
fetches fail, and
+since `celeborn.client.push.replicate.enabled` is `false` by default there is
no second copy -
+the engine recomputes the stages that produced the lost partitions. A rollout
across a fleet of
+workers can do this repeatedly, to whatever is running at the time.
+
+So on ephemeral storage the drain is what keeps a running job's shuffle alive,
and
+`alwaysDecommission` is the setting that matters - the scale-in/rollout
distinction is pointless
+when neither can recover:
+
+```yaml
+worker:
+ # Above celeborn.worker.decommission.forceExitTimeout (6h), so kubelet does
not kill a
+ # worker part-way through draining.
+ terminationGracePeriodSeconds: 21900
+ drain:
+ enabled: true
+ alwaysDecommission: true
+ volumes:
+ - name: celeborn-data
+ emptyDir: {}
+ volumeMounts:
+ - name: celeborn-data
+ mountPath: /celeborn-data/disk0
+celeborn:
+ celeborn.worker.storage.dirs: /celeborn-data/disk0:disktype=SSD
+ celeborn.worker.graceful.shutdown.enabled: true
+```
+
+A worker then stays up until the applications holding data on it are finished,
and only then
+exits - so no running job loses its shuffle to a deployment.
+
+Note that an `emptyDir` needs no `chown` init container, unlike the `hostPath`
volumes the chart
+mounts by default: Kubernetes applies `worker.podSecurityContext.fsGroup` to
it, so the worker
+can write to the mount as it is.
+
+One thing to expect when first enabling the drain: the script is shipped in
the config map that
+master and worker share, so adding it changes the
`celeborn.apache.org/conf-hash` annotation on
+both and the masters roll as well.
+
+Two things this still does not cover. Losing the node itself - a spot
interruption, or a node
+autoscaler consolidating - destroys the disks whatever the pod does, and gives
less time than a
+drain needs; set the node pool's grace period to at least the drain you
expect, and treat the
+remainder as accepted risk. And if you want a rollout to genuinely recover
rather than wait,
+storage has to outlive the pod: a `hostPath` on the NVMe mount for both
`storage.dirs` and
+`recoverPath` survives a pod restart on the same node, though nothing survives
the node being
+replaced, and a `hostPath` gives the scheduler no reason to put the pod back
where its data is.
+
+## Autoscaling workers with KEDA
+
+`worker.autoscaling` creates a [KEDA](https://keda.sh) `ScaledObject` for each
worker
+statefulset. With zone-aware replication enabled that is one per zone, so each
zone scales
+on its own load - which is what you want when applications are pinned to a
zone, because
+their load is genuinely uneven across zones.
+
+The chart ships two triggers - disk and off-heap memory, the two resources
whose exhaustion
+takes a worker out of service - both querying metrics the worker already
exports. Point them at
+a Prometheus-compatible endpoint and they work as they are:
+
+```yaml
+worker:
+ terminationGracePeriodSeconds: 21900 # see "Draining a worker before it is
removed"
+ drain:
+ enabled: true
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus.monitoring.svc.cluster.local:9090
+ maxReplicaCount: 6
+ behavior:
+ scaleDown:
+ stabilizationWindowSeconds: 1800
+ policies:
+ - type: Pods
+ value: 1
+ periodSeconds: 900
+```
+
+**Disk usage** is the signal that bites first, since a worker that fills up
stops accepting
+pushes. It scales on the fullest worker in the statefulset, as a fraction of
Celeborn disk
+capacity:
+
+```promql
+max((1 - metrics_DeviceCelebornFreeBytes_Value{...} /
metrics_DeviceCelebornTotalBytes_Value{...})
+ and on (instance) metrics_IsDecommissioningWorker_Value{...} == 0)
+```
+
+It uses `metricType: Value`, because a ratio must not be divided across
replicas.
+
+This trigger is also the slot utilization target, which is why there is no
separate one for
+slots. Celeborn sizes a disk's slot capacity from its space -
+`maxSlots = totalSpace / estimatedPartitionSize` in `WorkerInfo` - so slots
and bytes fill in
+step, and `activeSlots / maxSlots` reduces to the disk fraction above. A slot
percentage could
+not be computed anyway: `maxSlots` lives in the master's `DiskInfo` and no
metric exports it.
+If you want the raw count as a leading signal - slots are allocated when a
stage starts, before
+its data is written - add it yourself through `worker.autoscaling.triggers`
using
+`sum(metrics_ActiveSlotsCount_Value{role="Worker"})` with `metricType:
AverageValue` and a
+per-worker target.
+
+**Off-heap memory** is the other way a worker takes itself out of service: it
stops accepting
+pushes at `celeborn.worker.directMemoryRatioToPauseReceive` (0.85) and
replication at 0.95.
+`DirectMemoryUsageRatio` is `memoryUsage / maxDirectMemory`, so it is already
a fraction of
+`CELEBORN_WORKER_OFFHEAP_MEMORY`:
+
+```promql
+max(metrics_DirectMemoryUsageRatio_Value{...} and on (instance)
metrics_IsDecommissioningWorker_Value{...} == 0)
+```
+
+Also `metricType: Value`. Keep the threshold below 0.85 so capacity arrives
before pushes
+pause; the 0.70 default leaves the gap between scaling out and throttling.
+
+Both exclude decommissioning workers. A worker that is draining still holds
its disk and its
+slots for as long as it takes, and counting it would have the fleet scale out
to replace
+capacity it has not released yet.
+
+Disable either with `diskUsage.enabled: false` / `memoryUsage.enabled: false`,
and add your own
+with `worker.autoscaling.triggers`, which are appended to the built-in ones
and rendered
+through `tpl` against the zone's context, so `{{ .zone.name }}` resolves per
zone.
+
+The built-in queries select on `role="Worker"`, the release namespace and the
pod names of the
+statefulset they scale, so several Celeborn clusters can share one Prometheus
- or one
+namespace - without scaling on each other's workers.
+
+Celeborn exports gauges as `metrics_<Name>_Value` and counters as
`metrics_<Name>_Count`. Other
+metrics worth scaling on are `ActiveShuffleSize` and `ActiveShuffleFileCount`
(data held),
+`ActiveSlotsCount` (slots allocated), and `IsHighWorkload` /
`PausePushDataStatus` (the worker
+is already in trouble).
+
+`worker.zoneAwareReplication.metricsLabel` passes the zone into
`celeborn.metrics.extraLabels`
+so the worker stamps it on everything it emits. The built-in queries do not
use it - a `zone`
+label says nothing about which release a worker belongs to - but it is there
for queries of your
+own. `role="Worker"` matters too: masters report the device gauges for
whichever volume holds
+the Ratis directory.
+
+`minReplicaCount` defaults to the statefulset's own replica count, so a zone
never scales
+below the size it was deployed at unless you set it explicitly.
+
+Scaling to zero needs a trigger that does not come from the workers. Both
built-in queries read
+metrics the statefulset's own workers emit, so once the last one is gone they
return nothing and
+KEDA has no signal to scale back up on. The chart refuses `minReplicaCount: 0`
unless
+`worker.autoscaling.triggers` has an entry - a `cron` schedule, or a query on
something outside
+the workers such as pending Spark executors. It cannot tell what that trigger
reads, so make
+sure it is one that still fires with no workers running.
+
+Enable `worker.drain` as well. Scaling in deletes worker pods, and without the
drain those
+workers leave with shuffle data an application may still need - see
+[Draining a worker before it is
removed](#draining-a-worker-before-it-is-removed).
+
+Three properties of this fleet are worth sizing for. Scaling out is slow: a
new worker
+usually needs a new node, and it starts with no shuffle data on it, so
autoscaling answers
+sustained load changes rather than bursts. Scaling in always removes the
highest ordinal,
+which is not necessarily the least busy worker. And while a worker drains, its
statefulset
+cannot grow - so a scale-in decided at low load leaves that zone one worker
short until the
+drain finishes. Pace scale-in conservatively with `behavior.scaleDown`, and
consider
+excluding draining workers from the trigger query.
+
+### Helm and the autoscaler both own `replicas`
+
+The chart keeps rendering `spec.replicas`, so a fresh install starts at the
size you asked
+for rather than at one. The cost is that anything reconciling the rendered
manifest will fight
+KEDA over that field.
+
+A plain `helm upgrade` is included in that: its three-way merge patches a
KEDA-scaled replica
+count back to the chart's value, and with `OrderedReady` the controller then
removes the excess
+ordinals one at a time, waiting for each to terminate - which is slow once
`worker.drain` is
+enabled, and slower again with `alwaysDecommission`. Keep `worker.replicas` in
step with where
+KEDA has settled, or pass `--set worker.replicas=<current>` when upgrading.
+
+For a continuous-delivery tool, tell it to ignore the field - in Argo CD,
`ignoreDifferences`
+on `/spec/replicas` for the worker statefulsets, with
`RespectIgnoreDifferences=true`.
+
## Documentation
For additional details on deploying the Celeborn Kubernetes Helm chart, please
refer to the [Celeborn on
Kubernetes](https://celeborn.apache.org/docs/latest/deploy_on_k8s/)
documentation.
diff --git a/charts/celeborn/files/worker-drain.sh
b/charts/celeborn/files/worker-drain.sh
new file mode 100644
index 0000000000..a6d9ef99c6
--- /dev/null
+++ b/charts/celeborn/files/worker-drain.sh
@@ -0,0 +1,130 @@
+#!/bin/sh
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# preStop hook for a Celeborn worker. Asks the worker to decommission when a
scale-in is
+# removing it, and to shut down gracefully otherwise. See the chart's
README.md.
+#
+# Reads POD_NAME, POD_IP, STS_NAME, WORKER_HTTP_PORT and ALWAYS_DECOMMISSION
from the
+# environment.
+
+set -u
+
+SA=/var/run/secrets/kubernetes.io/serviceaccount
+
+# Kubernetes counts preStop against terminationGracePeriodSeconds, so no
request may hang: a
+# server that accepts the connection and then stops responding would burn the
whole grace
+# period and the pod would be killed without ever draining. Only the wait
after a successful
+# exit request is allowed to take that long. `-T` is the one timeout flag both
busybox and GNU
+# wget accept.
+HTTP_TIMEOUT=10
+CURL_OPTS="--connect-timeout 5 --max-time $HTTP_TIMEOUT"
+WGET_OPTS="-T $HTTP_TIMEOUT"
+
+# preStop output is not collected, so log through the main process.
+log() {
+ _msg="celeborn preStop: $*"
+ if [ -w /proc/1/fd/1 ]; then
+ echo "$_msg" >/proc/1/fd/1
+ else
+ echo "$_msg"
+ fi
+}
+
+ORDINAL=""
+DESIRED=""
+EXIT_TYPE=GRACEFUL
+
+if [ "${ALWAYS_DECOMMISSION:-false}" = "true" ]; then
+ # Graceful shutdown persists state to recover from on restart. Where the
worker's storage
+ # does not outlive the pod there is nothing to recover, so a rollout would
drop in-flight
+ # shuffles - drain every time instead, whatever is removing this pod.
+ EXIT_TYPE=DECOMMISSION
+ log "alwaysDecommission is set - draining regardless of why this pod is
going away"
+else
+ # A scale-in must decommission, waiting for the shuffle data to expire. A
rolling update or
+ # a node drain must not, or every pod replacement would block for hours. The
statefulset
+ # controller lowers spec.replicas before deleting pods on a scale-in, so an
ordinal at or
+ # above the desired count is being removed for good.
+ ORDINAL=${POD_NAME:-}
+ ORDINAL=${ORDINAL##*-}
+
+ if [ -r "$SA/token" ]; then
+ URL="https://kubernetes.default.svc/apis/apps/v1/namespaces/$(cat
"$SA/namespace")/statefulsets/${STS_NAME:-}/scale"
+ AUTH="Authorization: Bearer $(cat "$SA/token")"
+ if command -v curl >/dev/null 2>&1; then
+ SCALE=$(curl -sS $CURL_OPTS --cacert "$SA/ca.crt" -H "$AUTH" "$URL"
2>/dev/null)
+ else
+ SCALE=$(wget -q $WGET_OPTS -O - --ca-certificate="$SA/ca.crt"
--header="$AUTH" "$URL" 2>/dev/null)
+ fi
+ DESIRED=$(printf '%s' "$SCALE" | sed 's/"status".*//' | grep -o
'"replicas":[0-9 ]*' | head -n 1 | tr -dc '0-9')
+ fi
+
+ case "$ORDINAL" in ''|*[!0-9]*) ORDINAL="" ;; esac
+ case "$DESIRED" in ''|*[!0-9]*) DESIRED="" ;; esac
+
+ # Anything unreadable falls back to a graceful shutdown, so a failed lookup
cannot stall a
+ # rollout with a drain that was never wanted.
+ if [ -n "$ORDINAL" ] && [ -n "$DESIRED" ] && [ "$ORDINAL" -ge "$DESIRED" ];
then
+ EXIT_TYPE=DECOMMISSION
+ fi
+fi
+
+if [ "${ALWAYS_DECOMMISSION:-false}" != "true" ] && [ -z "$DESIRED" ]; then
+ log "could not read ${STS_NAME:-unknown} desired replicas - a scale-in will
NOT decommission"
+fi
+log "ordinal=${ORDINAL:-unknown} desired=${DESIRED:-unknown} exit=$EXIT_TYPE"
+
+BODY='{"type":"'"$EXIT_TYPE"'"}'
+
+# The exit endpoint hands the drain to a separate thread and answers straight
away, so bounding
+# the request does not cut a drain short.
+# The worker's HTTP server binds to one address, not the wildcard:
celeborn.worker.http.host
+# defaults to <localhost>, which resolves to this pod's own address, and Jetty
is given that
+# host. So loopback is refused - target the pod IP, and keep loopback only as
a fallback for a
+# deployment that has pointed the server somewhere else, such as 0.0.0.0.
+HOSTS=""
+if [ -n "${POD_IP:-}" ]; then
+ case "$POD_IP" in
+ *:*) HOSTS="[$POD_IP]" ;;
+ *) HOSTS="$POD_IP" ;;
+ esac
+fi
+HOSTS="$HOSTS 127.0.0.1"
+
+RC=1
+for HOST in $HOSTS; do
+ EXIT_URL="http://$HOST:${WORKER_HTTP_PORT:-9096}/api/v1/workers/exit"
+ if command -v curl >/dev/null 2>&1; then
+ curl -sS -f $CURL_OPTS -X POST -H 'Content-Type: application/json' -d
"$BODY" "$EXIT_URL" >/dev/null 2>&1
+ RC=$?
+ else
+ wget -q $WGET_OPTS -O /dev/null --header='Content-Type: application/json'
--post-data="$BODY" "$EXIT_URL"
+ RC=$?
+ fi
+ if [ "$RC" -eq 0 ]; then
+ break
+ fi
+ log "exit request to $HOST failed"
+done
+
+if [ "$RC" -eq 0 ]; then
+ # Hold the pod open while the worker drains. It exits on its own once done,
and
+ # terminationGracePeriodSeconds bounds the wait.
+ while :; do sleep 5; done
+else
+ log "exit request failed, falling through to SIGTERM"
+fi
diff --git a/charts/celeborn/templates/configmap.yaml
b/charts/celeborn/templates/configmap.yaml
index 8a533f5677..fd1ee75db9 100644
--- a/charts/celeborn/templates/configmap.yaml
+++ b/charts/celeborn/templates/configmap.yaml
@@ -51,3 +51,8 @@ data:
ratis-log4j.properties: |-
{{- .Files.Get "files/conf/ratis-log4j.properties" | nindent 4 }}
+{{- if .Values.worker.drain.enabled }}
+
+ worker-drain.sh: |-
+ {{- .Files.Get "files/worker-drain.sh" | nindent 4 }}
+{{- end }}
diff --git a/charts/celeborn/templates/role.yaml
b/charts/celeborn/templates/role.yaml
index dd48c1f3d2..36a986066c 100644
--- a/charts/celeborn/templates/role.yaml
+++ b/charts/celeborn/templates/role.yaml
@@ -27,5 +27,24 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "list", "delete"]
+{{- if and .Values.worker.drain.enabled (not
.Values.worker.drain.alwaysDecommission) }}
+{{- /*
+The worker's preStop hook reads its own statefulset's desired replica count,
to tell a
+scale-in from a rollout. With alwaysDecommission it never asks, so it needs no
access.
+*/}}
+{{- $root := . }}
+{{- $stsNames := list (include "celeborn.worker.statefulSet.name" .) }}
+{{- if .Values.worker.zoneAwareReplication.enabled }}
+{{- $stsNames = list }}
+{{- range .Values.worker.zoneAwareReplication.zones }}
+{{- $ctx := dict "Values" $root.Values "Release" $root.Release "Chart"
$root.Chart "zone" . }}
+{{- $stsNames = append $stsNames (include "celeborn.worker.statefulSet.name"
$ctx) }}
+{{- end }}
+{{- end }}
+- apiGroups: ["apps"]
+ resources: ["statefulsets/scale"]
+ resourceNames: [{{ range $i, $n := $stsNames }}{{ if $i }}, {{ end }}{{ $n |
quote }}{{ end }}]
+ verbs: ["get"]
+{{- end }}
{{- end }}
diff --git a/charts/celeborn/templates/worker/_helpers.tpl
b/charts/celeborn/templates/worker/_helpers.tpl
index 8d9e86266e..62239f8b6a 100644
--- a/charts/celeborn/templates/worker/_helpers.tpl
+++ b/charts/celeborn/templates/worker/_helpers.tpl
@@ -21,14 +21,22 @@ Common labels for Celeborn worker resources
{{- define "celeborn.worker.labels" -}}
{{ include "celeborn.labels" . }}
app.kubernetes.io/role: worker
+{{- if .zone }}
+celeborn.apache.org/zone: {{ .zone.name }}
+{{- end }}
{{- end }}
{{/*
-Selector labels for Celeborn worker pods
+Selector labels for Celeborn worker pods. The zone label is only added when
rendering
+within a zone context, so that cluster-wide selectors (service, pod monitor)
keep
+matching the workers of every zone.
*/}}
{{- define "celeborn.worker.selectorLabels" -}}
{{ include "celeborn.selectorLabels" . }}
app.kubernetes.io/role: worker
+{{- if .zone }}
+celeborn.apache.org/zone: {{ .zone.name }}
+{{- end }}
{{- end }}
{{/*
@@ -76,8 +84,69 @@ Create the name of the worker priority class to use
Create the name of the worker statefulset to use
*/}}
{{- define "celeborn.worker.statefulSet.name" -}}
+{{- if .zone -}}
+{{ include "celeborn.fullname" . }}-worker-{{ .zone.name }}
+{{- else -}}
{{ include "celeborn.fullname" . }}-worker
{{- end }}
+{{- end }}
+
+{{/*
+Number of replicas for a worker statefulset. Without zone-aware replication
this is
+`worker.replicas` as-is; with it, `worker.replicas` is the total across all
zones and each
+zone gets `ceil(replicas / zones)` unless the zone overrides it.
+*/}}
+{{- define "celeborn.worker.replicas" -}}
+{{- if .zone -}}
+{{- /* Key presence, not truthiness: an explicit 0 parks a zone without
removing it. */ -}}
+{{- if and (hasKey .zone "replicas") (not (kindIs "invalid" .zone.replicas))
-}}
+{{ .zone.replicas }}
+{{- else -}}
+{{ divf .Values.worker.replicas (len
.Values.worker.zoneAwareReplication.zones) | ceil | int }}
+{{- end }}
+{{- else -}}
+{{ .Values.worker.replicas }}
+{{- end }}
+{{- end }}
+
+{{/*
+Label selector used by the built-in autoscaling triggers. Scoped by pod name
to this
+statefulset's own workers, so releases sharing a namespace - or sharing a zone
name - never
+scale on each other's workers. The `zone` metric label is not enough on its
own: it says
+nothing about which release a worker belongs to.
+*/}}
+{{- define "celeborn.worker.autoscaling.selector" -}}
+{{- /* Anchored on the ordinal, so statefulset `x-a` does not also match the
pods of `x-a-y`. */ -}}
+{{ printf "role=\"Worker\",namespace=\"%s\",pod=~\"%s-[0-9]+\""
.Release.Namespace (include "celeborn.worker.statefulSet.name" .) }}
+{{- end }}
+
+{{/*
+Built-in autoscaling triggers, in front of any the user adds. Both exclude a
decommissioning
+worker, whose disk stays full and whose slots stay allocated while it drains -
counting it
+would have the fleet scale out to replace capacity it is still holding.
+*/}}
+{{- define "celeborn.worker.autoscaling.defaultTriggers" -}}
+{{- $selector := include "celeborn.worker.autoscaling.selector" . -}}
+{{- $live := printf "and on (instance)
metrics_IsDecommissioningWorker_Value{%s} == 0" $selector -}}
+{{- if .Values.worker.autoscaling.diskUsage.enabled }}
+- type: prometheus
+ {{- /* Value, not KEDA's AverageValue default: a ratio must not be divided
by the replicas. */}}
+ metricType: Value
+ metadata:
+ serverAddress: {{ required "worker.autoscaling.prometheusAddress is
required by the built-in triggers" .Values.worker.autoscaling.prometheusAddress
}}
+ query: max((1 - metrics_DeviceCelebornFreeBytes_Value{{ printf "{%s}"
$selector }} / metrics_DeviceCelebornTotalBytes_Value{{ printf "{%s}" $selector
}}) {{ $live }})
+ threshold: {{ .Values.worker.autoscaling.diskUsage.threshold | quote }}
+{{- end }}
+{{- if .Values.worker.autoscaling.memoryUsage.enabled }}
+- type: prometheus
+ {{- /* Value, not KEDA's AverageValue default: a ratio must not be divided
by the replicas. */}}
+ metricType: Value
+ metadata:
+ serverAddress: {{ required "worker.autoscaling.prometheusAddress is
required by the built-in triggers" .Values.worker.autoscaling.prometheusAddress
}}
+ query: max(metrics_DirectMemoryUsageRatio_Value{{ printf "{%s}" $selector
}} {{ $live }})
+ threshold: {{ .Values.worker.autoscaling.memoryUsage.threshold | quote }}
+{{- end }}
+{{- end }}
{{/*
Create the name of the worker podmonitor to use
diff --git a/charts/celeborn/templates/worker/scaledobject.yaml
b/charts/celeborn/templates/worker/scaledobject.yaml
new file mode 100644
index 0000000000..02b7e48c7e
--- /dev/null
+++ b/charts/celeborn/templates/worker/scaledobject.yaml
@@ -0,0 +1,77 @@
+{{- /*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/ -}}
+
+{{- if .Values.worker.autoscaling.enabled }}
+{{- if not (or .Values.worker.autoscaling.diskUsage.enabled
.Values.worker.autoscaling.memoryUsage.enabled
.Values.worker.autoscaling.triggers) }}
+{{- fail "worker.autoscaling.enabled is true but no trigger is: enable
worker.autoscaling.diskUsage or memoryUsage, or set
worker.autoscaling.triggers" }}
+{{- end }}
+{{- /* The built-in triggers read metrics from the workers themselves, so at
zero replicas nothing can wake the statefulset. */}}
+{{- $min := .Values.worker.autoscaling.minReplicaCount }}
+{{- if and (not (kindIs "invalid" $min)) (eq (toString $min) "0") (not
.Values.worker.autoscaling.triggers) }}
+{{- fail "worker.autoscaling.minReplicaCount: 0 needs a trigger in
worker.autoscaling.triggers that does not depend on the workers' own metrics:
the built-in ones go silent once the last worker is gone, and the statefulset
could never scale back up" }}
+{{- end }}
+{{- /* One ScaledObject per worker statefulset, so each zone scales on its own
load. */}}
+{{- $zones := list dict }}
+{{- if .Values.worker.zoneAwareReplication.enabled }}
+{{- $zones = .Values.worker.zoneAwareReplication.zones }}
+{{- end }}
+{{- range $index, $zone := $zones }}
+{{- if $index }}
+---
+{{- end }}
+{{- with dict "Values" $.Values "Release" $.Release "Chart" $.Chart
"Capabilities" $.Capabilities "Template" $.Template "Files" $.Files "zone"
$zone }}
+apiVersion: keda.sh/v1alpha1
+kind: ScaledObject
+metadata:
+ name: {{ include "celeborn.worker.statefulSet.name" . }}
+ namespace: {{ .Release.Namespace }}
+ labels:
+ {{- include "celeborn.worker.labels" . | nindent 4 }}
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: StatefulSet
+ name: {{ include "celeborn.worker.statefulSet.name" . }}
+ {{- /* Not `default`: it treats an explicit 0 as unset, which rules out
scaling to zero. */}}
+ minReplicaCount: {{ if kindIs "invalid"
.Values.worker.autoscaling.minReplicaCount }}{{ include
"celeborn.worker.replicas" . }}{{ else }}{{
.Values.worker.autoscaling.minReplicaCount }}{{ end }}
+ maxReplicaCount: {{ .Values.worker.autoscaling.maxReplicaCount }}
+ pollingInterval: {{ .Values.worker.autoscaling.pollingInterval }}
+ cooldownPeriod: {{ .Values.worker.autoscaling.cooldownPeriod }}
+ {{- with .Values.worker.autoscaling.behavior }}
+ advanced:
+ horizontalPodAutoscalerConfig:
+ behavior:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ triggers:
+ {{- with include "celeborn.worker.autoscaling.defaultTriggers" . | trim }}
+ {{- . | nindent 4 }}
+ {{- end }}
+ {{- $ctx := . }}
+ {{- with .Values.worker.autoscaling.triggers }}
+ {{- /*
+ Checked before rendering: without a zone `.zone.name` yields an empty
string, so a
+ `zone=""` matcher would quietly match every worker rather than failing.
+ */}}
+ {{- if and (not $ctx.Values.worker.zoneAwareReplication.enabled) (contains
".zone" (toYaml .)) }}
+ {{- fail "a worker.autoscaling.triggers entry references .zone, which only
has a value when worker.zoneAwareReplication is enabled" }}
+ {{- end }}
+ {{- tpl (toYaml .) $ctx | nindent 4 }}
+ {{- end }}
+{{- end }}
+{{- end }}
+{{- end }}
diff --git a/charts/celeborn/templates/worker/statefulset.yaml
b/charts/celeborn/templates/worker/statefulset.yaml
index 6b7e340619..06664063dc 100644
--- a/charts/celeborn/templates/worker/statefulset.yaml
+++ b/charts/celeborn/templates/worker/statefulset.yaml
@@ -15,6 +15,55 @@ See the License for the specific language governing
permissions and
limitations under the License.
*/ -}}
+{{- /*
+One statefulset per zone when zone-aware replication is enabled, a single one
otherwise.
+The no-zone case iterates once over an empty dict, which `if .zone` treats as
unset.
+*/ -}}
+{{- $zones := list dict }}
+{{- if .Values.worker.zoneAwareReplication.enabled }}
+{{- if not .Values.worker.zoneAwareReplication.zones }}
+{{- fail "worker.zoneAwareReplication.enabled is true but
worker.zoneAwareReplication.zones is empty" }}
+{{- end }}
+{{- $zones = .Values.worker.zoneAwareReplication.zones }}
+{{- $names := list }}
+{{- range $zone := $zones }}
+{{- if not $zone.name }}
+{{- fail "every entry in worker.zoneAwareReplication.zones needs a name" }}
+{{- end }}
+{{- $names = append $names $zone.name }}
+{{- end }}
+{{- if ne (len (uniq $names)) (len $names) }}
+{{- fail "worker.zoneAwareReplication.zones has duplicate names" }}
+{{- end }}
+{{- end }}
+{{- /* The drain hook's exit request is plain HTTP with no credentials. See
README.md. */}}
+{{- if .Values.worker.drain.enabled }}
+{{- if eq (get .Values.celeborn "celeborn.worker.http.ssl.enabled" | toString)
"true" }}
+{{- fail "worker.drain does not support celeborn.worker.http.ssl.enabled: the
preStop hook calls the worker over plain HTTP" }}
+{{- end }}
+{{- if get .Values.celeborn "celeborn.worker.http.auth.supportedSchemes" |
toString | trim }}
+{{- fail "worker.drain does not support
celeborn.worker.http.auth.supportedSchemes: the preStop hook sends no
credentials" }}
+{{- end }}
+{{- end }}
+{{- range $index, $zone := $zones }}
+{{- if $index }}
+---
+{{- end }}
+{{- /* Render the body against a context carrying the zone, so `.` keeps
working below. */}}
+{{- with dict "Values" $.Values "Release" $.Release "Chart" $.Chart
"Capabilities" $.Capabilities "Template" $.Template "Files" $.Files "zone"
$zone }}
+{{- $drainMount := .Values.worker.drain.enabled }}
+{{- /*
+Computed once: used both as an env var and referenced from the start command.
The value is
+never interpolated into the shell string - `$()` or a quote in a legitimate
label would
+otherwise be evaluated by /bin/sh at startup.
+*/}}
+{{- $zoneMetricLabels := "" }}
+{{- if and $zone .Values.worker.zoneAwareReplication.metricsLabel }}
+{{- $zoneMetricLabels = printf "zone=%s" $zone.name }}
+{{- with get .Values.celeborn "celeborn.metrics.extraLabels" }}
+{{- $zoneMetricLabels = printf "%s,%s" . $zoneMetricLabels }}
+{{- end }}
+{{- end }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
@@ -23,7 +72,7 @@ metadata:
labels:
{{- include "celeborn.worker.labels" . | nindent 4 }}
spec:
- replicas: {{ .Values.worker.replicas }}
+ replicas: {{ include "celeborn.worker.replicas" . }}
selector:
matchLabels:
{{- include "celeborn.worker.selectorLabels" . | nindent 6 }}
@@ -103,15 +152,51 @@ spec:
true; do
echo "waiting for master";
sleep 2;
- done && exec /opt/celeborn/sbin/start-worker.sh
+ {{- /*
+ Publish the zone as a metric label, so queries select on it rather
than on pod names.
+ The conf file is loaded with setIfMissing, so a system property wins
over it, and
+ start-worker.sh builds CELEBORN_JAVA_OPTS from
CELEBORN_WORKER_JAVA_OPTS - so this
+ composes with whatever `worker.env` already sets.
+ */}}
+ {{- $zoneOpts := "" }}
+ {{- if $zoneMetricLabels }}
+ {{- $zoneOpts = "export
CELEBORN_WORKER_JAVA_OPTS=\"${CELEBORN_WORKER_JAVA_OPTS:-}
-Dceleborn.metrics.extraLabels=$CELEBORN_ZONE_METRIC_LABELS\" && " }}
+ {{- end }}
+ done && {{ $zoneOpts }}exec /opt/celeborn/sbin/start-worker.sh
ports:
- containerPort: {{ get .Values.celeborn "celeborn.worker.http.port" |
default 9096 }}
name: metrics
protocol: TCP
- {{- with .Values.worker.env }}
+ {{- $drain := $drainMount }}
+ {{- if or $drain $zoneMetricLabels .Values.worker.env }}
env:
+ {{- with $zoneMetricLabels }}
+ - name: CELEBORN_ZONE_METRIC_LABELS
+ value: {{ . | quote }}
+ {{- end }}
+ {{- if $drain }}
+ {{- /* Not $HOSTNAME: with worker.hostNetwork that is the node name,
whose trailing
+ number would be read as an ordinal and decommission the worker on
every rollout. */}}
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ {{- /* The worker's http server binds to this address, not the
wildcard. */}}
+ - name: POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: STS_NAME
+ value: {{ include "celeborn.worker.statefulSet.name" . }}
+ - name: WORKER_HTTP_PORT
+ value: {{ get .Values.celeborn "celeborn.worker.http.port" | default
9096 | quote }}
+ - name: ALWAYS_DECOMMISSION
+ value: {{ .Values.worker.drain.alwaysDecommission | quote }}
+ {{- end }}
+ {{- with .Values.worker.env }}
{{- toYaml . | nindent 8 }}
{{- end }}
+ {{- end }}
{{- with .Values.worker.envFrom }}
envFrom:
{{- toYaml . | nindent 8 }}
@@ -126,9 +211,23 @@ spec:
- name: celeborn-conf
subPath: metrics.properties
mountPath: /opt/celeborn/conf/metrics.properties
+ {{- if $drainMount }}
+ - name: celeborn-conf
+ subPath: worker-drain.sh
+ mountPath: /opt/celeborn/drain/worker-drain.sh
+ {{- end }}
{{- with .Values.worker.volumeMounts }}
{{- toYaml . | nindent 8 }}
{{- end }}
+ {{- if $drain }}
+ {{- /* The drain logic lives in files/worker-drain.sh, shipped in the
config map. */}}
+ lifecycle:
+ preStop:
+ exec:
+ command:
+ - /bin/sh
+ - /opt/celeborn/drain/worker-drain.sh
+ {{- end }}
{{- with .Values.worker.resources }}
resources:
{{- toYaml . | nindent 10 }}
@@ -153,14 +252,24 @@ spec:
path: log4j2.xml
- key: metrics.properties
path: metrics.properties
+ {{- if $drainMount }}
+ - key: worker-drain.sh
+ path: worker-drain.sh
+ {{- end }}
{{- with .Values.worker.volumes }}
{{- toYaml . | nindent 6 }}
{{- end }}
- {{- with .Values.worker.nodeSelector }}
+ {{- $nodeSelector := merge (deepCopy (default dict (and .zone
.zone.nodeSelector))) (default dict .Values.worker.nodeSelector) }}
+ {{- with $nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
- {{- with .Values.worker.affinity }}
+ {{- $affinity := .Values.worker.affinity }}
+ {{- /* Key presence, not truthiness: `affinity: {}` clears the global
one for this zone. */}}
+ {{- if and .zone (hasKey .zone "affinity") }}
+ {{- $affinity = .zone.affinity }}
+ {{- end }}
+ {{- with $affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -187,3 +296,5 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
terminationGracePeriodSeconds: {{
.Values.worker.terminationGracePeriodSeconds }}
+{{- end }}
+{{- end }}
diff --git a/charts/celeborn/tests/configmap_test.yaml
b/charts/celeborn/tests/configmap_test.yaml
index 397e75bd2a..2579964e04 100644
--- a/charts/celeborn/tests/configmap_test.yaml
+++ b/charts/celeborn/tests/configmap_test.yaml
@@ -57,3 +57,21 @@ tests:
- notMatchRegex:
path: data["celeborn-defaults.conf"]
pattern: "(?m)^\\s*[^#\\s][^=\\n]*\\s+=.*"
+
+ - it: Should not ship the worker drain script as default
+ asserts:
+ - notExists:
+ path: data["worker-drain.sh"]
+
+ - it: Should ship the worker drain script when the drain is enabled
+ set:
+ worker:
+ drain:
+ enabled: true
+ asserts:
+ - matchRegex:
+ path: data["worker-drain.sh"]
+ pattern: EXIT_TYPE=DECOMMISSION
+ - matchRegex:
+ path: data["worker-drain.sh"]
+ pattern: \$\{POD_NAME:-\}
diff --git a/charts/celeborn/tests/role_test.yaml
b/charts/celeborn/tests/role_test.yaml
index 8165f6c535..24f0c4fa71 100644
--- a/charts/celeborn/tests/role_test.yaml
+++ b/charts/celeborn/tests/role_test.yaml
@@ -48,3 +48,55 @@ tests:
kind: Role
name: default
namespace: celeborn
+
+ - it: Should not grant statefulset scale access as default
+ asserts:
+ - equal:
+ path: rules
+ value:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs: ["create", "list", "delete"]
+
+ - it: Should grant statefulset scale access when the drain is enabled
+ set:
+ worker:
+ drain:
+ enabled: true
+ asserts:
+ - equal:
+ path: rules[1]
+ value:
+ apiGroups: ["apps"]
+ resources: ["statefulsets/scale"]
+ resourceNames: ["celeborn-worker"]
+ verbs: ["get"]
+
+ - it: Should not grant statefulset scale access when the drain always
decommissions
+ set:
+ worker:
+ drain:
+ enabled: true
+ alwaysDecommission: true
+ asserts:
+ - equal:
+ path: rules
+ value:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs: ["create", "list", "delete"]
+
+ - it: Should scope the scale access to every zone statefulset
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ - name: us-east-1b
+ drain:
+ enabled: true
+ asserts:
+ - equal:
+ path: rules[1].resourceNames
+ value: ["celeborn-worker-us-east-1a", "celeborn-worker-us-east-1b"]
diff --git a/charts/celeborn/tests/worker/scaledobject_test.yaml
b/charts/celeborn/tests/worker/scaledobject_test.yaml
new file mode 100644
index 0000000000..e5faf587ca
--- /dev/null
+++ b/charts/celeborn/tests/worker/scaledobject_test.yaml
@@ -0,0 +1,361 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+suite: Test Celeborn worker scaled object
+
+templates:
+ - worker/scaledobject.yaml
+
+release:
+ name: celeborn
+
+tests:
+ - it: Should not create a scaled object as default
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ - it: Should create a scaled object if `worker.autoscaling.enabled` is true
+ set:
+ worker:
+ replicas: 4
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ maxReplicaCount: 8
+ triggers:
+ - type: prometheus
+ metadata:
+ serverAddress: http://prometheus:9090
+ query: metrics_ActiveShuffleSize_Value
+ threshold: "1000"
+ asserts:
+ - hasDocuments:
+ count: 1
+ - equal:
+ path: metadata.name
+ value: celeborn-worker
+ - equal:
+ path: spec.scaleTargetRef
+ value:
+ apiVersion: apps/v1
+ kind: StatefulSet
+ name: celeborn-worker
+ - equal:
+ path: spec.minReplicaCount
+ value: 4
+ - equal:
+ path: spec.maxReplicaCount
+ value: 8
+
+ - it: Should create one scaled object per zone, each targeting its own
statefulset
+ set:
+ worker:
+ replicas: 9
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ - name: us-east-1b
+ - name: us-east-1c
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ triggers:
+ - type: prometheus
+ metadata:
+ serverAddress: http://prometheus:9090
+ query: metrics_ActiveShuffleSize_Value
+ threshold: "1000"
+ asserts:
+ - hasDocuments:
+ count: 3
+ - equal:
+ path: metadata.name
+ value: celeborn-worker-us-east-1c
+ documentIndex: 2
+ - equal:
+ path: spec.scaleTargetRef.name
+ value: celeborn-worker-us-east-1c
+ documentIndex: 2
+ - equal:
+ path: spec.minReplicaCount
+ value: 3
+ documentIndex: 2
+
+ - it: Should render the zone into the trigger query
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1b
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ triggers:
+ - type: prometheus
+ metadata:
+ serverAddress: http://prometheus:9090
+ query:
metrics_ActiveShuffleSize_Value{pod=~"celeborn-worker-{{ .zone.name }}-.*"}
+ threshold: "1000"
+ asserts:
+ - equal:
+ path: spec.triggers[2].metadata.query
+ value:
metrics_ActiveShuffleSize_Value{pod=~"celeborn-worker-us-east-1b-.*"}
+
+ - it: Should use `worker.autoscaling.minReplicaCount` when it is set
+ set:
+ worker:
+ replicas: 9
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ minReplicaCount: 2
+ triggers:
+ - type: prometheus
+ metadata:
+ serverAddress: http://prometheus:9090
+ query: metrics_ActiveShuffleSize_Value
+ threshold: "1000"
+ asserts:
+ - equal:
+ path: spec.minReplicaCount
+ value: 2
+
+ - it: Should pass scaling behavior through to the HPA
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ behavior:
+ scaleDown:
+ stabilizationWindowSeconds: 1800
+ triggers:
+ - type: prometheus
+ metadata:
+ serverAddress: http://prometheus:9090
+ query: metrics_ActiveShuffleSize_Value
+ threshold: "1000"
+ asserts:
+ - equal:
+ path:
spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.stabilizationWindowSeconds
+ value: 1800
+
+ - it: Should add the disk and memory usage triggers by default
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ asserts:
+ - lengthEqual:
+ path: spec.triggers
+ count: 2
+ - equal:
+ path: spec.triggers[0].metricType
+ value: Value
+ - matchRegex:
+ path: spec.triggers[0].metadata.query
+ pattern:
metrics_DeviceCelebornFreeBytes_Value\{role="Worker",namespace="NAMESPACE",pod=~"celeborn-worker-\[0-9\]\+"\}
+ - equal:
+ path: spec.triggers[0].metadata.threshold
+ value: "0.70"
+ - equal:
+ path: spec.triggers[1].metricType
+ value: Value
+ - matchRegex:
+ path: spec.triggers[1].metadata.query
+ pattern:
max\(metrics_DirectMemoryUsageRatio_Value\{role="Worker",namespace="NAMESPACE",pod=~"celeborn-worker-\[0-9\]\+"\}
+ - equal:
+ path: spec.triggers[1].metadata.threshold
+ value: "0.70"
+
+ - it: Should exclude decommissioning workers from both default triggers
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ asserts:
+ - matchRegex:
+ path: spec.triggers[0].metadata.query
+ pattern:
metrics_IsDecommissioningWorker_Value\{role="Worker",namespace="NAMESPACE",pod=~"celeborn-worker-\[0-9\]\+"\}
== 0
+ - matchRegex:
+ path: spec.triggers[1].metadata.query
+ pattern:
metrics_IsDecommissioningWorker_Value\{role="Worker",namespace="NAMESPACE",pod=~"celeborn-worker-\[0-9\]\+"\}
== 0
+ - notMatchRegex:
+ path: spec.triggers[0].metadata.query
+ pattern: DirectMemoryUsageRatio
+
+ - it: Should scope the default triggers to the zone's own statefulset
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ asserts:
+ - matchRegex:
+ path: spec.triggers[0].metadata.query
+ pattern:
role="Worker",namespace="NAMESPACE",pod=~"celeborn-worker-us-east-1a-\[0-9\]\+"
+ - matchRegex:
+ path: spec.triggers[1].metadata.query
+ pattern:
role="Worker",namespace="NAMESPACE",pod=~"celeborn-worker-us-east-1a-\[0-9\]\+"
+
+ - it: Should scope by pod name when the zone metric label is off
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ metricsLabel: false
+ zones:
+ - name: us-east-1a
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ asserts:
+ - matchRegex:
+ path: spec.triggers[0].metadata.query
+ pattern:
role="Worker",namespace="NAMESPACE",pod=~"celeborn-worker-us-east-1a-\[0-9\]\+"
+
+ - it: Should not select another release's workers in the same namespace
+ release:
+ name: beta
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ asserts:
+ - matchRegex:
+ path: spec.triggers[0].metadata.query
+ pattern: pod=~"beta-celeborn-worker-\[0-9\]\+"
+ - notMatchRegex:
+ path: spec.triggers[0].metadata.query
+ pattern: pod=~"alpha-celeborn-worker
+
+ - it: Should not select another release's workers sharing a zone name
+ release:
+ name: beta
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ asserts:
+ - matchRegex:
+ path: spec.triggers[0].metadata.query
+ pattern: pod=~"beta-celeborn-worker-us-east-1a-\[0-9\]\+"
+
+ - it: Should drop a default trigger that is disabled
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ memoryUsage:
+ enabled: false
+ asserts:
+ - lengthEqual:
+ path: spec.triggers
+ count: 1
+ - equal:
+ path: spec.triggers[0].metricType
+ value: Value
+
+ - it: Should append user triggers after the built-in ones
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ triggers:
+ - type: cron
+ metadata:
+ timezone: UTC
+ start: 0 6 * * *
+ end: 0 20 * * *
+ desiredReplicas: "6"
+ asserts:
+ - lengthEqual:
+ path: spec.triggers
+ count: 3
+ - equal:
+ path: spec.triggers[2].type
+ value: cron
+
+ - it: Should fail if a built-in trigger is enabled without a prometheus
address
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ asserts:
+ - failedTemplate:
+ errorMessage: worker.autoscaling.prometheusAddress is required by
the built-in triggers
+
+ - it: Should fail if autoscaling is enabled with no trigger at all
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ diskUsage:
+ enabled: false
+ memoryUsage:
+ enabled: false
+ triggers: []
+ asserts:
+ - failedTemplate:
+ errorMessage: "worker.autoscaling.enabled is true but no trigger is:
enable worker.autoscaling.diskUsage or memoryUsage, or set
worker.autoscaling.triggers"
+
+ - it: Should allow scaling to zero with a trigger of its own
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ minReplicaCount: 0
+ triggers:
+ - type: cron
+ metadata:
+ timezone: UTC
+ start: 0 6 * * *
+ end: 0 20 * * *
+ desiredReplicas: "3"
+ asserts:
+ - equal:
+ path: spec.minReplicaCount
+ value: 0
+
+ - it: Should fail scaling to zero on the built-in triggers alone
+ set:
+ worker:
+ autoscaling:
+ enabled: true
+ prometheusAddress: http://prometheus:9090
+ minReplicaCount: 0
+ asserts:
+ - failedTemplate:
+ errorMessage: "worker.autoscaling.minReplicaCount: 0 needs a trigger
in worker.autoscaling.triggers that does not depend on the workers' own
metrics: the built-in ones go silent once the last worker is gone, and the
statefulset could never scale back up"
diff --git a/charts/celeborn/tests/worker/statefulset_test.yaml
b/charts/celeborn/tests/worker/statefulset_test.yaml
index ba2b23b904..ce8661be91 100644
--- a/charts/celeborn/tests/worker/statefulset_test.yaml
+++ b/charts/celeborn/tests/worker/statefulset_test.yaml
@@ -546,3 +546,424 @@ tests:
asserts:
- notExists:
path: spec.template.spec.initContainers
+
+ - it: Should create a single worker statefulset when zone-aware replication
is disabled
+ template: worker/statefulset.yaml
+ asserts:
+ - hasDocuments:
+ count: 1
+ - equal:
+ path: metadata.name
+ value: celeborn-worker
+ - notExists:
+ path: spec.selector.matchLabels["celeborn.apache.org/zone"]
+
+ - it: Should create one worker statefulset per zone if
`worker.zoneAwareReplication` is enabled
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ replicas: 9
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ - name: us-east-1b
+ - name: us-east-1c
+ asserts:
+ - hasDocuments:
+ count: 3
+ - equal:
+ path: metadata.name
+ value: celeborn-worker-us-east-1b
+ documentIndex: 1
+ - equal:
+ path: spec.selector.matchLabels["celeborn.apache.org/zone"]
+ value: us-east-1b
+ documentIndex: 1
+ - equal:
+ path: spec.template.metadata.labels["celeborn.apache.org/zone"]
+ value: us-east-1b
+ documentIndex: 1
+ - equal:
+ path: spec.serviceName
+ value: celeborn-worker-svc
+ documentIndex: 1
+
+ - it: Should split `worker.replicas` across zones, rounding up
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ replicas: 5
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ - name: us-east-1b
+ - name: us-east-1c
+ asserts:
+ - equal:
+ path: spec.replicas
+ value: 2
+
+ - it: Should use the zone replicas if `zones[].replicas` is set
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ replicas: 9
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ replicas: 7
+ asserts:
+ - equal:
+ path: spec.replicas
+ value: 7
+
+ - it: Should merge the zone node selector over `worker.nodeSelector`
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ nodeSelector:
+ workload: celeborn
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ nodeSelector:
+ topology.kubernetes.io/zone: us-east-1a
+ asserts:
+ - equal:
+ path: spec.template.spec.nodeSelector
+ value:
+ workload: celeborn
+ topology.kubernetes.io/zone: us-east-1a
+
+ - it: Should use the zone affinity if `zones[].affinity` is set
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: topology.kubernetes.io/zone
+ operator: In
+ values:
+ - us-east-1a
+ asserts:
+ - equal:
+ path:
spec.template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values[0]
+ value: us-east-1a
+
+ - it: Should fail if zone-aware replication is enabled without zones
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones: []
+ asserts:
+ - failedTemplate:
+ errorMessage: worker.zoneAwareReplication.enabled is true but
worker.zoneAwareReplication.zones is empty
+
+ - it: Should not have a preStop drain hook as default
+ template: worker/statefulset.yaml
+ asserts:
+ - notExists:
+ path: spec.template.spec.containers[0].lifecycle
+ - notContains:
+ path: spec.template.spec.volumes[0].configMap.items
+ content:
+ key: worker-drain.sh
+ path: worker-drain.sh
+
+ - it: Should run the mounted drain script from the preStop hook
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ drain:
+ enabled: true
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].lifecycle.preStop.exec.command
+ value:
+ - /bin/sh
+ - /opt/celeborn/drain/worker-drain.sh
+ - contains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: celeborn-conf
+ subPath: worker-drain.sh
+ mountPath: /opt/celeborn/drain/worker-drain.sh
+ - contains:
+ path: spec.template.spec.volumes[0].configMap.items
+ content:
+ key: worker-drain.sh
+ path: worker-drain.sh
+
+ - it: Should fail the drain with worker HTTP TLS enabled
+ template: worker/statefulset.yaml
+ set:
+ celeborn:
+ celeborn.worker.http.ssl.enabled: true
+ worker:
+ drain:
+ enabled: true
+ asserts:
+ - failedTemplate:
+ errorMessage: "worker.drain does not support
celeborn.worker.http.ssl.enabled: the preStop hook calls the worker over plain
HTTP"
+
+ - it: Should fail the drain with worker HTTP authentication enabled
+ template: worker/statefulset.yaml
+ set:
+ celeborn:
+ celeborn.worker.http.auth.supportedSchemes: BASIC
+ worker:
+ drain:
+ enabled: true
+ asserts:
+ - failedTemplate:
+ errorMessage: "worker.drain does not support
celeborn.worker.http.auth.supportedSchemes: the preStop hook sends no
credentials"
+
+ - it: Should allow worker HTTP TLS when the drain is disabled
+ template: worker/statefulset.yaml
+ set:
+ celeborn:
+ celeborn.worker.http.ssl.enabled: true
+ celeborn.worker.http.auth.supportedSchemes: BASIC
+ asserts:
+ - notExists:
+ path: spec.template.spec.containers[0].lifecycle
+
+ - it: Should pass the pod, statefulset and port to the drain script
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ drain:
+ enabled: true
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].env[0]
+ value:
+ name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - equal:
+ path: spec.template.spec.containers[0].env[1]
+ value:
+ name: POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - equal:
+ path: spec.template.spec.containers[0].env[2]
+ value:
+ name: STS_NAME
+ value: celeborn-worker
+ - equal:
+ path: spec.template.spec.containers[0].env[3]
+ value:
+ name: WORKER_HTTP_PORT
+ value: "9096"
+ - equal:
+ path: spec.template.spec.containers[0].env[4]
+ value:
+ name: ALWAYS_DECOMMISSION
+ value: "false"
+
+ - it: Should tell the drain script to always decommission when asked
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ drain:
+ enabled: true
+ alwaysDecommission: true
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].env[4].value
+ value: "true"
+
+ - it: Should point the drain script at the zone statefulset
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ drain:
+ enabled: true
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: STS_NAME
+ value: celeborn-worker-us-east-1a
+
+ - it: Should not add the preStop drain hook if `worker.drain` is disabled
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ drain:
+ enabled: false
+ asserts:
+ - notExists:
+ path: spec.template.spec.containers[0].lifecycle
+
+ - it: Should not inject POD_NAME when the drain is disabled
+ template: worker/statefulset.yaml
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].env[0].name
+ value: CELEBORN_WORKER_MEMORY
+
+ - it: Should not set the zone metric label when zone-aware replication is off
+ template: worker/statefulset.yaml
+ asserts:
+ - notMatchRegex:
+ path: spec.template.spec.containers[0].command[4]
+ pattern: extraLabels
+
+ - it: Should publish the zone as a metric label in each zone
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ - name: us-east-1b
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: CELEBORN_ZONE_METRIC_LABELS
+ value: zone=us-east-1b
+ documentIndex: 1
+ - matchRegex:
+ path: spec.template.spec.containers[0].command[4]
+ pattern: extraLabels=\$CELEBORN_ZONE_METRIC_LABELS
+ documentIndex: 1
+
+ - it: Should append the zone to any configured extra metric labels
+ template: worker/statefulset.yaml
+ set:
+ celeborn:
+ celeborn.metrics.extraLabels: env=prod
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: CELEBORN_ZONE_METRIC_LABELS
+ value: env=prod,zone=us-east-1a
+
+ - it: Should not set the zone metric label if `metricsLabel` is false
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ metricsLabel: false
+ zones:
+ - name: us-east-1a
+ asserts:
+ - notMatchRegex:
+ path: spec.template.spec.containers[0].command[4]
+ pattern: extraLabels
+
+ - it: Should fail if a zone has no name
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - nodeSelector:
+ topology.kubernetes.io/zone: us-east-1a
+ asserts:
+ - failedTemplate:
+ errorMessage: every entry in worker.zoneAwareReplication.zones needs
a name
+
+ - it: Should fail if two zones share a name
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ - name: us-east-1a
+ asserts:
+ - failedTemplate:
+ errorMessage: worker.zoneAwareReplication.zones has duplicate names
+
+ - it: "Should honour an explicit `zones[].replicas: 0`"
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ replicas: 6
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ replicas: 0
+ - name: us-east-1b
+ asserts:
+ - equal:
+ path: spec.replicas
+ value: 0
+ documentIndex: 0
+ - equal:
+ path: spec.replicas
+ value: 3
+ documentIndex: 1
+
+ - it: Should let an empty `zones[].affinity` clear the chart affinity for
that zone
+ template: worker/statefulset.yaml
+ set:
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ affinity: {}
+ - name: us-east-1b
+ asserts:
+ - notExists:
+ path: spec.template.spec.affinity
+ documentIndex: 0
+ - exists:
+ path: spec.template.spec.affinity.podAntiAffinity
+ documentIndex: 1
+
+ - it: Should not interpolate the metric label value into the start command
+ template: worker/statefulset.yaml
+ set:
+ celeborn:
+ celeborn.metrics.extraLabels: env=$(id -u)
+ worker:
+ zoneAwareReplication:
+ enabled: true
+ zones:
+ - name: us-east-1a
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: CELEBORN_ZONE_METRIC_LABELS
+ value: env=$(id -u),zone=us-east-1a
+ - notMatchRegex:
+ path: spec.template.spec.containers[0].command[4]
+ pattern: id -u
diff --git a/charts/celeborn/values.yaml b/charts/celeborn/values.yaml
index 4bdabfde66..4fb8633959 100644
--- a/charts/celeborn/values.yaml
+++ b/charts/celeborn/values.yaml
@@ -223,11 +223,114 @@ master:
worker:
# -- Number of Celeborn worker replicas to deploy, should less than node
number.
+ # With `worker.zoneAwareReplication.enabled`, this is the total across all
zones and each
+ # zone deploys `ceil(replicas / number of zones)`.
replicas: 5
- # -- Value for terminationGracePeriodSeconds for worker pods
+ # Zone-aware replication: render one worker statefulset per zone instead of
a single one
+ # for the whole cluster, so zone membership is a property of the statefulset
rather than
+ # the outcome of scheduling. See README.md.
+ zoneAwareReplication:
+ # -- Whether to deploy one Celeborn worker statefulset per zone.
+ enabled: false
+ # -- Publish the zone as a `zone` label on every metric the worker emits,
through
+ # `celeborn.metrics.extraLabels`, so autoscaling and dashboards can select
a zone without
+ # matching on pod names. Appends to any `celeborn.metrics.extraLabels`
already configured.
+ metricsLabel: true
+ # -- Zone definitions. Redefine the whole list to change any part of it,
as YAML cannot
+ # merge list entries.
+ zones: []
+ # -- Name of the zone. Used in the statefulset name and in the
+ # `celeborn.apache.org/zone` label, so it must be a valid label value.
+ # - name: us-east-1a
+ # # -- Node selector for this zone, merged over `worker.nodeSelector`
(this one wins).
+ # nodeSelector:
+ # topology.kubernetes.io/zone: us-east-1a
+ # # -- Replicas for this zone. Overrides the even split of
`worker.replicas`.
+ # replicas: 3
+ # # -- Affinity for this zone. Replaces `worker.affinity` when set.
+ # affinity: {}
+
+ # -- Value for terminationGracePeriodSeconds for worker pods. Raise it above
+ # `celeborn.worker.decommission.forceExitTimeout` when `worker.drain` is
enabled, otherwise
+ # kubelet kills a draining worker before it finishes. See README.md.
terminationGracePeriodSeconds: 30
+ # Ask a worker to drain before Kubernetes removes it. Deleting a pod only
sends SIGTERM,
+ # which never reaches Celeborn's decommission path, so the worker would
leave with its
+ # shuffle data and any application still reading it loses that stage. Useful
with a fixed
+ # replica count as well as with `worker.autoscaling`.
+ drain:
+ # -- Whether to add a preStop hook that asks the worker to decommission
when a scale-in is
+ # removing it, and to shut down gracefully otherwise. The pod tells the
two apart by
+ # comparing its own ordinal with the statefulset's desired replica count,
and falls back to
+ # a graceful shutdown when it cannot read it. Enable this alongside
`worker.autoscaling`:
+ # without it a scale-in is destructive. Needs `rbac.create` and
+ # `celeborn.worker.graceful.shutdown.enabled`. Not supported with worker
HTTP TLS or
+ # authentication. See README.md.
+ enabled: false
+ # -- Decommission on every termination, not only a scale-in. Set this when
the worker's
+ # storage does not outlive its pod: graceful shutdown persists state to
recover from on
+ # restart, and with nothing to recover a rollout drops its in-flight
shuffles. The cost is
+ # that rollouts and node drains then wait for the drain too, bounded by
+ # `celeborn.worker.decommission.forceExitTimeout`. See README.md.
+ alwaysDecommission: false
+
+ # Autoscaling for Celeborn workers, via a KEDA ScaledObject per worker
statefulset.
+ # Requires KEDA in the cluster. See README.md.
+ autoscaling:
+ # -- Whether to create a KEDA ScaledObject for each worker statefulset.
+ enabled: false
+ # -- Lower bound. Empty means the statefulset's own replica count, which is
+ # `worker.replicas` split across zones when zone-aware replication is
enabled. 0 needs a
+ # trigger in `triggers` that does not depend on the workers' own metrics.
See README.md.
+ minReplicaCount:
+ # -- Upper bound, per statefulset (so per zone, when zones are enabled).
+ maxReplicaCount: 10
+ # -- How often KEDA evaluates the triggers, in seconds.
+ pollingInterval: 30
+ # -- Seconds before scaling to zero. Only applies with `minReplicaCount:
0`; pace
+ # ordinary scale-in with `behavior.scaleDown` instead.
+ cooldownPeriod: 300
+ # -- Scaling behaviour, passed through to the underlying HPA.
+ behavior: {}
+ # -- Prometheus-compatible endpoint the built-in triggers below query.
Required unless
+ # both are disabled.
+ prometheusAddress: ""
+
+ # Scale out when the fullest worker runs out of disk. This is the signal
that bites
+ # first: a worker that fills up stops accepting pushes.
+ # This doubles as the slot utilization target: a disk's `maxSlots` is its
total space
+ # divided by the estimated partition size, so slots and disk fill in step.
See README.md.
+ diskUsage:
+ # -- Whether to add the disk usage trigger.
+ enabled: true
+ # -- Fraction of the worker's Celeborn disk capacity in use, between 0
and 1.
+ threshold: "0.70"
+
+ # Scale out when workers run short of off-heap memory. A worker stops
accepting pushes at
+ # `celeborn.worker.directMemoryRatioToPauseReceive`, so this is the other
way a worker
+ # takes itself out of service. See README.md.
+ memoryUsage:
+ # -- Whether to add the off-heap memory trigger.
+ enabled: true
+ # -- Fraction of the worker's off-heap memory in use, between 0 and 1.
Keep it below
+ # `celeborn.worker.directMemoryRatioToPauseReceive` (0.85) so capacity
arrives before
+ # pushes pause.
+ threshold: "0.70"
+
+ # -- Extra KEDA triggers, appended to the built-in ones. Rendered through
`tpl`, so
+ # `{{ .zone.name }}` resolves to the zone of the statefulset being scaled
and one
+ # definition covers every zone. Referencing it with zones off is rejected
at render time.
+ triggers: []
+ # - type: prometheus
+ # # `Value`, not KEDA's `AverageValue` default, for a saturation ratio.
See README.md.
+ # metricType: Value
+ # metadata:
+ # serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
+ # query: max(1 -
metrics_DeviceCelebornFreeBytes_Value{role="Worker",zone="{{ .zone.name }}"} /
metrics_DeviceCelebornTotalBytes_Value{role="Worker",zone="{{ .zone.name }}"})
+ # threshold: "0.7"
+
# -- Annotations for Celeborn worker pods.
annotations:
# key1: value1