Skip to content

[SPARK-58203][K8S] Support configuring spark-managed UI service to work with spark.ui.port=0 - #57350

Open
zhengchenyu wants to merge 11 commits into
apache:masterfrom
zhengchenyu:SPARK-58203
Open

[SPARK-58203][K8S] Support configuring spark-managed UI service to work with spark.ui.port=0#57350
zhengchenyu wants to merge 11 commits into
apache:masterfrom
zhengchenyu:SPARK-58203

Conversation

@zhengchenyu

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Add config spark.kubernetes.driver.ui.service.enabled (default false). When enabled:

  • A new DriverUIServiceFeatureStep creates a dedicated ClusterIP Service exposing the driver's Web UI port.
  • After SparkContext initializes and the Web UI binds,K8sDriverUIServicePatcher (invoked from KubernetesClusterSchedulerBackend.start()) patches the Service's targetPort to the actual bound port.

When spark.ui.port=0, the Service is initially built with the default UI port as placeholder to satisfy Kubernetes port validation; the runtime patch then rewrites targetPort.

Two other configs: spark.kubernetes.driver.ui.service.type (ClusterIP / NodePort / LoadBalancer, default ClusterIP) and spark.kubernetes.driver.ui.service.name (optional explicit name).

Why are the changes needed?

In hostNetwork mode, spark.ui.port=0 is needed to avoid UI port collisions between drivers on the same host. Today the driver UI Service is created by spark-operator, not by Spark itself, and spark-operator has no way to know the actual UI port the driver ends up binding to.

Letting Spark create and manage the UI Service resolves this because Spark knows the actual bound port and patches the Service accordingly.

Does this PR introduce any user-facing change?

Yes. Three new opt-in configs, default off.

How was this patch tested?

  • New DriverUIServiceFeatureStepSuite:
  • Test on kubernetes cluster.

Was this patch authored or co-authored using generative AI tooling?

…rk with spark.ui.port=0

Signed-off-by: zhengchenyu <zhengchenyu16@163.com>

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds an opt-in, Spark-managed Kubernetes Service for the driver Web UI and patches its targetPort after Jetty binds. The direction addresses the host-network/random-port problem, but the current head does not yet make the stated spark.ui.port=0 path valid and misses one supported backend path.

Prior state and problem

The existing headless driver Service is built from the configured spark.ui.port. In host-network mode, multiple drivers can collide on a fixed UI port, while an external operator cannot know the random port selected by spark.ui.port=0.

Design approach

The patch adds enable/type/name configurations, creates a dedicated UI Service with a stable Service port and placeholder target port, passes the Service name to the driver, and patches targetPort from SparkUI.boundPort during Kubernetes scheduler-backend startup.

Correctness / compatibility analysis

Default-off behavior remains backward compatible, and keeping a stable Service port while patching only targetPort is appropriate for ClusterIP, NodePort, and LoadBalancer. The enabled path is currently blocked because the existing driver Pod and headless Service still contain port zero. In addition, driver-only local[*] applications never call the patcher, and the documented RBAC grant omits the GET verb used by the implementation.

Key design decisions

  • Keep the externally visible Service port stable while patching only targetPort.
  • Ensure no generated Kubernetes manifest contains a numeric port of zero.
  • Run patching from a driver lifecycle shared by Kubernetes and local driver-only backends, or explicitly scope the feature away from driver-only mode.
  • Treat get and patch as required Service permissions if retaining the read-before-write implementation.

Implementation sketch

  1. When the dedicated UI Service is enabled and spark.ui.port=0, omit the UI container-port declaration and omit or substitute the UI entry on the existing headless Service.
  2. Move the patch hook to a shared driver initialization point after UI binding, with Kubernetes-client access, or reject this configuration for driver-only mode.
  3. Update the RBAC/configuration documentation and add full-builder plus patcher/backend tests.

Behavioral changes worth calling out

