Yannick Goetschel created SPARK-58649:
-----------------------------------------

             Summary: Allow SparkApplication drivers ingress to SparkCluster 
workers  
                 Key: SPARK-58649
                 URL: https://issues.apache.org/jira/browse/SPARK-58649
             Project: Spark
          Issue Type: Bug
          Components: Kubernetes
    Affects Versions: kubernetes-operator-1.0.0
         Environment:   We run per-user Spark Connect sessions as 
SparkApplications attached to a shared
  standalone SparkCluster. Most jobs are fine, but every so often one dies like 
this:


{code:java}
Job aborted due to stage failure: Task 11 in stage 2.0 failed 4 times, most 
recent
failure: Lost task 11.3 in stage 2.0 (TID 86) (<executor-ip> executor 3):
TaskResultLost (result lost from block manager)
{code}

The driver log explains why only some of them:

{code:java}
 java.io.IOException: Connecting to /<executor-ip>:<ephemeral-port> timed out 
(120000 ms)
        at 
org.apache.spark.network.client.TransportClientFactory.createClient(...)
        at 
org.apache.spark.network.netty.NettyBlockTransferService$$anon$2.createAndStart(...)
        at 
org.apache.spark.network.shuffle.RetryingBlockTransferor.transferAllOutstanding(...)
        at org.apache.spark.storage.BlockManager.fetchRemoteManagedBuffer(...)
        at org.apache.spark.scheduler.TaskResultGetter$$anon$3.run(...)
 {code}

The driver cannot reach the executors' block manager. That only matters once a 
task
result goes over {{spark.task.maxDirectResultSize}} (1 MB): below it the result 
rides
along inline in the RPC, above it the executor parks the result in its block 
manager and
hands the driver an {{IndirectTaskResult}} to go and fetch. The tasks that 
stayed under
the limit never noticed anything. The three that went over timed out, retried 
three
times and gave up, and the stage aborted on the fourth attempt.

The culprit is the worker NetworkPolicy the operator generates for every 
SparkCluster in
{{SparkClusterResourceSpec#buildWorkerNetworkPolicy}}:

{code:yaml}
  podSelector:
    matchLabels: {spark-role: worker, spark.operator/spark-cluster-name: 
<cluster>}
  ingress:
  - from:
    - podSelector:
        matchLabels: {spark.operator/spark-cluster-name: <cluster>}
  policyTypes: [Ingress]
  {code}

Only pods carrying the cluster label may talk to the workers. But a 
SparkApplication
attached to the cluster is a separate resource: its driver is labelled
{{spark-role: driver}} and {{spark.operator/spark-app-name: <app>}}, never the 
cluster
label. So the driver is locked out of its own executors.

The executor-to-driver direction works perfectly well —
 executors register, tasks get dispatched, tasks run. Only the return path for 
large
 results is broken, so the cluster looks healthy right up until a job happens 
to cross
 the 1 MB threshold.

            Reporter: Yannick Goetschel


The worker \{{NetworkPolicy}} generated for every \{{SparkCluster}} by
  \{{SparkClusterResourceSpec#buildWorkerNetworkPolicy}} admits ingress only 
from pods
  labelled \{{spark.operator/spark-cluster-name: <cluster>}}:

  \{code:yaml}
  podSelector:
    matchLabels: \{spark-role: worker, spark.operator/spark-cluster-name: 
<cluster>}
  ingress:
  - from:
    - podSelector:
        matchLabels: \{spark.operator/spark-cluster-name: <cluster>}
  policyTypes: [Ingress]
  \{code}

  A \{{SparkApplication}} that attaches to a standalone \{{SparkCluster}}
  (\{{spark.master=spark://<cluster>-master-svc:7077}}) is a separate resource. 
Its driver
  pod is labelled \{{spark-role: driver}} and \{{spark.operator/spark-app-name: 
<app>}},
  never the cluster label, so every driver-to-worker connection is dropped.

  The gap is invisible under normal load. Worker egress is unrestricted and no 
policy
  selects driver pods, so executors register with the driver and tasks run 
normally. Spark
  only uses the driver-to-executor direction when a task result exceeds
  \{{spark.task.maxDirectResultSize}} (1 MB): the executor then stores the 
result in its
  block manager and returns an \{{IndirectTaskResult}} for the driver to fetch. 
That fetch
  times out, \{{RetryingBlockTransferor}} retries three times, and the task 
fails with:

  \{code}
  TaskResultLost (result lost from block manager)
  \{code}

  After four attempts the stage aborts. The failure is not reachable any other 
way: that
  branch in \{{TaskResultGetter#enqueueSuccessfulTask}} only runs for an
  \{{IndirectTaskResult}}.

  It cannot be worked around from the client side either. Executors read
  \{{spark.task.maxDirectResultSize}} from the driver \{{SparkConf}} at launch, 
so a Spark
  Connect client cannot raise it after the session has started.

  Observed on a Spark Connect session attached to a standalone 
\{{SparkCluster}}, on a CNI
  that enforces NetworkPolicy. Three of 25 tasks in a write stage produced 
results above
  the threshold and the job aborted; every other task in the same stage 
succeeded. Driver
  log:

  \{code}
  java.io.IOException: Connecting to /<executor-ip>:<ephemeral-port> timed out 
(120000 ms)
        at 
org.apache.spark.network.client.TransportClientFactory.createClient(...)
        at 
org.apache.spark.network.netty.NettyBlockTransferService$$anon$2.createAndStart(...)
        at 
org.apache.spark.network.shuffle.RetryingBlockTransferor.transferAllOutstanding(...)
        at org.apache.spark.storage.BlockManager.fetchRemoteManagedBuffer(...)
        at org.apache.spark.scheduler.TaskResultGetter$$anon$3.run(...)
  \{code}

  Proposed fix: add a second ingress peer to the generated policy admitting 
same-namespace
  pods labelled \{{spark-role: driver}}. This only widens ingress and cannot 
deny traffic
  that was previously allowed.
  \{code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

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

Reply via email to