[ 
https://issues.apache.org/jira/browse/SPARK-61130?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Pradeep Reddy updated SPARK-61130:
----------------------------------
    Description: 
Spark Dynamic Allocation adds and removes executors from the driver without 
changing the {{SparkApplication}} spec, and even with a fixed number of 
executors the spec cannot tell how many are running or whether they have 
started. The {{SparkApplication}} status does not declare its executors today, 
so anything that needs them, such as users, dashboards, autoscalers and batch 
schedulers, has to list and watch the executor pods itself and know which pods 
belong to the current attempt.

This gap blocks Kueue today. Kueue's chosen approach to elastic Spark 
applications is to follow executor counts and states that the operator declares 
in the status (kubernetes-sigs/kueue#17127: 
https://github.com/kubernetes-sigs/kueue/pull/17127). The Kueue community 
maintains a built-in framework integration for the Kubeflow Spark Operator, and 
that solution supports only that operator. Declaring the same information here 
closes the gap for this operator's applications, and gives every other consumer 
the same answer from the {{SparkApplication}} alone.

This proposes four new optional fields in {{SparkApplication}} status, for the 
current attempt:

||Field||Recorded for||Meaning||Lets a consumer||
|{{status.activeExecutors}}|every application|executor pods that are pending 
(including scheduling-gated) or running (including being deleted)|see the 
executors an application holds now, e.g. to size it|
|{{status.readyExecutors}}|every application|executor pods that are running and 
ready, and not being deleted|tell whether the executors have started, e.g. for 
a readiness timeout|
|{{status.maxExecutorId}}|applications with Dynamic Allocation|highest executor 
ID so far; Spark never reuses executor IDs, so it never decreases within an 
attempt|tell scale-ups apart, e.g. how many executors the driver has requested 
in total|
|{{status.executorState}}|applications with Dynamic Allocation, opt-in (off by 
default)|executor pod name -> {{PENDING}}, {{RUNNING}}, {{COMPLETED}}, 
{{FAILED}} or {{UNKNOWN}}|follow each executor, when the counts are not enough|

h3. Executor counts and highest executor ID

* Computed from the executor pods of the current attempt 
({{spark-app-selector}} equal to the attempt's {{spark.app.id}}) in the 
existing pod informer cache: no new watch, LIST or GET. They do not depend on 
{{executorState}}, so they stay exact however many executors an application has 
had.
* Unset until the attempt has an executor pod. The counts drop to 0 once every 
executor is gone, while {{maxExecutorId}} is kept. All of them start over with 
each attempt and when the application is suspended.
* The two counts can be turned off with 
{{spark.kubernetes.operator.reconciler.executorCountsEnabled}} (on by default, 
dynamically overridable), in case their status writes load the API server and 
etcd more than a cluster can take. Turning it off clears the counts recorded 
before; {{maxExecutorId}} and {{executorState}} are not affected.

h3. Executor states (opt-in)

* Off by default, turned on with 
{{spark.kubernetes.operator.reconciler.executorStateEnabled}} (dynamically 
overridable). A scheduler that reads only the counts does not need them, and 
every entry adds to each status write.
* An executor's state follows its pod phase ({{Pending}}, {{Running}}, 
{{Succeeded}}, {{Failed}}; {{UNKNOWN}} otherwise), and the entry is kept after 
its pod is gone. The informer's delete event records the pod's last phase, so 
an executor that completed or failed as Dynamic Allocation removed it is 
recorded as {{COMPLETED}} or {{FAILED}} rather than {{UNKNOWN}}. Otherwise an 
executor whose pod is gone becomes {{UNKNOWN}} while the driver runs, then 
{{COMPLETED}} or {{FAILED}} with the application.
* Bounded by {{spark.kubernetes.operator.reconciler.maxTrackedExecutorPerApp}} 
(default 1000, dynamically overridable) to keep the status within the size 
limit of a Kubernetes object. Beyond it, executors that are gone are evicted 
first, oldest first, so a long-running application keeps recording its new 
executors.

h3. API server load

* The status is written only when a recorded field changes, in one patch per 
reconciliation, so the writes are bounded by the existing per-application 
reconcile rate limiter ({{maxLoopForPeriod}} per {{refreshPeriodSeconds}}). 
Several executor changes within a rate-limit window are coalesced into one 
write.
* All fields are new and optional, v1 CRD only, so existing users are 
unaffected.


  was:
Spark Dynamic Allocation adds and removes executors from the driver without 
changing the {{SparkApplication}} spec, so a scheduler cannot tell from the 
spec how many executors an application holds or whether they have started. This 
enhancement will allow External Framework Integrations of Apache Spark Operator 
for Kueue to support Spark Dynamic Allocation with Kueue's 
ElasticJobsViaWorkloadSlices using the CR declared executor counts. The Kueue 
community maintains a built-in framework integration for the Kubeflow Spark 
Operator, and implemented Spark Dynamic Allocation support  part of 
(kubernetes-sigs/kueue#17127: 
https://github.com/kubernetes-sigs/kueue/pull/17127) only supports that 
operator. 

This proposes four new optional fields in {{SparkApplication}} status, for the 
current attempt:

||Field||Recorded for||Meaning||Used by a scheduler to||
|{{status.activeExecutors}}|every application|executor pods that are pending 
(including scheduling-gated) or running (including being deleted)|size the 
application: the executors it holds now|
|{{status.readyExecutors}}|every application|executor pods that are running and 
ready, and not being deleted|tell whether the executors have started, e.g. for 
Kueue's {{waitForPodsReady}}|
|{{status.maxExecutorId}}|applications with Dynamic Allocation|highest executor 
ID so far; Spark never reuses executor IDs, so it never decreases within an 
attempt|tell scale-ups apart, e.g. to give each Kueue Workload slice a unique 
name|
|{{status.executorState}}|applications with Dynamic Allocation, opt-in (off by 
default)|executor pod name -> {{PENDING}}, {{RUNNING}}, {{COMPLETED}}, 
{{FAILED}} or {{UNKNOWN}}|follow each executor, when the counts are not enough|

h3. Executor counts and highest executor ID

* Computed from the executor pods of the current attempt 
({{spark-app-selector}} equal to the attempt's {{spark.app.id}}) in the 
existing pod informer cache: no new watch, LIST or GET. They do not depend on 
{{executorState}}, so they stay exact however many executors an application has 
had.
* Unset until the attempt has an executor pod. The counts drop to 0 once every 
executor is gone, while {{maxExecutorId}} is kept. All of them start over with 
each attempt and when the application is suspended.

h3. Executor states (opt-in)

* Off by default, turned on with 
{{spark.kubernetes.operator.reconciler.executorStateEnabled}} (dynamically 
overridable). A scheduler that reads only the counts does not need them, and 
every entry adds to each status write.
* An executor's state follows its pod phase ({{Pending}}, {{Running}}, 
{{Succeeded}}, {{Failed}}; {{UNKNOWN}} otherwise), and the entry is kept after 
its pod is gone. The informer's delete event records the pod's last phase, so 
an executor that completed or failed as Dynamic Allocation removed it is 
recorded as {{COMPLETED}} or {{FAILED}} rather than {{UNKNOWN}}. Otherwise an 
executor whose pod is gone becomes {{UNKNOWN}} while the driver runs, then 
{{COMPLETED}} or {{FAILED}} with the application.
* Bounded by {{spark.kubernetes.operator.reconciler.maxTrackedExecutorPerApp}} 
(default 1000, dynamically overridable) to keep the status within the size 
limit of a Kubernetes object. Beyond it, executors that are gone are evicted 
first, oldest first, so a long-running application keeps recording its new 
executors.

h3. API server load

* The status is written only when a recorded field changes, in one patch per 
reconciliation, so the writes are bounded by the existing per-application 
reconcile rate limiter ({{maxLoopForPeriod}} per {{refreshPeriodSeconds}}). 
Several executor changes within a rate-limit window are coalesced into one 
write.
* All fields are new and optional, v1 CRD only, so existing users are 
unaffected.



>  Add activeExecutors, readyExecutors, maxExecutorId and ExecutorStates to 
> SparkApplication status
> -------------------------------------------------------------------------------------------------
>
>                 Key: SPARK-61130
>                 URL: https://issues.apache.org/jira/browse/SPARK-61130
>             Project: Spark
>          Issue Type: Improvement
>          Components: Kubernetes
>    Affects Versions: kubernetes-operator-1.1.0
>            Reporter: Pradeep Reddy
>            Priority: Major
>             Fix For: kubernetes-operator-1.1.0
>
>
> Spark Dynamic Allocation adds and removes executors from the driver without 
> changing the {{SparkApplication}} spec, and even with a fixed number of 
> executors the spec cannot tell how many are running or whether they have 
> started. The {{SparkApplication}} status does not declare its executors 
> today, so anything that needs them, such as users, dashboards, autoscalers 
> and batch schedulers, has to list and watch the executor pods itself and know 
> which pods belong to the current attempt.
> This gap blocks Kueue today. Kueue's chosen approach to elastic Spark 
> applications is to follow executor counts and states that the operator 
> declares in the status (kubernetes-sigs/kueue#17127: 
> https://github.com/kubernetes-sigs/kueue/pull/17127). The Kueue community 
> maintains a built-in framework integration for the Kubeflow Spark Operator, 
> and that solution supports only that operator. Declaring the same information 
> here closes the gap for this operator's applications, and gives every other 
> consumer the same answer from the {{SparkApplication}} alone.
> This proposes four new optional fields in {{SparkApplication}} status, for 
> the current attempt:
> ||Field||Recorded for||Meaning||Lets a consumer||
> |{{status.activeExecutors}}|every application|executor pods that are pending 
> (including scheduling-gated) or running (including being deleted)|see the 
> executors an application holds now, e.g. to size it|
> |{{status.readyExecutors}}|every application|executor pods that are running 
> and ready, and not being deleted|tell whether the executors have started, 
> e.g. for a readiness timeout|
> |{{status.maxExecutorId}}|applications with Dynamic Allocation|highest 
> executor ID so far; Spark never reuses executor IDs, so it never decreases 
> within an attempt|tell scale-ups apart, e.g. how many executors the driver 
> has requested in total|
> |{{status.executorState}}|applications with Dynamic Allocation, opt-in (off 
> by default)|executor pod name -> {{PENDING}}, {{RUNNING}}, {{COMPLETED}}, 
> {{FAILED}} or {{UNKNOWN}}|follow each executor, when the counts are not 
> enough|
> h3. Executor counts and highest executor ID
> * Computed from the executor pods of the current attempt 
> ({{spark-app-selector}} equal to the attempt's {{spark.app.id}}) in the 
> existing pod informer cache: no new watch, LIST or GET. They do not depend on 
> {{executorState}}, so they stay exact however many executors an application 
> has had.
> * Unset until the attempt has an executor pod. The counts drop to 0 once 
> every executor is gone, while {{maxExecutorId}} is kept. All of them start 
> over with each attempt and when the application is suspended.
> * The two counts can be turned off with 
> {{spark.kubernetes.operator.reconciler.executorCountsEnabled}} (on by 
> default, dynamically overridable), in case their status writes load the API 
> server and etcd more than a cluster can take. Turning it off clears the 
> counts recorded before; {{maxExecutorId}} and {{executorState}} are not 
> affected.
> h3. Executor states (opt-in)
> * Off by default, turned on with 
> {{spark.kubernetes.operator.reconciler.executorStateEnabled}} (dynamically 
> overridable). A scheduler that reads only the counts does not need them, and 
> every entry adds to each status write.
> * An executor's state follows its pod phase ({{Pending}}, {{Running}}, 
> {{Succeeded}}, {{Failed}}; {{UNKNOWN}} otherwise), and the entry is kept 
> after its pod is gone. The informer's delete event records the pod's last 
> phase, so an executor that completed or failed as Dynamic Allocation removed 
> it is recorded as {{COMPLETED}} or {{FAILED}} rather than {{UNKNOWN}}. 
> Otherwise an executor whose pod is gone becomes {{UNKNOWN}} while the driver 
> runs, then {{COMPLETED}} or {{FAILED}} with the application.
> * Bounded by 
> {{spark.kubernetes.operator.reconciler.maxTrackedExecutorPerApp}} (default 
> 1000, dynamically overridable) to keep the status within the size limit of a 
> Kubernetes object. Beyond it, executors that are gone are evicted first, 
> oldest first, so a long-running application keeps recording its new executors.
> h3. API server load
> * The status is written only when a recorded field changes, in one patch per 
> reconciliation, so the writes are bounded by the existing per-application 
> reconcile rate limiter ({{maxLoopForPeriod}} per {{refreshPeriodSeconds}}). 
> Several executor changes within a rate-limit window are coalesced into one 
> write.
> * All fields are new and optional, v1 CRD only, so existing users are 
> unaffected.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to