1use std::collections::BTreeMap;
21use std::sync::atomic::{AtomicUsize, Ordering};
22use std::sync::{Arc, Mutex};
23use std::time::{Duration, Instant};
24
25use anyhow::bail;
26use aws_smithy_runtime_api::box_error::BoxError;
27use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
28use aws_smithy_runtime_api::client::http::{
29 HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpClient,
30 SharedHttpConnector,
31};
32use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
33use aws_smithy_runtime_api::client::runtime_components::{
34 RuntimeComponents, RuntimeComponentsBuilder,
35};
36use aws_smithy_types::config_bag::ConfigBag;
37use bytes::Bytes;
38use futures::{StreamExt, TryStreamExt};
39use mz_ore::cast::CastLossy;
40use mz_ore::error::ErrorExt;
41use mz_ore::metrics::MetricsRegistry;
42use mz_ore::now::SYSTEM_TIME;
43use mz_ore::secure::SecureString;
44use mz_persist::location::Blob;
45use mz_persist::metrics::S3BlobMetrics;
46use mz_persist::s3::{BLOB_HTTP_POOL_MAX_IN_FLIGHT_PER_CLIENT, S3Blob, S3BlobConfig};
47
48use crate::cfg::PersistConfig;
49
50#[derive(Debug, clap::Args)]
52pub struct BlobReadArgs {
53 #[clap(subcommand)]
54 command: Command,
55}
56
57#[derive(Debug, clap::Subcommand)]
58enum Command {
59 Upload(UploadArgs),
62 Read(ReadArgs),
66}
67
68#[derive(Debug, clap::Args)]
69struct StoreArgs {
70 #[clap(long, default_value = "https://storage.googleapis.com")]
71 endpoint: String,
72 #[clap(long, default_value = "auto")]
73 region: String,
74 #[clap(long)]
75 bucket: String,
76 #[clap(long, default_value = "blob-read-bench")]
78 prefix: String,
79 #[clap(long, env = "BLOB_READ_BENCH_ACCESS_KEY_ID")]
80 access_key_id: String,
81 #[clap(
82 long,
83 env = "BLOB_READ_BENCH_SECRET_ACCESS_KEY",
84 hide_env_values = true
85 )]
86 secret_access_key: SecureString,
87}
88
89#[derive(Debug, clap::Args)]
90struct UploadArgs {
91 #[clap(flatten)]
92 store: StoreArgs,
93 #[clap(long, value_delimiter = ',', default_value = "8,16,64")]
94 sizes_mib: Vec<usize>,
95 #[clap(long, default_value_t = 70)]
96 count_per_size: usize,
97 #[clap(long, default_value_t = 16)]
98 concurrency: usize,
99}
100
101#[derive(Debug, clap::Args)]
102struct ReadArgs {
103 #[clap(flatten)]
104 store: StoreArgs,
105 #[clap(long, value_enum)]
106 client: ClientKind,
107 #[clap(long, value_delimiter = ',', default_value = "16,64,256,512")]
109 concurrency: Vec<usize>,
110 #[clap(long, value_parser = humantime::parse_duration, default_value = "30s")]
113 duration: Duration,
114 #[clap(long, default_value_t = 10)]
116 puts_per_sec: u32,
117 #[clap(long, default_value_t = 1024)]
118 put_size_kib: usize,
119 #[clap(long, value_parser = humantime::parse_duration, default_value = "10s")]
121 put_stall: Duration,
122}
123
124const PUT_KEYS: usize = 100;
127const PUT_KEY_PREFIX: &str = "put/";
129
130#[derive(Debug, Clone, Copy, clap::ValueEnum)]
131enum ClientKind {
132 Sdk,
134 Pooled,
137}
138
139pub(crate) async fn run(args: BlobReadArgs) -> Result<(), anyhow::Error> {
141 match args.command {
142 Command::Upload(args) => upload(args).await,
143 Command::Read(args) => read(args).await,
144 }
145}
146
147async fn open(
149 store: &StoreArgs,
150 client: ClientKind,
151 registry: &MetricsRegistry,
152 header_waits: Arc<HeaderWaits>,
153) -> Result<S3Blob, anyhow::Error> {
154 let knobs = PersistConfig::new_default_configs(&crate::BUILD_INFO, SYSTEM_TIME.clone());
155 let metrics = S3BlobMetrics::new(registry);
156 let pool_metrics = metrics.http_pool();
157 let config = S3BlobConfig::new(
158 store.bucket.clone(),
159 store.prefix.clone(),
160 None,
161 Some(store.endpoint.clone()),
162 Some(store.region.clone()),
163 Some((
164 store.access_key_id.clone(),
165 store.secret_access_key.unsecure().to_owned(),
166 )),
167 Box::new(knobs),
168 metrics,
169 )
170 .await?;
171 let http_client = match client {
172 ClientKind::Sdk => SharedHttpClient::new(mz_aws_util::http_client()),
173 ClientKind::Pooled => SharedHttpClient::new(mz_aws_util::pooled_http_client(
174 || *BLOB_HTTP_POOL_MAX_IN_FLIGHT_PER_CLIENT.default(),
175 pool_metrics,
176 )),
177 };
178 let config = config.with_http_client(TimedHttpClient {
179 inner: http_client,
180 waits: header_waits,
181 });
182 Ok(S3Blob::open(config).await?)
183}
184
185async fn upload(args: UploadArgs) -> Result<(), anyhow::Error> {
186 let blob = open(
187 &args.store,
188 ClientKind::Sdk,
189 &MetricsRegistry::new(),
190 Arc::default(),
191 )
192 .await?;
193 let objects: Vec<_> = args
194 .sizes_mib
195 .iter()
196 .flat_map(|size| (0..args.count_per_size).map(move |i| (format!("{size}mib-{i}"), *size)))
197 .collect();
198 let mut uploads = futures::stream::iter(objects)
199 .map(|(key, size_mib)| {
200 let blob = &blob;
201 async move {
202 let mut data = vec![0; size_mib * 1024 * 1024];
203 rand::fill(&mut data[..]);
204 blob.set(&key, Bytes::from(data)).await?;
205 Ok::<_, anyhow::Error>(key)
206 }
207 })
208 .buffer_unordered(args.concurrency);
209 while let Some(key) = uploads.try_next().await? {
210 println!("uploaded {key}");
211 }
212 Ok(())
213}
214
215#[derive(Debug, Default)]
216struct ReaderStats {
217 latencies: Vec<Duration>,
218 failed_latencies: Vec<Duration>,
219 bytes: u64,
220 errors: BTreeMap<String, u64>,
221}
222
223#[derive(Debug, Default)]
224struct PutStats {
225 latencies: Vec<Duration>,
226 stalled: usize,
227 errors: BTreeMap<String, u64>,
228}
229
230async fn read(args: ReadArgs) -> Result<(), anyhow::Error> {
231 let registry = MetricsRegistry::new();
232 let header_waits = Arc::new(HeaderWaits::default());
233 let blob = Arc::new(
234 open(
235 &args.store,
236 args.client,
237 ®istry,
238 Arc::clone(&header_waits),
239 )
240 .await?,
241 );
242 let mut keys = Vec::new();
243 blob.list_keys_and_metadata("", &mut |meta| {
244 if !meta.key.starts_with(PUT_KEY_PREFIX) {
245 keys.push(meta.key.to_owned());
246 }
247 })
248 .await?;
249 if keys.is_empty() {
250 bail!("no objects under prefix {}", args.store.prefix);
251 }
252 let keys = Arc::new(keys);
253 let mut put_data = vec![0; args.put_size_kib * 1024];
254 rand::fill(&mut put_data[..]);
255 let put_data = Bytes::from(put_data);
256 let next_put = Arc::new(AtomicUsize::new(0));
257
258 for &concurrency in &args.concurrency {
259 let read_timeouts_before = read_timeouts(®istry);
260 header_waits.take();
261 let next_key = Arc::new(AtomicUsize::new(0));
262 let start = Instant::now();
263 let deadline = start + args.duration;
264 let putter = (args.puts_per_sec > 0).then(|| {
265 let blob = Arc::clone(&blob);
266 let put_data = put_data.clone();
267 let next_put = Arc::clone(&next_put);
268 let period = Duration::from_secs(1) / args.puts_per_sec;
269 let put_stall = args.put_stall;
270 mz_ore::task::spawn(|| "blob_read_bench_putter", async move {
271 let mut puts = Vec::new();
274 let mut ticker = tokio::time::interval(period);
275 while Instant::now() < deadline {
276 ticker.tick().await;
277 let n = next_put.fetch_add(1, Ordering::Relaxed) % PUT_KEYS;
278 let key = format!("{PUT_KEY_PREFIX}{n}");
279 let blob = Arc::clone(&blob);
280 let put_data = put_data.clone();
281 puts.push(mz_ore::task::spawn(|| "blob_read_bench_put", async move {
282 let put_start = Instant::now();
283 let result = blob.set(&key, put_data).await;
284 (put_start.elapsed(), result)
285 }));
286 }
287 let mut stats = PutStats::default();
288 for put in puts {
289 let (elapsed, result) = put.await;
290 if elapsed > put_stall {
291 stats.stalled += 1;
292 }
293 match result {
294 Ok(()) => stats.latencies.push(elapsed),
295 Err(err) => {
296 *stats
297 .errors
298 .entry(err.display_with_causes().to_string())
299 .or_default() += 1
300 }
301 }
302 }
303 stats
304 })
305 });
306 let readers: Vec<_> = (0..concurrency)
307 .map(|_| {
308 let blob = Arc::clone(&blob);
309 let keys = Arc::clone(&keys);
310 let next_key = Arc::clone(&next_key);
311 mz_ore::task::spawn(|| "blob_read_bench_reader", async move {
312 let mut stats = ReaderStats::default();
313 while Instant::now() < deadline {
314 let key = &keys[next_key.fetch_add(1, Ordering::Relaxed) % keys.len()];
315 let get_start = Instant::now();
316 let result = blob.get(key).await;
317 let elapsed = get_start.elapsed();
318 let err = match result {
319 Ok(Some(value)) => {
320 stats.latencies.push(elapsed);
321 stats.bytes += u64::try_from(value.len()).expect("fits");
322 continue;
323 }
324 Ok(None) => format!("{key} missing"),
325 Err(err) => err.display_with_causes().to_string(),
326 };
327 stats.failed_latencies.push(elapsed);
328 *stats.errors.entry(err).or_default() += 1;
329 }
330 stats
331 })
332 })
333 .collect();
334
335 let mut total = ReaderStats::default();
336 for reader in readers {
337 let stats = reader.await;
338 total.latencies.extend(stats.latencies);
339 total.failed_latencies.extend(stats.failed_latencies);
340 total.bytes += stats.bytes;
341 for (err, count) in stats.errors {
342 *total.errors.entry(err).or_default() += count;
343 }
344 }
345 let elapsed = start.elapsed();
346 let puts = match putter {
347 Some(putter) => putter.await,
348 None => PutStats::default(),
349 };
350
351 println!(
353 "client={:?} concurrency={} elapsed={:.1}s read_timeouts={} get_MiB/s={:.1}",
354 args.client,
355 concurrency,
356 elapsed.as_secs_f64(),
357 read_timeouts(®istry) - read_timeouts_before,
358 f64::cast_lossy(total.bytes) / (1024.0 * 1024.0) / elapsed.as_secs_f64(),
359 );
360 println!(
361 " gets: {} errors={}",
362 summary(total.latencies),
363 total.errors.values().sum::<u64>(),
364 );
365 println!(" failed gets: {}", summary(total.failed_latencies));
366 println!(
367 " puts: {} errors={} stalled(>{:?})={}",
368 summary(puts.latencies),
369 puts.errors.values().sum::<u64>(),
370 args.put_stall,
371 puts.stalled,
372 );
373 for (method, waits) in header_waits.take() {
374 println!(" {method} wait for headers: {}", summary(waits));
375 }
376 for (err, count) in &total.errors {
377 println!(" get error {count}x: {err}");
378 }
379 for (err, count) in &puts.errors {
380 println!(" put error {count}x: {err}");
381 }
382 }
383 Ok(())
384}
385
386fn summary(mut samples: Vec<Duration>) -> String {
388 samples.sort();
389 let percentile = |p: usize| {
390 samples
391 .get(samples.len().saturating_sub(1) * p / 100)
392 .copied()
393 .unwrap_or_default()
394 };
395 format!(
396 "n={} p50={:?} p99={:?} max={:?}",
397 samples.len(),
398 percentile(50),
399 percentile(99),
400 percentile(100),
401 )
402}
403
404#[derive(Debug, Default)]
409struct HeaderWaits(Mutex<BTreeMap<String, Vec<Duration>>>);
410
411impl HeaderWaits {
412 fn record(&self, method: String, wait: Duration) {
413 let mut waits = self.0.lock().expect("lock poisoned");
414 waits.entry(method).or_default().push(wait);
415 }
416
417 fn take(&self) -> BTreeMap<String, Vec<Duration>> {
418 std::mem::take(&mut *self.0.lock().expect("lock poisoned"))
419 }
420}
421
422#[derive(Debug)]
424struct TimedHttpClient {
425 inner: SharedHttpClient,
426 waits: Arc<HeaderWaits>,
427}
428
429impl HttpClient for TimedHttpClient {
430 fn http_connector(
431 &self,
432 settings: &HttpConnectorSettings,
433 components: &RuntimeComponents,
434 ) -> SharedHttpConnector {
435 SharedHttpConnector::new(TimedConnector {
436 inner: self.inner.http_connector(settings, components),
437 waits: Arc::clone(&self.waits),
438 })
439 }
440
441 fn validate_base_client_config(
442 &self,
443 runtime_components: &RuntimeComponentsBuilder,
444 cfg: &ConfigBag,
445 ) -> Result<(), BoxError> {
446 self.inner
447 .validate_base_client_config(runtime_components, cfg)
448 }
449
450 fn validate_final_config(
451 &self,
452 runtime_components: &RuntimeComponents,
453 cfg: &ConfigBag,
454 ) -> Result<(), BoxError> {
455 self.inner.validate_final_config(runtime_components, cfg)
456 }
457
458 fn connector_metadata(&self) -> Option<ConnectorMetadata> {
459 self.inner.connector_metadata()
460 }
461}
462
463#[derive(Debug)]
464struct TimedConnector {
465 inner: SharedHttpConnector,
466 waits: Arc<HeaderWaits>,
467}
468
469impl HttpConnector for TimedConnector {
470 fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
471 let method = request.method().to_owned();
472 let waits = Arc::clone(&self.waits);
473 let start = Instant::now();
474 let response = self.inner.call(request);
475 HttpConnectorFuture::new(async move {
476 let response = response.await;
477 waits.record(method, start.elapsed());
478 response
479 })
480 }
481}
482
483fn read_timeouts(registry: &MetricsRegistry) -> u64 {
486 registry
487 .gather()
488 .iter()
489 .find(|family| family.name() == "mz_persist_s3_read_timeouts")
490 .map_or(0, |family| {
491 u64::cast_lossy(family.get_metric()[0].get_counter().value())
492 })
493}