Conversation
When an executor exceeds its container memory limit the kernel kills the whole JVM, taking every task on it along with its cached blocks and shuffle files. Failing a single task is retried by Spark and costs far less. Sample the container's memory usage from the cgroup at existing execution checkpoints and fail the current task once usage reaches a configurable fraction of the limit. Read the kernel's number rather than Comet's own allocator accounting: measurement on TPC-H SF100 showed the allocator balance does not lead RSS at any horizon and that the offset between them wanders further than the balance's entire range, so a threshold on it cannot say anything about what the OOM killer compares against. Discovery covers cgroup v2 at the mount root, which is the Kubernetes case, then cgroup v2 resolved from /proc/self/cgroup, then cgroup v1 for older Kubernetes and YARN. With no container limit the guard disables itself, so this is a no-op outside containers and off Linux. Add spark.comet.exec.memoryGuard.enabled, defaulting to false, and spark.comet.exec.memoryGuard.threshold, defaulting to 0.9.
Member
Author
|
Closing this. The approach was validated end to end on k8s and the plumbing works: cgroup v2 discovery inside a pod, correct limit, trip propagating as In an A/B on TPC-H SF100 Q9 at a 9Gi pod limit, the guard failed the job (24 trips) while the identical run with the guard off completed in 58.62s, and nothing was OOMKilled in either. Measured Details and the cgroup trace are in #4576. A future attempt should threshold on |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Relates to #4576. Draft, opened for discussion rather than merge, and deliberately overlapping with @comphead's #5666 so the two approaches can be compared.
Rationale for this change
When an executor exceeds its container memory limit the kernel kills the whole JVM, taking every task on it, its cached blocks and its shuffle files. A single failed task is retried by Spark and costs almost nothing by comparison. This adds an optional guard that samples the container's real memory usage and fails the current task when usage approaches the limit.
The design differs from the two previous attempts in one respect, and it is the point of the PR: it reads the kernel's number rather than Comet's own accounting.
The closing review of #4582 set this precondition:
I measured that and posted the result on #4576. On TPC-H SF100 with the default allocator, across ~4500 samples pairing
native_allocatedwith kernel RSS at the same trace anchor:A threshold on the allocator balance cannot mean anything about the quantity the kernel kills on, because the noise in the offset exceeds the whole signal. That is the same conclusion the #4582 review reached analytically ("the tracked balance is layout bytes, not RSS ... that gap is unbounded and always in the dangerous direction"), now with numbers.
Reading the cgroup avoids the problem entirely: it is the number the OOM killer compares against the limit, and it already includes the JVM heap, Comet's native allocations, JVM-side Arrow, Spark's own off-heap and mapped files. In my measurements the JVM heap was the dominant term, so any signal that excludes it is measuring the wrong thing.
What changes are included in this PR?
native/core/src/execution/memory_guard.rs(new). Discovers the container's memory usage file and limit: cgroup v2 at the mount root (the Kubernetes case, where the pod's cgroup is namespaced), then cgroup v2 resolved from/proc/self/cgroupfor non-containerised hosts, then cgroup v1 for older Kubernetes and YARN.check()samples usage, throttled to at most once per 100 ms, and reports a trip at a configurable fraction of the limit.jni_api.rscalls it at the two existing execution checkpoints: the asyncbatch_receiverpath and the ScanExec busy-poll path's 100-poll checkpoint. A trip becomesDataFusionError::ResourcesExhausted, which reaches the JVM asCometNativeException, whichCometExecIteratorlogs with the task id and rethrows, so Spark fails and retries one task.spark.comet.exec.memoryGuard.enabled(defaultfalse) andspark.comet.exec.memoryGuard.threshold(default0.9). Names match feat: experiment withRealUsagePool#5666 so the two are comparable.No allocator wrapper, no
panic_any, no stamped-thread set. That sidesteps the four defects the #4582 review found: the two enforcement layers not actually layering, the stamped set covering tokio's blocking pool so panics escaped asJoinError, theLOCAL_DRIFTleak on thread exit, and the layout-bytes-versus-RSS gap.Limitations, stated up front
How are these changes tested?
Unit tests for the parsing, which is the part that can be tested off Linux: the cgroup v2 path from
/proc/self/cgroupincluding the0::/container form, a v1-only host producing no v2 path,maxtreated as no limit, usage parsing, and out-of-range thresholds disabling the guard.What is not tested, and I would not merge it without this: the guard has never run in a container.
discover()is gated on a runtimecfg!(target_os = "linux")check, so the/sys/fs/cgroupreads typecheck on macOS but never execute there, and I have no Kubernetes environment to hand. What it needs is a pod with a memory limit, a query that exceeds it, and confirmation that the task fails and is retried while the executor survives. If anyone can run that, I would value it more than any amount of further code review.