Skip to content

Commit 2dbf1fb

Browse files
committed
Local worker allocations
1 parent fb7b8c5 commit 2dbf1fb

10 files changed

Lines changed: 176 additions & 19 deletions

File tree

benchmarks/cdk/bin/worker.rs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
8888
info!("Starting HTTP listener on {LISTENER_ADDR}...");
8989
let listener = tokio::net::TcpListener::bind(LISTENER_ADDR).await?;
9090

91+
let self_url = get_self_url().await?;
92+
info!("Resolved self URL as {self_url}");
93+
9194
// Register S3 object store
9295
let s3_url = Url::parse(&format!("s3://{}", cmd.bucket))?;
9396

@@ -100,9 +103,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
100103
let runtime_env = Arc::new(RuntimeEnv::default());
101104
runtime_env.register_object_store(&s3_url, s3);
102105

106+
let worker = Worker::from_session_builder(|ctx: WorkerQueryContext| async move {
107+
Ok(ctx
108+
.builder
109+
.with_distributed_user_codec(WorkUnitFileScanCodec)
110+
.build())
111+
})
112+
.with_runtime_env(Arc::clone(&runtime_env));
113+
103114
let state_builder = SessionStateBuilder::new()
104115
.with_default_features()
105-
.with_runtime_env(Arc::clone(&runtime_env))
116+
.with_runtime_env(runtime_env)
117+
.with_distributed_local_worker_context(worker.to_local_worker_context(self_url))
106118
.with_distributed_worker_resolver(Ec2WorkerResolver::new())
107119
.with_distributed_planner()
108120
.with_distributed_broadcast_joins(cmd.broadcast_joins)?
@@ -119,14 +131,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
119131
let ctx = SessionContext::from(state);
120132
let ctx_clone = ctx.clone();
121133