Enabling the feature creates an additional Kubernetes Service and currently makes patch failure fatal during SparkContext initialization. LoadBalancer can expose the UI externally. The behavior for supported spark.kubernetes.driver.master=local[*] applications therefore needs to be defined explicitly.

Suggested improvements

Add a full KubernetesDriverBuilder test for spark.ui.port=0, focused patcher tests for matching/missing/updated targets, integration coverage for driver-only mode, and the three new settings plus their RBAC requirements to docs/running-on-kubernetes.md.

* [[org.apache.spark.scheduler.cluster.k8s.K8sDriverUIServicePatcher]] updates the Service's
* `targetPort` to the real bound port.
*/
private lazy val servicePort: Int = if (configuredUIPort == 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Handle port zero in the existing driver resources too

This placeholder only fixes the new dedicated Service. With spark.ui.port=0, BasicDriverFeatureStep still emits containerPort=0, and DriverServiceFeatureStep still emits port=0 / targetPort=0 for the mandatory headless Service. Kubernetes requires these numeric ports to be in 1..65535, so the driver Pod/resources are rejected before this runtime patch can run. Please omit or substitute both existing UI port declarations and cover the complete KubernetesDriverBuilder output in a test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunchao In fact, Because I think the internal service do not need to expose ports. So I use spark.kubernetes.executor.useDriverPodIP=true and add org.apache.spark.deploy.k8s.features.DriverServiceFeatureStep to spark.kubernetes.driver.pod.excludedFeatureSteps.

If the driver service's port is configured to 0, then this is indeed necessary. However, it's unclear whether it's appropriate to do this in this PR, since this is a PR for adding a UI service. I'll add it to this PR if you think it's suitable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This remains reproducible on 44b952a19a85a35e9ecad237dde9cfb604183e9b. BasicDriverFeatureStep.scala:100,117-120 still copies spark.ui.port=0 into the driver's containerPort, and DriverServiceFeatureStep.scala:52,89-93 independently emits Service port=0 and targetPort=0. The proposed spark.kubernetes.executor.useDriverPodIP=true plus excluding DriverServiceFeatureStep only removes the second invalid resource; BasicDriverFeatureStep still makes the Pod invalid before the runtime patcher can execute. Because this PR explicitly promises spark.ui.port=0, both existing declarations need to be omitted/replaced, with a full KubernetesDriverBuilder regression test.

if (!conf.get(KUBERNETES_EXECUTOR_DISABLE_CONFIGMAP)) {
setUpExecutorConfigMap(podAllocator.driverPod)
}
maybePatchDriverUIServiceTargetPort()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Patch the Service in driver-only mode too

spark.kubernetes.driver.master=local[*] is a supported driver-only mode, but KubernetesClusterManager returns LocalSchedulerBackend for it, so this method never runs even though DriverUIServiceFeatureStep still creates the Service. After the zero-port manifest issue is fixed, the UI binds randomly while targetPort remains at the placeholder; a nonzero bind collision produces the same stale target. Please move this hook to a driver lifecycle shared by both backends, or explicitly reject/disable the feature in driver-only mode.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local mode generally doesn't require exposing the UI port, so there's no need to patch the port; simply keep spark.kubernetes.driver.ui.service.enabled set to false.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This still fails on 44b952a19a85a35e9ecad237dde9cfb604183e9b, and the latest endpointless-Service change makes it affect even a normal fixed port with no bind collision. For spark.kubernetes.driver.master=local[*], KubernetesClusterManager.scala:56-72 returns LocalSchedulerBackend, but only KubernetesClusterSchedulerBackend.start() invokes the patcher. DriverUIServiceFeatureStep now creates every enabled UI Service without a selector, so spark.ui.port=4040 plus spark.kubernetes.driver.ui.service.enabled=true leaves the Service permanently endpointless. Setting the feature to false avoids the configuration rather than handling the explicitly enabled one; please patch from a shared driver lifecycle or reject/disable this incompatible combination.

"Web UI (separate from the headless driver service). When enabled, after the driver " +
"Web UI starts, Spark will patch the Service's targetPort to match the actual bound " +
"UI port, which allows using `spark.ui.port=0` (random port). Requires the driver's " +
"ServiceAccount to have `patch services` permission.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Include GET in the required RBAC permissions

The patcher calls get() before patch(), and Kubernetes RBAC verbs are independent. A least-privilege role granting only the documented patch verb receives a 403 on the GET; the exception is rethrown from backend startup and aborts SparkContext initialization. Please document both get and patch, or issue a targeted patch that does not require the preliminary read.

@zhengchenyu zhengchenyu Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the document.

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Document these public Kubernetes settings in the configuration table

Config.scala:104 adds three public user-facing settings, but none is listed in the Kubernetes configuration table in docs/running-on-kubernetes.md. Users therefore cannot discover the enable flag, service type/name controls and defaults, supported service types, or the required get + patch RBAC permissions from the primary Kubernetes documentation. Please add table entries for all three settings.

@zhengchenyu

Copy link
Copy Markdown
Contributor Author

[P2] Document these public Kubernetes settings in the configuration table

Config.scala:104 adds three public user-facing settings, but none is listed in the Kubernetes configuration table in docs/running-on-kubernetes.md. Users therefore cannot discover the enable flag, service type/name controls and defaults, supported service types, or the required get + patch RBAC permissions from the primary Kubernetes documentation. Please add table entries for all three settings.

@sunchao I have updated the document!

@zhengchenyu
zhengchenyu requested a review from sunchao July 23, 2026 01:16

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental findings on exact head e9497be15df10c121a9f1d8db5a09ea9b56f77ec.

.addToLabels(kubernetesConf.serviceLabels.asJava)
.endMetadata()
.withNewSpec()
.withType(serviceType)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the configured IP family on the UI Service

This Service does not set ipFamilyPolicy or ipFamilies, even though the existing driver Service honors spark.kubernetes.driver.service.ipFamilyPolicy / .ipFamilies. On a dual-stack cluster, Kubernetes defaults an unspecified Service to SingleStack on the first Service CIDR, so applications configured for IPv6 or RequireDualStack get a compatible RPC Service but an incompatible UI Service. Please apply the driver Service family settings here (or add equivalent UI-service settings) and cover the dual-stack case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunchao I have updated the code. UI service will reuse Driver Service's ip family to keep the same IP family.

Comment thread docs/running-on-kubernetes.md Outdated
<td><code>false</code></td>
<td>
If true, Spark will create a dedicated Kubernetes Service for the Spark driver Web UI.
When enabled, after the driver Web UI starts, Spark will patch the Service's

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Remove the trailing whitespace from this line

This line ends with a trailing space, and git diff --check c9c45a6..e9497be exits 2 on it. Please remove the whitespace so the patch passes the standard diff check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I have removed the space.

@zhengchenyu
zhengchenyu requested a review from sunchao July 27, 2026 06:14

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental findings on exact head 7cd1b9647603ac655b7e925647f8bcaa2ee011cd.

"Web UI starts, Spark will patch the Service's targetPort to match the actual bound " +
"UI port, which allows using `spark.ui.port=0` (random port). Requires the driver's " +
"ServiceAccount to have `get` and `patch` verbs on `services`.")
.version("4.3.0")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Declare binding policies for all three new configs

