1use std::path::PathBuf;
11use std::sync::Arc;
12use std::sync::LazyLock;
13use std::time::Duration;
14
15use anyhow::Context;
16use axum::http::StatusCode;
17use axum::routing;
18use fail::FailScenario;
19use futures::future;
20use hyper_util::rt::TokioIo;
21use mz_build_info::{BuildInfo, build_info};
22use mz_cloud_resources::AwsExternalIdPrefix;
23use mz_cluster_client::client::TimelyConfig;
24use mz_compute::server::{ComputeInstanceContext, ComputeRuntimeRole};
25use mz_http_util::DynamicFilterTarget;
26use mz_orchestrator_tracing::{StaticTracingConfig, TracingCliArgs};
27use mz_ore::cli::{self, CliConfig};
28use mz_ore::error::ErrorExt;
29use mz_ore::metrics::{MetricsRegistry, register_runtime_metrics};
30use mz_ore::netio::{Listener, SocketAddr};
31use mz_ore::now::SYSTEM_TIME;
32use mz_persist_client::cache::PersistClientCache;
33use mz_persist_client::cfg::PersistConfig;
34use mz_persist_client::rpc::{GrpcPubSubClient, PersistPubSubClient, PersistPubSubClientConfig};
35use mz_service::emit_boot_diagnostics;
36use mz_service::secrets::SecretsReaderCliArgs;
37use mz_service::transport;
38use mz_service::transport::ClusterServerMetrics;
39use mz_storage::storage_state::StorageInstanceContext;
40use mz_storage_types::connections::ConnectionContext;
41use mz_timely_util::capture::arc_event_link;
42use mz_txn_wal::operator::TxnsContext;
43use tokio::runtime::Handle;
44use tower::Service;
45use tracing::{Instrument, debug, error, info, info_span};
46
47mod usage_metrics;
48
49const BUILD_INFO: BuildInfo = build_info!();
50
51pub static VERSION: LazyLock<String> = LazyLock::new(|| BUILD_INFO.human_version(None));
52
53#[derive(clap::Parser)]
55#[clap(name = "clusterd", version = VERSION.as_str())]
56struct Args {
57 #[clap(
61 long,
62 env = "STORAGE_CONTROLLER_LISTEN_ADDR",
63 value_name = "HOST:PORT",
64 default_value = "127.0.0.1:2100"
65 )]
66 storage_controller_listen_addr: SocketAddr,
67 #[clap(
70 long,
71 env = "COMPUTE_CONTROLLER_LISTEN_ADDR",
72 value_name = "HOST:PORT",
73 default_value = "127.0.0.1:2101"
74 )]
75 compute_controller_listen_addr: SocketAddr,
76 #[clap(
78 long,
79 env = "INTERNAL_HTTP_LISTEN_ADDR",
80 value_name = "HOST:PORT",
81 default_value = "127.0.0.1:6878"
82 )]
83 internal_http_listen_addr: SocketAddr,
84 #[clap(long, env = "GRPC_HOST", value_name = "NAME")]
89 grpc_host: Option<String>,
90
91 #[clap(long, env = "STORAGE_TIMELY_CONFIG")]
94 storage_timely_config: TimelyConfig,
95 #[clap(long, env = "COMPUTE_TIMELY_CONFIG")]
97 compute_timely_config: TimelyConfig,
98 #[clap(long, env = "PROCESS")]
100 process: usize,
101
102 #[clap(
105 long,
106 env = "PERSIST_PUBSUB_URL",
107 value_name = "http://HOST:PORT",
108 default_value = "http://localhost:6879"
109 )]
110 persist_pubsub_url: String,
111
112 #[clap(long, env = "AWS_EXTERNAL_ID", value_name = "ID", value_parser = AwsExternalIdPrefix::new_from_cli_argument_or_environment_variable)]
117 aws_external_id_prefix: Option<AwsExternalIdPrefix>,
118
119 #[clap(long, env = "AWS_CONNECTION_ROLE_ARN")]
122 aws_connection_role_arn: Option<String>,
123
124 #[clap(flatten)]
126 secrets: SecretsReaderCliArgs,
127
128 #[clap(flatten)]
130 tracing: TracingCliArgs,
131
132 #[clap(long, env = "ENVIRONMENT_ID")]
136 environment_id: String,
137
138 #[clap(long, env = "SCRATCH_DIRECTORY", value_name = "PATH")]
140 scratch_directory: Option<PathBuf>,
141
142 #[clap(long)]
147 announce_memory_limit: Option<usize>,
148
149 #[clap(long)]
156 heap_limit: Option<usize>,
157
158 #[clap(long)]
161 is_cc: bool,
162
163 #[clap(long)]
169 worker_core_affinity: bool,
170
171 #[clap(long)]
174 enable_storage_introspection_logs: bool,
175}
176
177fn process_ordinal_from_hostname(hostname: &str) -> Option<&str> {
186 let ordinal = hostname.rsplit('-').next()?;
187 ordinal.parse::<usize>().ok().map(|_| ordinal)
188}
189
190pub fn main() {
191 mz_ore::panic::install_enhanced_handler();
192
193 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
200
201 if std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
206 && std::env::var("CLUSTERD_PROCESS").is_err()
207 {
208 if let Ok(hostname) = std::env::var("HOSTNAME") {
209 if let Some(ordinal) = process_ordinal_from_hostname(&hostname) {
210 unsafe { std::env::set_var("CLUSTERD_PROCESS", ordinal) };
215 }
216 }
217 }
218
219 let args = cli::parse_args(CliConfig {
220 env_prefix: Some("CLUSTERD_"),
221 enable_version_flag: true,
222 });
223
224 let ncpus_useful = usize::max(1, std::cmp::min(num_cpus::get(), num_cpus::get_physical()));
225 let runtime = tokio::runtime::Builder::new_multi_thread()
226 .worker_threads(ncpus_useful)
227 .thread_stack_size(3 * 1024 * 1024) .thread_name_fn(|| {
233 use std::sync::atomic::{AtomicUsize, Ordering};
234 static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
235 let id = ATOMIC_ID.fetch_add(1, Ordering::Relaxed);
236 format!("tokio:work-{}", id)
237 })
238 .enable_all()
239 .build()
240 .unwrap();
241 if let Err(err) = runtime.block_on(run(args)) {
242 panic!("clusterd: fatal: {}", err.display_with_causes());
243 }
244}
245
246async fn run(args: Args) -> Result<(), anyhow::Error> {
247 let metrics_registry = MetricsRegistry::new();
248 let tracing_handle = args
249 .tracing
250 .configure_tracing(
251 StaticTracingConfig {
252 service_name: "clusterd",
253 build_info: BUILD_INFO,
254 },
255 metrics_registry.clone(),
256 )
257 .await?;
258
259 let tracing_handle = Arc::new(tracing_handle);
260 register_runtime_metrics("main", Handle::current().metrics(), &metrics_registry);
261
262 mz_timely_util::panic::halt_on_timely_communication_panic();
265
266 let _failpoint_scenario = FailScenario::setup();
267
268 emit_boot_diagnostics!(&BUILD_INFO);
269
270 mz_alloc::register_metrics_into(&metrics_registry).await;
271 mz_metrics::register_metrics_into(
272 &metrics_registry,
273 mz_dyncfgs::all_dyncfgs(),
274 args.scratch_directory.clone(),
275 )
276 .await;
277
278 if let Some(heap_limit) = args.heap_limit {
279 mz_compute::memory_limiter::start_limiter(heap_limit, &metrics_registry);
280 } else {
281 info!("no heap limit announced; disabling memory limiter");
282 }
283
284 let secrets_reader = args
285 .secrets
286 .load()
287 .await
288 .context("loading secrets reader")?;
289
290 let usage_collector = Arc::new(usage_metrics::Collector {
291 disk_root: args.scratch_directory.clone(),
292 });
293
294 mz_ore::task::spawn(|| "clusterd_internal_http_server", {
295 let metrics_registry = metrics_registry.clone();
296 tracing::info!(
297 "serving internal HTTP server on {}",
298 args.internal_http_listen_addr
299 );
300 let listener = Listener::bind(args.internal_http_listen_addr).await?;
301 let mut make_service = mz_prof_http::router(&BUILD_INFO)
302 .route(
303 "/api/livez",
304 routing::get(mz_http_util::handle_liveness_check),
305 )
306 .route(
307 "/metrics",
308 routing::get(move |headers: axum::http::HeaderMap| async move {
309 mz_http_util::handle_prometheus(&metrics_registry, headers).await
310 }),
311 )
312 .route("/api/tracing", routing::get(mz_http_util::handle_tracing))
313 .route(
314 "/api/opentelemetry/config",
315 routing::put({
316 move |_: axum::Json<DynamicFilterTarget>| async {
317 (
318 StatusCode::BAD_REQUEST,
319 "This endpoint has been replaced. \
320 Use the `opentelemetry_filter` system variable."
321 .to_string(),
322 )
323 }
324 }),
325 )
326 .route(
327 "/api/stderr/config",
328 routing::put({
329 move |_: axum::Json<DynamicFilterTarget>| async {
330 (
331 StatusCode::BAD_REQUEST,
332 "This endpoint has been replaced. \
333 Use the `log_filter` system variable."
334 .to_string(),
335 )
336 }
337 }),
338 )
339 .route(
340 "/api/usage-metrics",
341 routing::get(async move || axum::Json(usage_collector.collect())),
342 )
343 .into_make_service();
344
345 async move {
348 loop {
349 let (conn, remote_addr) = match listener.accept().await {
350 Ok(peer) => peer,
351 Err(error) => {
352 if is_connection_error(&error) {
357 debug!("accepted connection already errored: {error:#}");
358 } else {
359 error!("internal_http accept error: {error:#}");
360 tokio::time::sleep(Duration::from_secs(1)).await;
361 }
362 continue;
363 }
364 };
365
366 let tower_service = make_service.call(&conn).await.expect("infallible");
367 let hyper_service =
368 hyper::service::service_fn(move |req| tower_service.clone().call(req));
369
370 mz_ore::task::spawn(
371 || format!("clusterd_internal_http_server:{remote_addr}"),
372 async move {
373 if let Err(error) = hyper::server::conn::http1::Builder::new()
374 .serve_connection(TokioIo::new(conn), hyper_service)
375 .await
376 {
377 info!("error serving internal_http connection: {error:#}");
381 }
382 },
383 );
384 }
385 }
386 });
387
388 let pubsub_caller_id = std::env::var("HOSTNAME")
389 .ok()
390 .or_else(|| args.tracing.log_prefix.clone())
391 .unwrap_or_default();
392 let mut persist_cfg =
393 PersistConfig::new(&BUILD_INFO, SYSTEM_TIME.clone(), mz_dyncfgs::all_dyncfgs());
394 persist_cfg.is_cc_active = args.is_cc;
395 persist_cfg.announce_memory_limit = args.announce_memory_limit;
396 persist_cfg.disable_compaction();
398
399 let persist_clients = Arc::new(PersistClientCache::new(
400 persist_cfg,
401 &metrics_registry,
402 |persist_cfg, metrics| {
403 let cfg = PersistPubSubClientConfig {
404 url: args.persist_pubsub_url,
405 caller_id: pubsub_caller_id,
406 persist_cfg: persist_cfg.clone(),
407 };
408 GrpcPubSubClient::connect(cfg, metrics)
409 },
410 ));
411 let txns_ctx = TxnsContext::default();
412
413 let connection_context = ConnectionContext::from_cli_args(
414 args.environment_id,
415 &args.tracing.startup_log_filter,
416 args.aws_external_id_prefix,
417 args.aws_connection_role_arn,
418 secrets_reader,
419 None,
420 );
421
422 let grpc_host = args.grpc_host.and_then(|h| (!h.is_empty()).then_some(h));
423 let cluster_server_metrics = ClusterServerMetrics::register_with(&metrics_registry);
424
425 let mut storage_timely_config = args.storage_timely_config;
426 storage_timely_config.process = args.process;
427 let mut compute_timely_config = args.compute_timely_config;
428 compute_timely_config.process = args.process;
429
430 assert_eq!(
432 storage_timely_config.workers, compute_timely_config.workers,
433 "storage and compute must have equal workers-per-process",
434 );
435
436 let (storage_log_writers, storage_log_readers) = if args.enable_storage_introspection_logs {
438 (0..storage_timely_config.workers)
439 .map(|_| arc_event_link())
440 .unzip()
441 } else {
442 (Vec::new(), Vec::new())
443 };
444
445 let storage_client_builder = mz_storage::serve(
447 storage_timely_config,
448 &metrics_registry,
449 Arc::clone(&persist_clients),
450 txns_ctx.clone(),
451 Arc::clone(&tracing_handle),
452 SYSTEM_TIME.clone(),
453 connection_context.clone(),
454 StorageInstanceContext::new(args.scratch_directory.clone(), args.announce_memory_limit),
455 storage_log_writers,
456 )
457 .await?;
458 info!(
459 "listening for storage controller connections on {}",
460 args.storage_controller_listen_addr
461 );
462 mz_ore::task::spawn(
463 || "storage_server",
464 transport::serve(
465 args.storage_controller_listen_addr,
466 BUILD_INFO.semver_version(),
467 grpc_host.clone(),
468 Duration::MAX,
469 storage_client_builder,
470 cluster_server_metrics.for_server("storage"),
471 )
472 .instrument(info_span!("ctp", name = "storage")),
473 );
474
475 let compute_client_builder = mz_compute::server::serve(
477 compute_timely_config,
478 ComputeRuntimeRole::Solo,
479 &metrics_registry,
480 persist_clients,
481 txns_ctx,
482 tracing_handle,
483 ComputeInstanceContext {
484 scratch_directory: args.scratch_directory,
485 worker_core_affinity: args.worker_core_affinity,
486 connection_context,
487 },
488 storage_log_readers,
489 )
490 .await?;
491 info!(
492 "listening for compute controller connections on {}",
493 args.compute_controller_listen_addr
494 );
495 mz_ore::task::spawn(
496 || "compute_server",
497 transport::serve(
498 args.compute_controller_listen_addr,
499 BUILD_INFO.semver_version(),
500 grpc_host.clone(),
501 Duration::MAX,
502 compute_client_builder,
503 cluster_server_metrics.for_server("compute"),
504 )
505 .instrument(info_span!("ctp", name = "compute")),
506 );
507
508 future::pending().await
512}
513
514fn is_connection_error(e: &std::io::Error) -> bool {
518 matches!(
519 e.kind(),
520 std::io::ErrorKind::ConnectionRefused
521 | std::io::ErrorKind::ConnectionAborted
522 | std::io::ErrorKind::ConnectionReset
523 )
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[mz_ore::test]
531 fn test_process_ordinal_from_hostname() {
532 assert_eq!(
534 process_ordinal_from_hostname("mz5ncn-cluster-s1-replica-s1-gen-1-0"),
535 Some("0")
536 );
537 assert_eq!(
538 process_ordinal_from_hostname("mz5ncn-cluster-s1-replica-s1-gen-1-11"),
539 Some("11")
540 );
541 assert_eq!(process_ordinal_from_hostname("7"), Some("7"));
543
544 assert_eq!(process_ordinal_from_hostname("clusterd"), None);
548 assert_eq!(process_ordinal_from_hostname("replica-abc"), None);
549 assert_eq!(process_ordinal_from_hostname("replica-"), None);
550 assert_eq!(process_ordinal_from_hostname(""), None);
551 }
552}