Repository navigation
Conversation
glibc keeps much of the memory that Comet's native operators free in its per-thread arenas instead of returning it to the operating system. Spills free a lot of memory, and one stage's native plans run on different threads than the next stage's, so an executor's resident memory can stay near one stage's peak while the next stage allocates on top of it. The container can then run out of memory while Comet's memory pools stay within their limits. Add spark.comet.memory.trimNativeHeapWhenIdle.enabled, off by default. When it is set, releasing the last native plan running on an executor calls glibc's malloc_trim(0) and logs how long that took and the anonymous resident memory before and after. It only acts on Linux builds that use glibc and the default system allocator, and it is read from the executor's configuration.
|
Thanks for digging into this. The analysis of what glibc retains between stages matches what #7108 measured, and gating the trim on the plan registry so it only runs when no operator can be holding an arena lock is a nice idea. Before we add a config for this, though, I want to step back and ask whether this is the right layer to fix it at. The JDK already does this. The bigger question is the one #7108 asks: should the release build use a different allocator? People in #1648 already run Comet with jemalloc preloaded because it returns memory more predictably, and the So a few asks:
|
Which issue does this PR close?
None. Related to #7108, which stays open: it asks whether release builds should default to a different allocator, and this PR adds an opt-in mitigation for the default glibc build instead.
Rationale for this change
An executor can be OOMKilled by its container while Comet's memory pools and allocation counter stay well within their limits. Much of the difference is native memory that has already been freed: glibc keeps a large part of what Comet's operators free in its per-thread arenas instead of returning it to the operating system. #7108 measured this on Linux. Live native memory dropped to about 2 MiB between queries while RSS stayed near 12 GiB, and one
malloc_trimreleased 5 to 6 GiB.It gets worse between stages:
mmap.BatchProducer::spawn), and a plan that reads shuffle runs on the Spark task thread. The next stage does not reuse what the previous one left in the other threads' arenas, so its allocations add to the retained memory.Spark's own off-heap memory does not build up this way, because its pages are 64 MiB and glibc maps and unmaps each of them directly.
I saw this pattern on a wide group-by (87 sort keys, sort-based aggregation) that Spark finishes and Comet does not: every result-stage wave was OOMKilled. A local reproduction at the same per-task size shows the mechanism. Comet's result stage alone needs about what Spark's does (9.1 vs 8.9 GiB), but right after the map stage it reaches 16.2 GiB, because about 7 GiB that the map stage freed is still resident when it starts.
What changes are included in this PR?
spark.comet.memory.trimNativeHeapWhenIdle.enabled,falseby default. When it is set, releasing the last native plan running on an executor calls glibc'smalloc_trim(0)and logs at INFO level how long that took and the process's anonymous resident memory before and after. It only runs when no plan is left, which is typically between stages, so it does not take the arena locks of running operators.jemallocormimallocfeature, it does nothing.CometExecIterator.serializeCometSQLConfsreads it from the executor'sSparkConfrather than the session's, so it applies however the job is launched (see Map stages of RDD-level jobs run Comet with default settings instead of the session's #7301). An invalid value logs a warning once and leaves trimming off.native_heap_trim.rsmodule and a hook at the end ofrelease_planinjni_api.rs, with a smalllive_plan_count()helper. The memory tuning guide gets a section on the setting.How are these changes tested?
native_heap_trim::testschecks that it trims only when the build supports it and no plan is left. ACometExecIteratorLifecycleSuitetest covers parsing the setting.CometExecSuite"SQLConf serde resolves the configs that native code parses" checks that the value always crosses JNI resolved, from the executor's configuration. These have not run locally yet.malloc_trimdoes not exist on macOS, where I work, so I type-checked and linted the Linux path foraarch64-unknown-linux-gnu(clippy with-D warnings). It has not run on Linux yet. A Linux run with the setting on is the next step, and the INFO line will show its effect.malloc_trim(a test-only change, not in this PR), and with jemalloc keeping freed memory as glibc does. With the setting off, 4 of 4 runs exceeded the memory limit in the result stage. With it on, 4 of 4 passed. The hook ran twice per job, once after the map stage's last task and once at the end, and each purge took 0.3 to 0.7 s.