These three ConfigBuilder entries do not call .withBindingPolicy(...), so each registers with bindingPolicy=None. SparkConfigBindingPolicySuite requires every new config to declare a policy and treats the old exceptions list as frozen. These are deployment-only settings, so please add .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE) to the enabled, type, and name entries. The current audit suite stays green only because this lazily loaded Kubernetes Config object is not initialized in that test JVM.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I have fixed it.

Comment on lines +80 to +83
client.services()
.inNamespace(namespace)
.withName(serviceName)
.patch(updated)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retry resourceVersion conflicts while patching the UI Service

updated retains the first GET's metadata.resourceVersion. In Fabric8 7.8, patch(T) uses JSON Patch and performs a second GET; its diff logic clears only the second-GET base's resourceVersion, specifically causing the first GET's value to be sent in the patch. If a controller updates the Service between those GETs—for example while publishing LoadBalancer status—the API returns 409 for the stale version. This path has no conflict retry, and the catch below rethrows, aborting backend startup. Please retry 409s boundedly with a fresh GET/rebuild, or send a narrow targetPort patch that does not carry the stale full-object version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! Instead of using get, we used patch directly to solve this problem, and the documentation was also updated.

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One additional issue found on the latest head; the existing unresolved findings remain applicable.

.withType(serviceType)
.withIpFamilyPolicy(ipFamilyPolicy)
.withIpFamilies(ipFamilies)
.withSelector(kubernetesConf.labels.asJava)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep the placeholder Service endpointless until the UI port is known

