All checks were successful
Run Check Script / check (pull_request) Successful in 1m3s
254 lines
8.3 KiB
Rust
254 lines
8.3 KiB
Rust
use std::fs;
|
|
use std::io::{self, Write};
|
|
use std::process::{Command, Stdio};
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use chrono::Local;
|
|
use clap::Parser;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// A simple yet powerful I/O benchmarking tool using fio.
|
|
#[derive(Parser, Debug)]
|
|
#[command(author, version, about, long_about = None)]
|
|
struct Args {
|
|
/// Target for the benchmark.
|
|
/// Formats:
|
|
/// - localhost (default)
|
|
/// - ssh/{user}@{host}
|
|
/// - ssh/{user}@{host}:{port}
|
|
/// - k8s/{namespace}/{pod}
|
|
#[arg(short, long, default_value = "localhost")]
|
|
target: String,
|
|
|
|
#[arg(short, long, default_value = ".")]
|
|
benchmark_dir: String,
|
|
|
|
/// Comma-separated list of tests to run.
|
|
/// Available tests: read, write, randread, randwrite,
|
|
/// multiread, multiwrite, multirandread, multirandwrite.
|
|
#[arg(long, default_value = "read,write,randread,randwrite,multiread,multiwrite,multirandread,multirandwrite")]
|
|
tests: String,
|
|
|
|
/// Duration of each test in seconds.
|
|
#[arg(long, default_value_t = 15)]
|
|
duration: u64,
|
|
|
|
/// Output directory for results.
|
|
/// Defaults to ./iobench-{current_datetime}.
|
|
#[arg(long)]
|
|
output_dir: Option<String>,
|
|
|
|
/// The size of the test file for fio.
|
|
#[arg(long, default_value = "1G")]
|
|
size: String,
|
|
|
|
/// The block size for I/O operations.
|
|
#[arg(long, default_value = "4k")]
|
|
block_size: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct FioOutput {
|
|
jobs: Vec<FioJobResult>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct FioJobResult {
|
|
jobname: String,
|
|
read: FioMetrics,
|
|
write: FioMetrics,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct FioMetrics {
|
|
bw: f64,
|
|
iops: f64,
|
|
clat_ns: LatencyMetrics,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct LatencyMetrics {
|
|
mean: f64,
|
|
stddev: f64,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct BenchmarkResult {
|
|
test_name: String,
|
|
iops: f64,
|
|
bandwidth_kibps: f64,
|
|
latency_mean_ms: f64,
|
|
latency_stddev_ms: f64,
|
|
}
|
|
|
|
fn main() -> io::Result<()> {
|
|
let args = Args::parse();
|
|
|
|
let output_dir = args.output_dir.unwrap_or_else(|| {
|
|
format!("./iobench-{}", Local::now().format("%Y-%m-%d-%H%M%S"))
|
|
});
|
|
fs::create_dir_all(&output_dir)?;
|
|
|
|
let tests_to_run: Vec<&str> = args.tests.split(',').collect();
|
|
let mut results = Vec::new();
|
|
|
|
for test in tests_to_run {
|
|
println!("--------------------------------------------------");
|
|
println!("Running test: {}", test);
|
|
|
|
let (rw, numjobs) = match test {
|
|
"read" => ("read", 1),
|
|
"write" => ("write", 1),
|
|
"randread" => ("randread", 1),
|
|
"randwrite" => ("randwrite", 1),
|
|
"multiread" => ("read", 4),
|
|
"multiwrite" => ("write", 4),
|
|
"multirandread" => ("randread", 4),
|
|
"multirandwrite" => ("randwrite", 4),
|
|
_ => {
|
|
eprintln!("Unknown test: {}. Skipping.", test);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let test_name = format!("{}-{}-sync-test", test, args.block_size);
|
|
let fio_command = format!(
|
|
"fio --filename={}/iobench_testfile --direct=1 --fsync=1 --rw={} --bs={} --numjobs={} --iodepth=1 --runtime={} --time_based --group_reporting --name={} --size={} --output-format=json",
|
|
args.benchmark_dir, rw, args.block_size, numjobs, args.duration, test_name, args.size
|
|
);
|
|
|
|
println!("Executing command:\n{}\n", fio_command);
|
|
|
|
let output = match run_command(&args.target, &fio_command) {
|
|
Ok(out) => out,
|
|
Err(e) => {
|
|
eprintln!("Failed to execute command for test {}: {}", test, e);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
|
|
let result = parse_fio_output(&output, &test_name, rw);
|
|
// TODO store raw fio output and print it
|
|
match result {
|
|
Ok(res) => {
|
|
results.push(res);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error parsing fio output for test {}: {}", test, e);
|
|
eprintln!("Raw output:\n{}", output);
|
|
}
|
|
}
|
|
|
|
println!("{output}");
|
|
println!("Test {} completed.", test);
|
|
// A brief pause to let the system settle before the next test.
|
|
thread::sleep(Duration::from_secs(2));
|
|
}
|
|
|
|
// Cleanup the test file on the target
|
|
println!("--------------------------------------------------");
|
|
println!("Cleaning up test file on target...");
|
|
let cleanup_command = "rm -f ./iobench_testfile";
|
|
if let Err(e) = run_command(&args.target, cleanup_command) {
|
|
eprintln!("Warning: Failed to clean up test file on target: {}", e);
|
|
} else {
|
|
println!("Cleanup successful.");
|
|
}
|
|
|
|
|
|
if results.is_empty() {
|
|
println!("\nNo benchmark results to display.");
|
|
return Ok(());
|
|
}
|
|
|
|
// Output results to a CSV file for easy analysis
|
|
let csv_path = format!("{}/summary.csv", output_dir);
|
|
let mut wtr = csv::Writer::from_path(&csv_path)?;
|
|
for result in &results {
|
|
wtr.serialize(result)?;
|
|
}
|
|
wtr.flush()?;
|
|
|
|
println!("\nBenchmark summary saved to {}", csv_path);
|
|
println!("\n--- Benchmark Results Summary ---");
|
|
println!("{:<25} {:>10} {:>18} {:>20} {:>22}", "Test Name", "IOPS", "Bandwidth (KiB/s)", "Latency Mean (ms)", "Latency StdDev (ms)");
|
|
println!("{:-<98}", "");
|
|
for result in results {
|
|
println!("{:<25} {:>10.2} {:>18.2} {:>20.4} {:>22.4}", result.test_name, result.iops, result.bandwidth_kibps, result.latency_mean_ms, result.latency_stddev_ms);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn run_command(target: &str, command: &str) -> io::Result<String> {
|
|
let (program, args) = if target == "localhost" {
|
|
("sudo", vec!["sh".to_string(), "-c".to_string(), command.to_string()])
|
|
} else if target.starts_with("ssh/") {
|
|
let target_str = target.strip_prefix("ssh/").unwrap();
|
|
let ssh_target;
|
|
let mut ssh_args = vec!["-o".to_string(), "StrictHostKeyChecking=no".to_string()];
|
|
let port_parts: Vec<&str> = target_str.split(':').collect();
|
|
if port_parts.len() == 2 {
|
|
ssh_target = port_parts[0].to_string();
|
|
ssh_args.push("-p".to_string());
|
|
ssh_args.push(port_parts[1].to_string());
|
|
} else {
|
|
ssh_target = target_str.to_string();
|
|
}
|
|
ssh_args.push(ssh_target);
|
|
ssh_args.push(format!("sudo sh -c '{}'", command));
|
|
("ssh", ssh_args)
|
|
} else if target.starts_with("k8s/") {
|
|
let parts: Vec<&str> = target.strip_prefix("k8s/").unwrap().split('/').collect();
|
|
if parts.len() != 2 {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid k8s target format. Expected k8s/{namespace}/{pod}"));
|
|
}
|
|
let namespace = parts[0];
|
|
let pod = parts[1];
|
|
("kubectl", vec!["exec".to_string(), "-n".to_string(), namespace.to_string(), pod.to_string(), "--".to_string(), "sh".to_string(), "-c".to_string(), command.to_string()])
|
|
} else {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid target format"));
|
|
};
|
|
|
|
let mut cmd = Command::new(program);
|
|
cmd.args(&args);
|
|
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
|
|
|
let child = cmd.spawn()?;
|
|
let output = child.wait_with_output()?;
|
|
|
|
if !output.status.success() {
|
|
eprintln!("Command failed with status: {}", output.status);
|
|
io::stderr().write_all(&output.stderr)?;
|
|
return Err(io::Error::new(io::ErrorKind::Other, "Command execution failed"));
|
|
}
|
|
|
|
String::from_utf8(output.stdout)
|
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
|
}
|
|
|
|
fn parse_fio_output(output: &str, test_name: &str, rw: &str) -> Result<BenchmarkResult, String> {
|
|
let fio_data: FioOutput = serde_json::from_str(output)
|
|
.map_err(|e| format!("Failed to deserialize fio JSON: {}", e))?;
|
|
|
|
let job_result = fio_data.jobs.iter()
|
|
.find(|j| j.jobname == test_name)
|
|
.ok_or_else(|| format!("Could not find job result for '{}' in fio output", test_name))?;
|
|
|
|
let metrics = if rw.contains("read") {
|
|
&job_result.read
|
|
} else {
|
|
&job_result.write
|
|
};
|
|
|
|
Ok(BenchmarkResult {
|
|
test_name: test_name.to_string(),
|
|
iops: metrics.iops,
|
|
bandwidth_kibps: metrics.bw,
|
|
latency_mean_ms: metrics.clat_ns.mean / 1_000_000.0,
|
|
latency_stddev_ms: metrics.clat_ns.stddev / 1_000_000.0,
|
|
})
|
|
}
|