|
| 1 | +# FIO Per-Job Data: OpenSearch vs Raw Archives |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +FIO benchmark results contain both **aggregated metrics** (totals across all jobs/disks) and **per-job breakdown** (individual disk performance). Due to OpenSearch's 5,000 field limit, only aggregated data is exported to the `zathras-results` index. Per-job data remains available in the raw JSON archives. |
| 6 | + |
| 7 | +## What's in OpenSearch (zathras-results) |
| 8 | + |
| 9 | +Each FIO run in OpenSearch contains: |
| 10 | + |
| 11 | +### Aggregated Metrics |
| 12 | +- **Bandwidth**: total_bandwidth_kbps (min/max/mean) |
| 13 | +- **IOPS**: total_iops (min/max/mean) |
| 14 | +- **Latency**: avg_latency_mean_ns, avg_clat_mean_ns, avg_slat_mean_ns (with min/max/stddev) |
| 15 | +- **Latency percentiles**: p1, p5, p10, p50, p90, p95, p99, p99.5, p99.9 |
| 16 | +- **I/O totals**: total_io_bytes, total_ios |
| 17 | +- **CPU**: avg_cpu_usr_pct, avg_cpu_sys_pct |
| 18 | +- **Metadata**: num_jobs, num_disks |
| 19 | + |
| 20 | +### Timeseries Summary |
| 21 | +- Statistical summary of timeseries data: count, mean, min, max, stddev |
| 22 | + |
| 23 | +### Configuration |
| 24 | +- All FIO test parameters and settings |
| 25 | + |
| 26 | +**Use OpenSearch for**: Aggregate performance trends, run comparisons, dashboard visualizations |
| 27 | + |
| 28 | +## What's in Raw JSON Archives |
| 29 | + |
| 30 | +The full JSON documents (not exported to OpenSearch) additionally contain: |
| 31 | + |
| 32 | +### Per-Job Details (`metrics.jobs` array) |
| 33 | + |
| 34 | +For **each disk/job**: |
| 35 | +- Job metadata: job_number, jobname, device path, elapsed_seconds |
| 36 | +- Read metrics (if read test): |
| 37 | + - Bandwidth: kbps, min, max, mean, stddev, aggregate % |
| 38 | + - IOPS: value, min, max, mean, stddev |
| 39 | + - Latency: mean, min, max, stddev (regular + clat + slat) |
| 40 | + - **Latency percentiles**: p1, p5, p10, p50, p90, p95, p99, p99.5, p99.9 |
| 41 | + - I/O: bytes, count, runtime |
| 42 | + - CPU: usr%, sys% |
| 43 | + - **I/O depth distribution**: % at depth 1, 2, 4, 8, 16, 32, 64+ |
| 44 | + - **Latency distribution buckets**: microsecond and millisecond ranges |
| 45 | +- Write metrics (if write test): same structure |
| 46 | +- Mixed metrics (if mixed test): both read and write |
| 47 | + |
| 48 | +### Full Timeseries Data |
| 49 | +- Every timeseries point with timestamp and metrics |
| 50 | +- Available in separate `zathras-timeseries` index (if enabled) |
| 51 | + |
| 52 | +**Use raw JSON for**: Per-disk analysis, identifying slow disks, latency distribution analysis |
| 53 | + |
| 54 | +## Accessing Per-Job Data |
| 55 | + |
| 56 | +### Method 1: Direct File Read |
| 57 | + |
| 58 | +Raw JSON documents are stored in the same location as the benchmark archives: |
| 59 | + |
| 60 | +```python |
| 61 | +import json |
| 62 | +from pathlib import Path |
| 63 | + |
| 64 | +# Load the full document |
| 65 | +json_path = Path("/path/to/archive/fio-results.json") |
| 66 | +with open(json_path) as f: |
| 67 | + doc = json.load(f) |
| 68 | + |
| 69 | +# Access per-job data for a specific run |
| 70 | +jobs = doc["results"]["runs"]["run_0"]["metrics"]["jobs"] |
| 71 | + |
| 72 | +for job in jobs: |
| 73 | + print(f"Device: {job['device']}") |
| 74 | + print(f" Bandwidth: {job['read']['bandwidth_kbps']} kbps") |
| 75 | + print(f" IOPS: {job['read']['iops']}") |
| 76 | + print(f" P99 Latency: {job['read']['latency_percentiles']['p99']} ns") |
| 77 | +``` |
| 78 | + |
| 79 | +### Method 2: Programmatic Access (Python API) |
| 80 | + |
| 81 | +```python |
| 82 | +from chronicler.processors.fio_processor import FioProcessor |
| 83 | + |
| 84 | +# Process with full detail |
| 85 | +processor = FioProcessor("/path/to/benchmark/archive") |
| 86 | +document = processor.process() |
| 87 | + |
| 88 | +# Get full dict (includes per-job data) |
| 89 | +full_dict = document.to_dict() |
| 90 | + |
| 91 | +# Access per-job data |
| 92 | +jobs = full_dict["results"]["runs"]["run_0"]["metrics"]["jobs"] |
| 93 | +``` |
| 94 | + |
| 95 | +### Method 3: Query Pattern for Analysis |
| 96 | + |
| 97 | +Example script to find slow disks across multiple test runs: |
| 98 | + |
| 99 | +```python |
| 100 | +import json |
| 101 | +from pathlib import Path |
| 102 | + |
| 103 | +def find_slow_disks(json_path, p99_threshold_ns=500_000): |
| 104 | + """Find disks with p99 latency above threshold.""" |
| 105 | + with open(json_path) as f: |
| 106 | + doc = json.load(f) |
| 107 | + |
| 108 | + slow_disks = [] |
| 109 | + for run_key, run_data in doc["results"]["runs"].items(): |
| 110 | + if "metrics" not in run_data or "jobs" not in run_data["metrics"]: |
| 111 | + continue |
| 112 | + |
| 113 | + for job in run_data["metrics"]["jobs"]: |
| 114 | + device = job.get("device", "unknown") |
| 115 | + read_data = job.get("read", {}) |
| 116 | + p99 = read_data.get("latency_percentiles", {}).get("p99") |
| 117 | + |
| 118 | + if p99 and p99 > p99_threshold_ns: |
| 119 | + slow_disks.append({ |
| 120 | + "run": run_key, |
| 121 | + "device": device, |
| 122 | + "p99_latency_ns": p99, |
| 123 | + "bandwidth_kbps": read_data.get("bandwidth_kbps"), |
| 124 | + "iops": read_data.get("iops"), |
| 125 | + }) |
| 126 | + |
| 127 | + return slow_disks |
| 128 | + |
| 129 | +# Usage |
| 130 | +slow = find_slow_disks("fio-results.json", p99_threshold_ns=500_000) |
| 131 | +for disk in slow: |
| 132 | + print(f"{disk['device']} in {disk['run']}: p99={disk['p99_latency_ns']}ns") |
| 133 | +``` |
| 134 | + |
| 135 | +## Why Not Store Per-Job Data in OpenSearch? |
| 136 | + |
| 137 | +### Design Decision |
| 138 | + |
| 139 | +FIO was the only benchmark that stored per-instance breakdown in OpenSearch. Other benchmarks (CoreMark, Passmark, Uperf) follow an aggregated approach: |
| 140 | +- **CoreMark**: Aggregate across threads, not per-thread |
| 141 | +- **Passmark**: Aggregate across iterations, not per-iteration |
| 142 | +- **Uperf**: Aggregate across workers, not per-worker |
| 143 | + |
| 144 | +To maintain consistency and stay within OpenSearch's 5,000 field limit, FIO now follows the same pattern. |
| 145 | + |
| 146 | +### Field Count Impact |
| 147 | + |
| 148 | +**With per-job data** (48 runs, 1 job each): |
| 149 | +- Fields: ~6,632 |
| 150 | +- Status: ❌ Exceeds 5,000 limit |
| 151 | + |
| 152 | +**Without per-job data**: |
| 153 | +- Fields: ~3,176 |
| 154 | +- Status: ✅ Under 5,000 limit (36% headroom) |
| 155 | + |
| 156 | +### Future: Separate Per-Job Index |
| 157 | + |
| 158 | +If per-job querying in OpenSearch becomes a frequent need, a separate `zathras-fio-job-timeseries` index could be implemented (similar to how general timeseries data is handled). See [GitHub issue #19](https://github.com/redhat-performance/chronicler/issues/19) for discussion. |
| 159 | + |
| 160 | +## Summary |
| 161 | + |
| 162 | +| Data Type | OpenSearch | Raw JSON | |
| 163 | +|-----------|------------|----------| |
| 164 | +| Aggregated metrics | ✅ | ✅ | |
| 165 | +| Timeseries summary | ✅ | ✅ | |
| 166 | +| Configuration | ✅ | ✅ | |
| 167 | +| Per-job breakdown | ❌ | ✅ | |
| 168 | +| Full timeseries | ❌ | ✅ | |
| 169 | + |
| 170 | +**For most analysis**: Use OpenSearch (fast queries, dashboards) |
| 171 | +**For per-disk troubleshooting**: Use raw JSON archives (full granularity) |
0 commit comments