122-
let worker = Worker::from_session_builder(|ctx: WorkerQueryContext| async move {
123-
Ok(ctx
124-
.builder
125-
.with_distributed_user_codec(WorkUnitFileScanCodec)
126-
.build())
127-
})
128-
.with_runtime_env(runtime_env);
129-
130134
let http_server = axum::serve(
131135
listener,
132136
Router::new()
@@ -295,6 +299,38 @@ fn err(s: impl Display) -> (StatusCode, String) {
295299
(StatusCode::INTERNAL_SERVER_ERROR, s.to_string())
296300
}
297301

302+
async fn get_self_url() -> Result<Url, Box<dyn Error>> {
303+
let client = reqwest::Client::new();
304+
// AWS EC2 Instance Metadata Service (IMDS) IPv4 endpoint.
305+
// AWS docs: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
306+
// We read the instance's private IPv4 from here so the worker can build its own
307+
// gRPC URL as http://<private-ip>:9001.
308+
let metadata_base = "http://169.254.169.254/latest/meta-data";
309+
310+
// Try IMDSv2 first, then fall back to unauthenticated metadata reads if token
311+
// acquisition is disabled in the instance configuration.
312+
let token = match client
313+
.put("http://169.254.169.254/latest/api/token")
314+
.header("X-aws-ec2-metadata-token-ttl-seconds", "21600")
315+
.send()
316+
.await
317+
{
318+
Ok(response) => match response.error_for_status() {
319+
Ok(response) => response.text().await.ok(),
320+
Err(_) => None,
321+
},
322+
Err(_) => None,
323+
};
324+
325+
let mut request = client.get(format!("{metadata_base}/local-ipv4"));
326+
if let Some(token) = token {
327+
request = request.header("X-aws-ec2-metadata-token", token);
328+
}
329+
330+
let local_ip = request.send().await?.error_for_status()?.text().await?;
331+
Ok(Url::parse(&format!("http://{}:9001", local_ip.trim()))?)
332+
}
333+
298334
#[derive(Clone)]
299335
struct Ec2WorkerResolver {
300336
urls: Arc<RwLock<Vec<Url>>>,

src/coordinator/prepare_dynamic_plan.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,16 @@ pub(super) async fn prepare_dynamic_plan(
8383
let mut load_info_rxs = Vec::with_capacity(input_stage.tasks);
8484

8585
let routed_urls = if input_stage.tasks == 1 {
86-
// If there's an input stage with a single worker, and the current stage is also
87-
// going to run in a single worker, we want to co-locate them so that unnecessary
88-
// network transfers are avoided.
89-
match stage_coordinator.find_input_stage_with_single_url() {
86+
match stage_coordinator
87+
// If the current coordinating context is running within the scope of a local
88+
// worker (same coordinating machine happens to also be a worker), we prefer to
89+
// co-locate single-tasked stages on it.
90+
.find_self_url()
91+
// If there's an input stage with a single worker, and the current stage is also
92+
// going to run in a single worker, we want to co-locate them so that unnecessary
93+
// network transfers are avoided.
94+
.or_else(|| stage_coordinator.find_input_stage_with_single_url())
95+
{
9096
Some(single_url) => vec![single_url],
9197
None => stage_coordinator.routed_urls()?,
9298
}

src/coordinator/query_coordinator.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::passthrough_headers::get_passthrough_headers;
88
use crate::stage::LocalStage;
99
use crate::work_unit_feed::WorkUnitFeedRegistry;
1010
use crate::work_unit_feed::{build_work_unit_batch_msg, set_work_unit_send_time};
11+
use crate::worker::LocalWorkerContext;
1112
use crate::{
1213
BytesCounterMetric, BytesMetricExt, CoordinatorToWorkerMsg,
1314
DISTRIBUTED_DATAFUSION_TASK_ID_LABEL, DistributedCodec, DistributedTaskContext,
@@ -408,6 +409,13 @@ impl<'a> StageCoordinator<'a> {
408409
Ok(routed_urls)
409410
}
410411

412+
pub(super) fn find_self_url(&self) -> Option<Url> {
413+
self.task_ctx
414+
.session_config()
415+
.get_extension::<LocalWorkerContext>()
416+
.map(|v| v.self_url.clone())
417+
}
418+
411419
pub(super) fn find_input_stage_with_single_url(&self) -> Option<Url> {
412420
let mut single_stage_url = None;
413421
self.plan

src/distributed_ext.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use crate::protocol::set_distributed_channel_resolver;
88
use crate::work_unit_feed::set_distributed_work_unit_feed;
99
use crate::worker_resolver::set_distributed_worker_resolver;
1010
use crate::{
11-
ChannelResolver, DistributedConfig, TaskEstimator, WorkUnitFeed, WorkUnitFeedProvider,
12-
WorkerResolver, get_distributed_worker_resolver,
11+
ChannelResolver, DistributedConfig, LocalWorkerContext, TaskEstimator, WorkUnitFeed,
12+
WorkUnitFeedProvider, WorkerResolver, get_distributed_worker_resolver,
1313
};
1414
use datafusion::common::DataFusionError;
1515
use datafusion::config::ConfigExtension;
@@ -600,6 +600,17 @@ pub trait DistributedExt: Sized {
600600
&mut self,
601601
bytes_per_partition_per_second: usize,
602602
) -> Result<(), DataFusionError>;
603+
604+
/// Target throughput in bytes per partition per second used by the dynamic task count
605+
/// allocator to decide how many tasks to assign to each stage based on runtime statistics.
606+
fn with_distributed_local_worker_context(
607+
self,
608+
local_worker_context: LocalWorkerContext,
609+
) -> Self;
610+
611+
/// Same as [DistributedExt::with_distributed_local_worker_context] but with an
612+
/// in-place mutation.
613+
fn set_distributed_local_worker_context(&mut self, local_worker_context: LocalWorkerContext);
603614
}
604615

605616
/// Trait to have a unified interface for getting structs & properties from SessionConfig that are used in distributed context.
@@ -768,6 +779,10 @@ impl DistributedExt for SessionConfig {
768779
Ok(())
769780
}
770781

782+
fn set_distributed_local_worker_context(&mut self, local_worker_context: LocalWorkerContext) {
783+
self.set_extension(Arc::new(local_worker_context));
784+
}
785+
771786
delegate! {
772787
to self {
773788
#[call(set_distributed_option_extension)]
@@ -859,6 +874,10 @@ impl DistributedExt for SessionConfig {
859874
#[call(set_distributed_bytes_per_partition_per_second)]
860875
#[expr($?;Ok(self))]
861876
fn with_distributed_bytes_per_partition_per_second(mut self, bytes_per_partition_per_second: usize) -> Result<Self, DataFusionError>;
877+
878+
#[call(set_distributed_local_worker_context)]
879+
#[expr($;self)]
880+
fn with_distributed_local_worker_context(mut self, local_worker_context: LocalWorkerContext) -> Self;
862881
}
863882
}
864883
}
@@ -987,6 +1006,11 @@ impl DistributedExt for SessionStateBuilder {
9871006
#[call(set_distributed_bytes_per_partition_per_second)]
9881007
#[expr($?;Ok(self))]
9891008
fn with_distributed_bytes_per_partition_per_second(mut self, bytes_per_partition_per_second: usize) -> Result<Self, DataFusionError>;
1009+
1010+
fn set_distributed_local_worker_context(&mut self, local_worker_context: LocalWorkerContext);
1011+
#[call(set_distributed_local_worker_context)]
1012+
#[expr($;self)]
1013+
fn with_distributed_local_worker_context(mut self, local_worker_context: LocalWorkerContext) -> Self;
9901014
}
9911015
}
9921016
}
@@ -1117,6 +1141,11 @@ impl DistributedExt for SessionState {
11171141
#[call(set_distributed_bytes_per_partition_per_second)]
11181142
#[expr($?;Ok(self))]
11191143
fn with_distributed_bytes_per_partition_per_second(mut self, bytes_per_partition_per_second: usize) -> Result<Self, DataFusionError>;
1144+
1145+
fn set_distributed_local_worker_context(&mut self, local_worker_context: LocalWorkerContext);
1146+
#[call(set_distributed_local_worker_context)]
1147+
#[expr($;self)]
1148+
fn with_distributed_local_worker_context(mut self, local_worker_context: LocalWorkerContext) -> Self;
11201149
}
11211150
}
11221151
}
@@ -1240,6 +1269,11 @@ impl DistributedExt for SessionContext {
12401269
#[call(set_distributed_bytes_per_partition_per_second)]
12411270
#[expr($?;Ok(self))]
12421271
fn with_distributed_bytes_per_partition_per_second(self, bytes_per_partition_per_second: usize) -> Result<Self, DataFusionError>;
1272+
1273+
fn set_distributed_local_worker_context(&mut self, local_worker_context: LocalWorkerContext);
1274+
#[call(set_distributed_local_worker_context)]
1275+
#[expr($;self)]
1276+
fn with_distributed_local_worker_context(self, local_worker_context: LocalWorkerContext) -> Self;
12431277
}
12441278
}
12451279
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub use metrics::{
3333
MaxLatencyMetric, MinLatencyMetric, P50LatencyMetric, P75LatencyMetric, P95LatencyMetric,
3434
P99LatencyMetric, rewrite_distributed_plan_with_metrics,
3535
};
36+
pub use worker::LocalWorkerContext;
3637

3738
#[cfg(any(feature = "integration", test))]
3839
pub mod test_utils;

src/test_utils/localhost.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use datafusion::common::runtime::JoinSet;
55
use datafusion::execution::SessionStateBuilder;
66
use datafusion::prelude::{SessionConfig, SessionContext};
77
use std::error::Error;
8+
use std::sync::Arc;
89
use std::time::Duration;
910
use tokio::net::TcpListener;
1011
use tonic::transport::Server;
@@ -61,11 +62,18 @@ where
6162
});
6263
}
6364
tokio::time::sleep(Duration::from_millis(100)).await;
65+
let first_worker_url = Url::parse(&format!("http://127.0.0.1:{}", ports[0])).unwrap();
6466

