andygrove commented on issue #1943:
URL: 
https://github.com/apache/datafusion-ballista/issues/1943#issuecomment-4886318132

   ## Investigation summary: root cause + six attempts + the design tension
   
   I spent a good while reproducing and instrumenting this on a 2-executor × 
8-core
   cluster (TPC-H SF100 from S3/MinIO, `target_partitions` 128/256). Posting 
the full
   findings so the fix can be decided as a design question rather than guessed 
at — I
   tried six approaches and each hit a different corner of what looks like a 
fundamental
   tension.
   
   ### Confirmed root cause: shuffle-connection churn
   
   With executor-side `RUST_LOG=info,h2=debug`, a single failing 
`target_partitions=128`
   run shows the shuffle client **opening and tearing down ~3,500 connections** 
(client-
   initiated `GOAWAY` frames, all `NO_ERROR`; **zero `RST_STREAM`**, so not h2
   rapid-reset). The failure is an intermittent **race**: the client's own 
connection
   close racing an in-flight body read produces the `h2 … BrokenPipe` ("error 
reading a
   body from connection"); with client caching off the same churn exhausts 
ephemeral
   ports (`EADDRNOTAVAIL`). The churn comes from the pool checking out an 
**exclusive
   connection per fetch** and returning it on drop.
   
   ### The tension (why it's not a one-line fix)
   
   A shuffle read opens **many concurrent partition streams**. Each open stream 
needs
   *either* its own connection *or* to share one — and every option hits a wall:
   
   | Connection model | Failure |
   |---|---|
   | One connection **per fetch** (today) | churn → broken-pipe race / 
ephemeral-port exhaustion |
   | Hold-connection-for-stream + keepalive | still broken pipe — the churn is 
unchanged |
   | **Multiplex**, 1 connection/peer | **hang** — the shared 64 KB h2 
connection-level flow-control window starves under many concurrent fetch 
streams; executors go fully idle |
   | **Multiplex**, small pool (N/peer) | same hang, just on heavier queries 
(raises the threshold, doesn't remove it) |
   | **Bounded exclusive pool** (semaphore) | **hang** — permit starvation: a 
stage opens more concurrent partition streams than the pool cap; the streams 
holding connections can't release (they're waiting on the consumer, which is 
waiting on a stream that can't get a connection) |
   | Unbounded exclusive pool | back to churn / port exhaustion |
   
   So: exclusive-per-open-stream → either unbounded (churn/ports) or bounded 
(starvation
   deadlock); shared/multiplexed → h2 flow-control deadlock. Each of the six 
attempts
   landed on one of these.
   
   ### What each attempt showed (evidence)
   
   1. **Hold the pooled connection for the response-stream lifetime** (so it 
isn't
      reused/evicted mid-stream). Reduced it but did **not** fix it — still 
broke, because
      the *churn* itself remained.
   2. **HTTP/2 keepalive on shuffle connections** (they were created with a 
`None` config,
      i.e. *no* keepalive at all). Correct on its own merits, but didn't fix 
the broken
      pipe — the connection dies while *actively* streaming, not while idle.
   3. **Multiplex over one connection per peer.** Removed the churn entirely, 
but **hung**:
      the executors sit at ~0 CPU on the first shuffle-heavy query. The tiny 
default
      connection-level flow-control window is the suspect.
   4. **Multiplex over a small round-robin pool (N=8 per peer).** Cleared the 
moderate
      shuffle stage but **hung on a heavier join** (more concurrent 
streams/connection).
   5. **Bounded *exclusive* pool (semaphore, ≤64/endpoint, reuse, one fetch per
      connection).** Also **hung**, and earlier — permit starvation as above.
   
   (Per-query timings, where runs completed, were on par with `main` — so none 
of this is
   a throughput problem; it's a liveness one.)
   
   ### Options that remain (haven't tried / need design)
   
   1. **Multiplex + enlarge the h2 flow-control windows.** Bump
      `initial_connection_window_size` / `initial_stream_window_size` from the 
64 KB
      default to several MB on the shuffle client (and matching server 
settings). This is
      the one principled lever I did **not** pull, and it directly targets the 
multiplex
      deadlock cause. If it works, multiplexing becomes viable and removes the 
churn.
   2. **Fetch each partition to completion before releasing the connection** 
(buffer to
      memory or spill to disk). Decouples connection-hold-time from 
consume-time, so a
      bounded exclusive pool no longer starves. Costs a per-partition buffer.
   3. **A dedicated shuffle transport** not built on per-partition Flight 
`do_get`.
   
   ### Environment / repro
   
   - Ballista `main` (DataFusion 54), executors 8 cores each, `--client-ttl 60`.
   - TPC-H SF100 from S3; `target_partitions` 128 fails ~2 of 3 runs on `main`; 
256 fails
     more often. The shuffle-heavy queries (Q7/Q9/Q17/Q21) are where failures 
cluster.
   
   Happy to run any of the above against this cluster if a direction is chosen. 
My instinct
   is option 1 (flow-control windows) is the smallest principled next step, but 
this is a
   core-transport decision so I wanted the full picture on the record first.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to