Skip to content

Commit b89b617

Browse files
frankmcsherryclaude
andcommitted
persist: place each snapshot part on the least loaded worker
`shard_source` handed each part to a worker chosen at random. The comment explained why not round robin: parts that alternate large and small would leave half the workers idle. Random dodges that but accepts balls-in-bins imbalance, and with the tens of parts a snapshot typically has, the busiest worker ends up with two to three times the bytes of the least busy one, and hydration waits for the busiest. Send each part to the worker that has received the fewest encoded bytes so far. The size is already computed for the coalescing logic. This handles both failure modes, since placement follows load rather than position, and is deterministic for a given part sequence. Hydrating an index over a 10M-row table on four workers, three repetitions: 1.14s (1.10 to 1.25) to 0.90s (0.89 to 0.91), with the per-worker dataflow time going from 0.43, 0.60, 1.18, 0.35s to 0.85, 0.55, 0.67, 0.60s. An index on a distinct view went from 1.07s (1.02 to 1.36) to 1.04s (1.02 to 1.05). A join whose skew comes from a small dimension table was unchanged. Single-worker hydration is unaffected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent f17fb27 commit b89b617

1 file changed

Lines changed: 21 additions & 10 deletions

File tree

src/persist-client/src/operators/shard_source.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use std::hash::{Hash, Hasher};
2020
use std::pin::pin;
2121
use std::rc::Rc;
2222
use std::sync::Arc;
23-
use std::time::Instant;
2423

2524
use anyhow::anyhow;
2625
use arrow::array::ArrayRef;
@@ -511,6 +510,9 @@ where
511510
let coalesce_target = u64::cast_from(SOURCE_HYDRATION_FRONTIER_COALESCE_BYTES.get(&cfg));
512511
// Encoded bytes emitted since the last forwarded progress.
513512
let mut coalesced_bytes: u64 = 0;
513+
// Encoded bytes handed to each worker so far, so each part can go to the least
514+
// loaded worker.
515+
let mut worker_bytes: Vec<u64> = vec![0; num_workers];
514516

515517
// If `until.less_equal(current_frontier)`, it means that all subsequent batches will contain only
516518
// times greater or equal to `until`, which means they can be dropped in their entirety.
@@ -598,15 +600,22 @@ where
598600
}
599601
}
600602

601-
// Give the part to a random worker. This isn't round robin in an attempt to avoid
602-
// skew issues: if your parts alternate size large, small, then you'll end up only
603-
// using half of your workers.
604-
//
605-
// There's certainly some other things we could be doing instead here, but this has
606-
// seemed to work okay so far. Continue to revisit as necessary.
607-
let worker_idx = usize::cast_from(Instant::now().hashed()) % num_workers;
608-
batch_bytes =
609-
batch_bytes.saturating_add(u64::cast_from(part_desc.encoded_size_bytes()));
603+
// Give the part to the worker that has received the fewest encoded bytes so
604+
// far. Round robin fails when parts alternate large and small, leaving half the
605+
// workers idle; random placement avoids that but accepts balls-in-bins
606+
// imbalance, which with the tens of parts a snapshot typically has leaves the
607+
// busiest worker with two to three times the bytes of the least busy one, and
608+
// hydration waits for the busiest. Placing by load handles both, and is
609+
// deterministic for a given part sequence.
610+
let part_bytes = u64::cast_from(part_desc.encoded_size_bytes());
611+
let worker_idx = worker_bytes
612+
.iter()
613+
.enumerate()
614+
.min_by_key(|(idx, bytes)| (**bytes, *idx))
615+
.map(|(idx, _)| idx)
616+
.expect("at least one worker");
617+
worker_bytes[worker_idx] = worker_bytes[worker_idx].saturating_add(part_bytes);
618+
batch_bytes = batch_bytes.saturating_add(part_bytes);
610619
let (part, lease) = part_desc.into_exchangeable_part();
611620
leases.borrow_mut().push_at(current_ts.clone(), lease);
612621
descs_output.give(&session_cap, (worker_idx, part));
@@ -808,6 +817,8 @@ where
808817

809818
#[cfg(test)]
810819
mod tests {
820+
use std::time::Instant;
821+
811822
use super::*;
812823
use std::sync::Arc;
813824

0 commit comments

Comments
 (0)