6567
let worker_resolver = LocalHostWorkerResolver::new(ports);
6668
let state = SessionStateBuilder::new()
6769
.with_default_features()
68-
.with_config(SessionConfig::new().with_target_partitions(3))
70+
.with_config(
71+
SessionConfig::new()
72+
.with_target_partitions(3)
73+
.with_extension(Arc::new(
74+
workers[0].to_local_worker_context(first_worker_url),
75+
)),
76+
)
6977
.with_distributed_planner()
7078
.with_distributed_worker_resolver(worker_resolver)
7179
// Test datasets are tiny, so budget one byte per partition: the estimator then asks for far

src/worker/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ mod worker_connection_pool;
99
mod worker_service;
1010

1111
pub(crate) use single_write_multi_read::SingleWriteMultiRead;
12-
pub(crate) use worker_connection_pool::{LocalWorkerContext, WorkerConnectionPool};
12+
pub(crate) use worker_connection_pool::WorkerConnectionPool;
1313

1414
pub use session_builder::{
1515
DefaultSessionBuilder, MappedWorkerSessionBuilder, MappedWorkerSessionBuilderExt,
1616
WorkerQueryContext, WorkerSessionBuilder,
1717
};
1818
pub use task_data::TaskData;
19+
pub use worker_connection_pool::LocalWorkerContext;
1920
pub use worker_service::Worker;