This installs the real driver selector while targetPort is still the 4040 placeholder. In the hostNetwork setup motivating this change, the selected Pod endpoint is the node IP; if another driver already owns 4040, this Service routes to that other driver's UI until KubernetesClusterSchedulerBackend.start() patches it. With NodePort or LoadBalancer, this can expose the wrong application's UI, and the window can last until user code creates SparkContext. The existing zero-port manifest blocker currently masks this path, but it becomes reachable once that blocker is fixed by omitting the zero-valued port declarations. Please initially create the Service without a matching selector, then patch the selector and actual targetPort together after boundPort is known.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A selector has already been added during patching, and patching only occurs when spark.ui port=0.

Signed-off-by: zhengchenyu <zhengchenyu16@163.com>

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental findings on exact head cfdb584a865a056ea5065343996ffd726d77f391.

override def getAdditionalPodSystemProperties(): Map[String, String] = {
// These properties exist solely to drive the runtime patch, which only happens for a random
// port. A fixed-port Service is already complete, so nothing needs to reach the driver.
if (enabled && configuredUIPort == 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reconcile fixed UI ports after bind collisions too

A nonzero spark.ui.port is not guaranteed to be the actual bound port: JettyUtils.startJettyServer delegates to Utils.startServiceOnPort, which retries 4040 at 4041 and higher after a collision. With two hostNetwork drivers requesting 4040, the second driver can bind 4041, but this condition omits all runtime patch metadata and the new Service permanently selects nodeIP:4040—the first application's UI. NodePort/LoadBalancer therefore exposes the wrong application's UI, while ordinary pod networking leaves the second UI unreachable. Please emit reconciliation metadata for fixed ports as well and install the selector only after SparkUI.boundPort is known.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunchao
This issue stems from my previous commit. In that commit, I only patched the UI service for ports configured as 0. I've now changed it to patch the UI service for all configured task ports.

The problem arises because the bound port might change when we configure hostNetwork or set it to 0. Therefore, I personally suggest setting a switch to enable hostNetwork, similar to FLINK-24947. If this switch is enabled, the relevant port will be configured as 0, and the patch UI/driver service will be started. If you agree, I think we should create a new issue to address this problem.

extends KubernetesFeatureConfigStep with Logging {
import DriverUIServiceFeatureStep._

private val enabled = kubernetesConf.get(KUBERNETES_DRIVER_UI_SERVICE_ENABLED)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not expose a UI Service when the Spark UI is disabled

This only checks spark.kubernetes.driver.ui.service.enabled; it never checks spark.ui.enabled. With spark.ui.enabled=false, spark.ui.port=4040, this feature enabled, and hostNetwork=true, Spark still creates an immediately selectable Service targeting the node's port 4040 even though SparkContext never starts a UI. If another application on that node owns 4040, this application's Service routes to that unrelated UI, and NodePort/LoadBalancer can publish it externally. The runtime cannot repair this because sc.ui is None. Please suppress the Service or reject this configuration whenever the Spark UI is disabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I have fixed it.

configuredUIPort
}

private lazy val serviceName: String = kubernetesConf.get(KUBERNETES_DRIVER_UI_SERVICE_NAME)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reject custom Service names already owned by another application

A static spark.kubernetes.driver.ui.service.name can be reused by two live Spark applications, but this override is accepted without checking whether the Service already exists or who owns it. During submission, KubernetesClientApplication calls addOwnerReference(createdDriverPod, ...) and then .forceConflicts().serverSideApply(); addOwnerReference replaces the owner references with the new driver Pod. Consequently, the second submission can forcibly transfer the existing UI Service's selector, application labels, and controller ownership to itself, disrupting the first application and causing the shared Service to be garbage-collected with the wrong Pod. Please use create-only semantics or verify existing ownership before applying, reject collisions with this application's mandatory driver Service, and add cross-application collision coverage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically, all Kubernetes resources have this issue; it wasn't introduced by this PR. If configured, users need to ensure no duplicates. However, without configuration, the default random value makes duplicates almost impossible.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This collision surface is introduced by the new arbitrary spark.kubernetes.driver.ui.service.name override. The existing mandatory driver Service derives its name from the application-specific randomized resourceNamePrefix; existing driver Pods use create-only semantics. By contrast, two otherwise distinct applications can now both set shared-ui: KubernetesUtils.addOwnerReference replaces the controller owner UID, and KubernetesClientApplication.scala:175-177 uses forceConflicts().serverSideApply() on that same Service; the second application's runtime patch then replaces the selector. The first application silently loses its Service, and the Service can be garbage-collected with the wrong Pod. Choosing this application's own mandatory driver-Service name also makes both feature steps emit the same Service identity. Please reject name collisions or verify existing ownership before applying; unique defaults do not protect explicitly configured names.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will change it to be the same as the driver, and no longer use separate configuration.

.addNewPort()
.withName(UI_PORT_NAME)
.withPort(servicePort)
.withNewTargetPort(servicePort)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Target the HTTPS connector when UI TLS is enabled

For spark.ui.port=4040 with spark.ssl.ui.enabled=true, Jetty binds HTTPS on spark.ssl.ui.port or 4440 by default, and port 4040 becomes an HTTP-to-HTTPS redirect connector. This Service nevertheless targets and exposes only 4040; TLS clients therefore reach a plaintext connector, and HTTP clients are redirected to HTTPS port 4440/8443, which the Service does not expose. SparkUI.boundPort already resolves to the actual secure connector, but the current implementation only consults it when the configured UI port equals zero. Please reconcile fixed-port Services against SparkUI.boundPort too, and cover both default and explicitly configured TLS ports.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with "[P1] Reconcile fixed UI ports after bind collisions too", have fix it.

@zhengchenyu

Copy link
Copy Markdown
Contributor Author

@sunchao
Thanks for the detailed review. Several of these findings share the same root cause: on hostNetwork, the port the driver actually binds to may differ from the configured one, so we should really be patching the Service's targetPort based on the actual bound port for both the driver Service and the UI Service. And I submit https://issues.apache.org/jira/browse/SPARK-58499. Do you think we should continue this?

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental findings on exact head 44b952a19a85a35e9ecad237dde9cfb604183e9b.

try {
val patch = new ServiceBuilder()
.withNewSpec()
.withSelector(selector.asJava)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Withdraw the UI Service selector before stopping Jetty

This installs the real selector but never withdraws it. With hostNetwork=true, spark.kubernetes.driver.ui.service.type=NodePort (or LoadBalancer), and spark.kubernetes.driver.service.deleteOnTermination=false, SparkContext.stop() closes the UI at SparkContext.scala:2350 before scheduler/backend shutdown at SparkContext.scala:2360, while KubernetesClusterSchedulerBackend.stop() leaves this Service and its selector intact. The driver JVM can continue running after sc.stop(), so its Pod remains a Ready endpoint at the shared node IP. If another application binds the released port on that node, the first application's externally reachable Service now routes to the other application's UI. This is distinct from the already-fixed startup placeholder window. Please clear the selector or delete the dedicated UI Service before releasing the UI port, regardless of the headless driver Service's retention setting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The driver has the same issue. Furthermore, by default, spark.kubernetes.driver.service.deleteOnTermination=true will delete the UI service when stops. I think this could be modified in a separate PR: if spark.kubernetes.driver.service.deleteOnTermination=false is configured, then the selector will be dropped. Actually, I don't think it's necessary to change it since the default value is already true.

.addToLabels(kubernetesConf.serviceLabels.asJava)
.endMetadata()
.withNewSpec()
.withType(serviceType)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Clean up external UI Services when the driver exits before SparkContext

A LoadBalancer or NodePort Service is created by KubernetesClientApplication before the driver application constructs SparkContext. If application startup exits or throws before a context exists, KubernetesClusterSchedulerBackend.stop() never runs, so no Service cleanup occurs even when spark.kubernetes.driver.service.deleteOnTermination=true. Owner references do not help until the driver Pod is deleted; Spark's Kubernetes documentation says completed driver Pods remain until eventual garbage collection or manual cleanup. Every early failure can therefore retain a cloud load balancer/IP or an allocated NodePort for the Pod retention period. The existing headless driver Service did not allocate either external resource. Please defer external Service provisioning until the driver initializes, or clean it up when its Pod reaches a terminal phase.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The driver service also has the same issue, which can lead to resource leaks. However, compared to that, using LoadBalancer/NodePort might result in wasted resources. Perhaps we could move the service creation to after the driver starts, but that's outside the scope of this PR discussion.

@zhengchenyu
zhengchenyu requested a review from sunchao August 4, 2026 11:00
@sunchao

sunchao commented Aug 4, 2026

Copy link
Copy Markdown
Member

@sunchao Thanks for the detailed review. Several of these findings share the same root cause: on hostNetwork, the port the driver actually binds to may differ from the configured one, so we should really be patching the Service's targetPort based on the actual bound port for both the driver Service and the UI Service. And I submit https://issues.apache.org/jira/browse/SPARK-58499. Do you think we should continue this?

@zhengchenyu Yes, I think we should continue. This PR is useful on its own because it provides a dedicated UI Service, while SPARK-58499 can address the broader driver-Service reconciliation problem. One thing to clarify is that spark.ui.port=0 still produces an invalid driver Pod, so either fix that here or avoid claiming support until SPARK-58499 resolves it. For this PR, we should also make sure the new UI Service works in driver-only mode and is properly cleaned up when the UI stops.

<td><code>spark.kubernetes.driver.ui.service.enabled</code></td>
<td><code>false</code></td>
<td>
If true, Spark will create a dedicated Kubernetes Service for the Spark driver Web UI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Document that UI Service creation requires cluster deploy mode

Spark also supports Kubernetes client-mode drivers running inside Pods, but --deploy-mode client does not run KubernetesDriverBuilder, so enabling spark.kubernetes.driver.ui.service.enabled=true silently creates no Service. Supporting client mode can remain out of scope; please document here that this setting only applies to cluster deploy mode.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, added.

@zhengchenyu

Copy link
Copy Markdown
Contributor Author

@sunchao Thanks for the detailed review. Several of these findings share the same root cause: on hostNetwork, the port the driver actually binds to may differ from the configured one, so we should really be patching the Service's targetPort based on the actual bound port for both the driver Service and the UI Service. And I submit https://issues.apache.org/jira/browse/SPARK-58499. Do you think we should continue this?

@zhengchenyu Yes, I think we should continue. This PR is useful on its own because it provides a dedicated UI Service, while SPARK-58499 can address the broader driver-Service reconciliation problem. One thing to clarify is that spark.ui.port=0 still produces an invalid driver Pod, so either fix that here or avoid claiming support until SPARK-58499 resolves it. For this PR, we should also make sure the new UI Service works in driver-only mode and is properly cleaned up when the UI stops.

@sunchao In fact, I want to solve the invalid port 0 in #57349. This PR aim to add UI service.

@zhengchenyu
zhengchenyu requested a review from sunchao August 7, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants