[
https://issues.apache.org/jira/browse/SPARK-58935?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
ASF GitHub Bot updated SPARK-58935:
-----------------------------------
Labels: pull-request-available (was: )
> ExecutorAllocationManager silently stalls at zero needed executors when a
> stage-submitted event is dropped from the executorManagement queue
> --------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: SPARK-58935
> URL: https://issues.apache.org/jira/browse/SPARK-58935
> Project: Spark
> Issue Type: Bug
> Components: Scheduler, Spark Core
> Affects Versions: 4.0.1
> Environment: Kubernetes deployment, Spark Connect (long-running
> driver), Spark 4.0.1
> Reporter: dongjunhwang
> Priority: Major
> Labels: pull-request-available
>
> h3. Problem
> {{LiveListenerBus}} delivers the same logical event to several independent,
> separately-capacity-limited queues. {{ExecutorAllocationManager}}'s listener
> is registered on the {{executorManagement}} queue
> ({{LiveListenerBus.addToManagementQueue}},
> [LiveListenerBus.scala#L70|https://github.com/apache/spark/blob/5abbd34c837571b059de659898bbf699f6c6d3fb/core/src/main/scala/org/apache/spark/scheduler/LiveListenerBus.scala#L70]),
> while the listener that drives the Spark UI is registered on a completely
> separate {{appStatus}} queue ({{addToStatusQueue}},
> [LiveListenerBus.scala#L75|https://github.com/apache/spark/blob/5abbd34c837571b059de659898bbf699f6c6d3fb/core/src/main/scala/org/apache/spark/scheduler/LiveListenerBus.scala#L75]).
> Each queue independently drops events once its bounded capacity is exceeded
> ({{AsyncEventQueue.post}},
> [AsyncEventQueue.scala#L168|https://github.com/apache/spark/blob/5abbd34c837571b059de659898bbf699f6c6d3fb/core/src/main/scala/org/apache/spark/scheduler/AsyncEventQueue.scala#L168]).
> A dropped event is never redelivered or resynced.
> {{ExecutorAllocationManager}}'s "executors needed" calculation is driven
> entirely by {{stageAttemptToNumTasks}}, which is populated *only* in
> {{onStageSubmitted}}
> ([ExecutorAllocationManager.scala#L687|https://github.com/apache/spark/blob/5abbd34c837571b059de659898bbf699f6c6d3fb/core/src/main/scala/org/apache/spark/ExecutorAllocationManager.scala#L687])
> and consumed by {{maxNumExecutorsNeededPerResourceProfile}}
> ([ExecutorAllocationManager.scala#L297|https://github.com/apache/spark/blob/5abbd34c837571b059de659898bbf699f6c6d3fb/core/src/main/scala/org/apache/spark/ExecutorAllocationManager.scala#L297]).
> If the {{SparkListenerStageSubmitted}} event for a given stage is dropped
> specifically from the {{executorManagement}} queue:
> * The {{appStatus}} queue is unaffected, so the Spark UI/REST API continues
> to show the stage as {{RUNNING}} normally.
> * {{ExecutorAllocationManager}} never learns about that stage's tasks.
> {{maxNumExecutorsNeededPerResourceProfile}} permanently omits them from its
> calculation for the lifetime of the stage -- there is no periodic resync.
> * When the stage eventually completes, {{onStageCompleted}} removes a
> {{StageAttempt}} key that was never inserted; this is a silent no-op on the
> underlying {{mutable.HashMap}}, so there is no error, warning, or other
> signal that anything went wrong.
> The net effect: dynamic allocation can stop requesting new executors for an
> application that, from the UI/REST API and logs, looks completely healthy and
> busy.
> h3. How this was found
> Observed on a long-running (2+ day uptime) Spark Connect driver (Spark 4.0.1)
> in a Kubernetes deployment. {{ExecutorAllocationManager}}'s JMX metrics
> showed:
> {code}
> numberMaxNeededExecutors = 0 (with 2 pending tasks on an active stage)
> numberTargetExecutors = 0
> {code}
> while the Spark UI's REST API ({{/api/v1/applications/<id>/jobs}})
> simultaneously reported the job as {{RUNNING}}. A completely unrelated,
> concurrently-running session on the same driver process was independently
> affected in the same way, ruling out any per-job cause. The only workaround
> was restarting the driver process. Thread dumps showed the dynamic-allocation
> scheduling thread alive and running normally (not deadlocked) -- only the
> *calculation result* was wrong.
> h3. Reproduction
> Deterministically reproduced by constructing a {{LiveListenerBus}} with
> {{spark.scheduler.listenerbus.eventqueue.executorManagement.capacity=1}},
> occupying the queue's single dispatch thread with a blocking listener, and
> posting a {{SparkListenerStageSubmitted}} event for a 2-task stage once the
> queue is full. The event is dropped (confirmed via
> {{queue.executorManagement.numDroppedEvents}}), and
> {{ExecutorAllocationManager.maxNumExecutorsNeededPerResourceProfile}} returns
> {{0}} for that stage, even though 2 tasks are pending.
> In a real cluster, this queue can fill up under a burst of many other events
> sharing the same queue ({{HeartbeatReceiver}} is also registered on
> {{executorManagement}},
> [LiveListenerBus.scala#L70|https://github.com/apache/spark/blob/5abbd34c837571b059de659898bbf699f6c6d3fb/core/src/main/scala/org/apache/spark/scheduler/LiveListenerBus.scala#L70]
> area), or if a listener on that queue is briefly slow.
> h3. Expected behavior and proposed fix (this PR)
> A full self-healing fix (e.g., periodically reconciling
> {{ExecutorAllocationManager}}'s bookkeeping against the ground-truth
> stage/task state already tracked by {{AppStatusStore}}) is a larger, more
> invasive change that needs broader design discussion.
> As a first, narrowly-scoped step, this PR makes the problem *observable*
> instead of silent: it exposes the executor-management queue's dropped-event
> count as a new {{ExecutorAllocationManager}} metric
> ({{numDroppedExecutorManagementEvents}}), so operators can alert on it and
> correlate "dynamic allocation stopped requesting executors" with "the
> executorManagement queue actually dropped an event" -- which is not currently
> possible (the only existing signal is a generic, easy-to-miss {{WARN}} log
> line shared by every queue, logged at most once per 60 seconds, with no
> indication of which downstream component is affected).
> This PR does not change any existing behavior; it only adds a new gauge
> metric, matching the existing pattern used by
> {{ExecutorAllocationManagerSource}}'s other gauges (e.g.
> {{numberMaxNeededExecutors}}).
> h3. Related work
> * SPARK-32597 (still open, unassigned) previously identified that event drops
> in the async listener bus can cause "inconsistent state for the Spark
> Application (sometimes application is hung state)" in general, and proposed a
> more invasive {{VariableLinkedBlockingQueue}} approach ([PR
> #29413|https://github.com/apache/spark/pull/29413], closed unmerged in 2020).
> This ticket documents a specific, reproducible manifestation of that general
> class of problem, with a much narrower first fix.
> * SPARK-26927 (Fixed) previously fixed a different specific mechanism --
> event *ordering* (not dropping) across queues -- that similarly corrupted
> {{ExecutorAllocationManager}}'s bookkeeping and caused a production Spark
> Thrift Server hang.
> * SPARK-58446 (open, unassigned, [PR
> #57651|https://github.com/apache/spark/pull/57651]) reports a
> similarly-surfacing symptom -- dynamic allocation stuck at zero needed/target
> executors -- but from a distinct mechanism confirmed at the code level: a
> late {{TaskStart}}/{{SpeculativeTaskSubmitted}} event arriving *after*
> {{onStageCompleted}} has already removed the stage's bookkeeping re-creates
> stale state and drives the pending-task count negative. That fix guards
> {{onTaskStart}}/{{onSpeculativeTaskSubmitted}} with
> {{stageAttemptToNumTasks.contains(stageAttempt)}}; it does not touch
> {{onStageSubmitted}} and would not prevent or fix the issue described here,
> where {{stageAttemptToNumTasks}} is never populated for the affected stage
> attempt in the first place because the {{SparkListenerStageSubmitted}} event
> never reaches the listener.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]