src/worker/worker_connection_pool.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use url::Url;
2525
///
2626
/// This information can be used for executing tasks locally bypassing gRPC comms if the tasks that
2727
/// needs to be remotely executed happens to be owned by this same worker.
28-
pub(crate) struct LocalWorkerContext {
28+
pub struct LocalWorkerContext {
2929
/// The registry of in-flight tasks the [crate::Worker] in the current scope owns.
3030
pub(crate) task_data_entries: Arc<TaskDataEntries>,
3131
/// The URL of the [crate::Worker] in scope. When trying to reach to a target URL that happens

src/worker/worker_service.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::worker::{SingleWriteMultiRead, WorkerSessionBuilder};
1+
use crate::worker::{LocalWorkerContext, SingleWriteMultiRead, WorkerSessionBuilder};
22
use crate::{DefaultSessionBuilder, TaskData, TaskKey};
33
use datafusion::common::DataFusionError;
44
use datafusion::execution::runtime_env::RuntimeEnv;
@@ -8,6 +8,7 @@ use moka::future::Cache;
88
use std::borrow::Cow;
99
use std::sync::Arc;
1010
use std::time::Duration;
11+
use url::Url;
1112

1213
const TASK_CACHE_TTI: Duration = Duration::from_mins(10);
1314

@@ -121,6 +122,17 @@ impl Worker {
121122
&self.version
122123
}
123124

125+
/// Builds a [LocalWorkerContext] suitable to be injecting into a coordinating [SessionContext].
126+
/// Having a [LocalWorkerContext] present in the coordinating [SessionContext] is not strictly
127+
/// necessary, but it allows the planner to better colocate small stages near it, avoiding
128+
/// unnecessary network hops.
129+
pub fn to_local_worker_context(&self, self_url: Url) -> LocalWorkerContext {
130+
LocalWorkerContext {
131+
task_data_entries: Arc::clone(&self.task_data_entries),
132+
self_url,
133+
}
134+
}
135+
124136
/// Returns the number of cached task entries currently held by this worker.
125137
#[cfg(any(test, feature = "integration"))]
126138
pub async fn tasks_running(&self) -> usize {

tests/local_connections.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#[cfg(test)]
2+
mod tests {
3+
use datafusion::common::internal_err;
4+
use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion};
5+
use datafusion::physical_plan::collect;
6+
use datafusion_distributed::test_utils::localhost::start_localhost_context;
7+
use datafusion_distributed::test_utils::parquet::register_parquet_tables;
8+
use datafusion_distributed::{
9+
DefaultSessionBuilder, DistributedExt, DistributedMetricsFormat, NetworkBoundaryExt,
10+
display_plan_ascii, rewrite_distributed_plan_with_metrics,
11+
};
12+
use std::sync::Arc;
13+
14+
/// During dynamic planning, if the planner decides that all the stages in the query are small
15+
/// enough to fit in a single machine, it should co-locate them on in the machine that contains
16+
/// the coordinating context, avoiding all network jumps.
17+
#[tokio::test]
18+
async fn all_local_connections_dynamic_planner() -> Result<(), Box<dyn std::error::Error>> {
19+
let (mut ctx, _guard, _) = start_localhost_context(3, DefaultSessionBuilder).await;
20+
register_parquet_tables(&ctx).await?;
21+
ctx.set_distributed_dynamic_task_count(true)?;
22+
ctx.set_distributed_file_scan_config_bytes_per_partition(1024 * 1024)?;
23+
24+
let query =
25+
r#"SELECT count(*), "RainToday" FROM weather GROUP BY "RainToday" ORDER BY count(*)"#;
26+
27+
let df = ctx.sql(query).await?;
28+
let plan = df.create_physical_plan().await?;
29+
collect(Arc::clone(&plan), ctx.task_ctx()).await?;
30+
let format = DistributedMetricsFormat::Aggregated;
31+
let plan = rewrite_distributed_plan_with_metrics(plan, format).await?;
32+
println!("{}", display_plan_ascii(plan.as_ref(), true));
33+
plan.apply(|plan| {
34+
if !plan.is_network_boundary() {
35+
return Ok(TreeNodeRecursion::Continue);
36+
};
37+
38+
let metrics = plan.metrics().unwrap();
39+
let local_connections_used = metrics
40+
.sum(|v| v.value().name() == "local_connections_used")
41+
.map_or(0, |v| v.as_usize());
42+
if local_connections_used == 0 {
43+
return internal_err!("local_connections_used==0");
44+
};
45+
46+
Ok(TreeNodeRecursion::Continue)
47+
})?;
48+
49+
Ok(())
50+
}
51+
}

0 commit comments

Comments
 (0)