Skip to content

Commit 8cab32f

Browse files
committed
perf: add NBI benchmark python script; update docs
Signed-off-by: Tony Chen <a122774007@gmail.com>
1 parent 2031091 commit 8cab32f

9 files changed

Lines changed: 319 additions & 20 deletions

File tree

docs/assets/nbi/nbi_scale.png

170 KB
Loading

docs/nbi.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,7 @@ $ ais ls ais://.sys-inventory --props chunked,size
380380
NAME CHUNKED SIZE
381381
aws/@#/training-data-v5/inv-Tp4nR7kWx yes 1.28GiB
382382
```
383+
384+
## Performance
385+
386+
A latency-vs-scale benchmark comparing NBI listing, AIS regular listing, and direct S3 access (boto3) from 1K to 80K objects is available at [`python/tests/perf/nbi/`](../python/tests/perf/nbi/README.md).

python/aistore/common_requirements

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ braceexpand>=0.1.7
1010
msgspec>=0.18.5
1111
xxhash>=3.5.0
1212
boto3>=1.37.10
13+
tqdm>=4.66.3
1314
pylint>=3.2.7
1415
pytest>=8.3.5,<9.0.0 # TODO: The newly released pytest 9.0.0 is incompatible with pytest-xdist on Python 3.10.x. Revisit and remove this upper limit.
1516
pytest-xdist>=3.6.1

python/tests/perf/__init__.py

Whitespace-only changes.

python/tests/perf/get_batch/get_batch_bench.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414

1515
@dataclass
16-
class BenchmarkResult:
16+
class BenchmarkResult: # pylint: disable=too-many-instance-attributes
1717
"""Structured benchmark result data"""
1818

1919
bucket_name: str
@@ -71,7 +71,7 @@ def compare_to_baseline(self, baseline: BenchmarkResult):
7171
)
7272

7373

74-
def benchmark_sequential_get(
74+
def benchmark_sequential_get( # pylint: disable=too-many-locals
7575
client: Client, bucket_name: str, num_objects: int
7676
) -> BenchmarkResult:
7777
"""Benchmark getting objects one by one sequentially."""
@@ -119,7 +119,7 @@ def benchmark_sequential_get(
119119
return result
120120

121121

122-
def benchmark_get_batch(
122+
def benchmark_get_batch( # pylint: disable=too-many-locals
123123
client: Client,
124124
bucket_name: str,
125125
num_objects: int,
@@ -287,7 +287,8 @@ def print_comprehensive_results(results: List[BenchmarkComparison]):
287287

288288
print(f"\n{'='*120}")
289289
print(
290-
f"{'Test Configuration':<35} {'Duration':<10} {'vs Baseline':<20} {'Speedup':<10} {'Throughput':<15} {'vs Baseline':<12}"
290+
f"{'Test Configuration':<35} {'Duration':<10} {'vs Baseline':<20}"
291+
f" {'Speedup':<10} {'Throughput':<15} {'vs Baseline':<12}"
291292
)
292293
print(f"{'='*120}")
293294

@@ -315,7 +316,8 @@ def print_comprehensive_results(results: List[BenchmarkComparison]):
315316
improvement_str = "N/A"
316317

317318
print(
318-
f"{comp.test_config:<35} {result.duration:<10.2f} {time_vs_baseline:<20} {speedup_str:<10} {throughput_str:<15} {improvement_str:<12}"
319+
f"{comp.test_config:<35} {result.duration:<10.2f} {time_vs_baseline:<20}"
320+
f" {speedup_str:<10} {throughput_str:<15} {improvement_str:<12}"
319321
)
320322

321323
print(f"{'='*120}")
@@ -334,7 +336,9 @@ def print_comprehensive_results(results: List[BenchmarkComparison]):
334336
print(f" Total Objects: {result.total_objects:,}")
335337
print(f" Duration: {result.duration:.3f} seconds")
336338
print(
337-
f" Success Rate: {result.successful_reads}/{result.total_objects} ({100*result.successful_reads/result.total_objects:.1f}%)"
339+
f" Success Rate: "
340+
f"{result.successful_reads}/{result.total_objects}"
341+
f" ({100*result.successful_reads/result.total_objects:.1f}%)"
338342
)
339343
print(f" Throughput: {result.throughput_objects_per_sec:.1f} objects/s")
340344
print(f" Data Throughput: {result.throughput_mib_per_sec:.2f} MiB/s")
@@ -386,32 +390,32 @@ def save_results_to_json(results: List[BenchmarkComparison], filename: str = Non
386390
def main():
387391
"""Main comprehensive benchmarking function."""
388392
# Configuration
389-
AISTORE_URL = "http://<ais-endpt>:51080" # Replace <ais-endpt> with the actual AIStore endpoint
390-
BUCKET_NAME = "benchmark-10KiB" # Updated to match populate_bucket.py
391-
TOTAL_OBJECTS = 10000 # 100,000 objects as requested
393+
aistore_url = "http://<ais-endpt>:51080" # Replace with actual endpoint
394+
bucket_name = "benchmark-10KiB"
395+
total_objects = 10000
392396

393397
print("Starting comprehensive batch benchmark...")
394-
print(f"AIStore URL: {AISTORE_URL}")
395-
print(f"Bucket: {BUCKET_NAME}")
396-
print(f"Total objects per test: {TOTAL_OBJECTS:,}")
398+
print(f"AIStore URL: {aistore_url}")
399+
print(f"Bucket: {bucket_name}")
400+
print(f"Total objects per test: {total_objects:,}")
397401

398402
# Create client with optimized pool size
399-
client = Client(AISTORE_URL, max_pool_size=1000)
403+
client = Client(aistore_url, max_pool_size=1000)
400404

401405
# Verify bucket exists
402406
try:
403-
bucket = client.bucket(BUCKET_NAME)
407+
bucket = client.bucket(bucket_name)
404408
bucket.head()
405-
print(f"Bucket '{BUCKET_NAME}' found - proceeding with benchmark")
409+
print(f"Bucket '{bucket_name}' found - proceeding with benchmark")
406410
except Exception as e:
407-
print(f"Error accessing bucket '{BUCKET_NAME}': {e}")
411+
print(f"Error accessing bucket '{bucket_name}': {e}")
408412
print("Make sure to run populate_bucket.py first to create test objects")
409413
print("The bucket should contain at least 100,000 objects for this benchmark")
410414
return
411415

412416
# Run comprehensive benchmark
413417
try:
414-
results = run_comprehensive_benchmark(client, BUCKET_NAME, TOTAL_OBJECTS)
418+
results = run_comprehensive_benchmark(client, bucket_name, total_objects)
415419

416420
if not results:
417421
print("No benchmark results obtained. Check for errors above.")

python/tests/perf/get_batch/populate_bucket.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,8 @@ def upload_object(self, bucket, obj_name: str, content: bytes, retries: int = 3)
5555
if attempt < retries - 1:
5656
time.sleep(0.1 * (2**attempt)) # Exponential backoff
5757
continue
58-
else:
59-
with self.lock:
60-
self.stats["failed"] += 1
58+
with self.lock:
59+
self.stats["failed"] += 1
6160
self.logger.error(
6261
"Failed to upload %s after %d attempts: %s",
6362
obj_name,

python/tests/perf/nbi/README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# NBI Latency-vs-Scale Benchmark
2+
3+
Measures listing latency of [Native Bucket Inventory (NBI)](../../../../docs/nbi.md) against two baselines — AIS regular listing and direct S3 access via boto3 — across bucket sizes from 1K to 80K objects.
4+
5+
## Background
6+
7+
| Path | How it works |
8+
|---|---|
9+
| **AIS Regular** | Client sends one request to the AIS proxy. One designated target walks `ListObjectsV2` sequentially (1000 keys/page) and returns the aggregated result. |
10+
| **NBI creation** | A one-time job that walks S3 (same as Regular) and writes the full bucket namespace as binary inventory chunks on each target's local disk. |
11+
| **NBI listing** | Subsequent listings read the pre-built inventory chunks from local disk — no S3 calls. |
12+
| **S3 Direct** | Client calls boto3 `ListObjectsV2` in a pagination loop directly against S3 (MaxKeys=1000, same page size as AIS). |
13+
14+
## Setup
15+
16+
### 1. Connect to the cluster
17+
18+
```bash
19+
export AIS_ENDPOINT=http://<proxy>:51080
20+
ais show cluster
21+
```
22+
23+
### 2. Generate source objects (one-time)
24+
25+
Objects are 1-byte files uploaded via `aisloader` to an S3-backed bucket. The subdirectory prefix isolates each scale point so they can coexist in the same bucket.
26+
27+
```bash
28+
for subdir in 1k 2k 5k 10k 20k 40k 50k 80k; do
29+
count=$(echo $subdir | sed 's/k/*1000/' | bc)
30+
aisloader -bucket=my-bucket -provider=aws \
31+
-subdir=bench/${subdir} \
32+
-pctput=100 -maxputs=${count} \
33+
-minsize=1 -maxsize=1 \
34+
-numworkers=16 -cleanup=false -skiplist \
35+
-duration=10m
36+
done
37+
```
38+
39+
Or run each prefix individually:
40+
41+
```bash
42+
aisloader -bucket=my-bucket -provider=aws -subdir=bench/1k -pctput=100 -maxputs=1000 -minsize=1 -maxsize=1 -numworkers=16 -cleanup=false -skiplist -duration=10m
43+
aisloader -bucket=my-bucket -provider=aws -subdir=bench/2k -pctput=100 -maxputs=2000 -minsize=1 -maxsize=1 -numworkers=16 -cleanup=false -skiplist -duration=10m
44+
aisloader -bucket=my-bucket -provider=aws -subdir=bench/5k -pctput=100 -maxputs=5000 -minsize=1 -maxsize=1 -numworkers=16 -cleanup=false -skiplist -duration=10m
45+
aisloader -bucket=my-bucket -provider=aws -subdir=bench/10k -pctput=100 -maxputs=10000 -minsize=1 -maxsize=1 -numworkers=16 -cleanup=false -skiplist -duration=10m
46+
aisloader -bucket=my-bucket -provider=aws -subdir=bench/20k -pctput=100 -maxputs=20000 -minsize=1 -maxsize=1 -numworkers=16 -cleanup=false -skiplist -duration=10m
47+
aisloader -bucket=my-bucket -provider=aws -subdir=bench/40k -pctput=100 -maxputs=40000 -minsize=1 -maxsize=1 -numworkers=16 -cleanup=false -skiplist -duration=10m
48+
aisloader -bucket=my-bucket -provider=aws -subdir=bench/50k -pctput=100 -maxputs=50000 -minsize=1 -maxsize=1 -numworkers=16 -cleanup=false -skiplist -duration=10m
49+
aisloader -bucket=my-bucket -provider=aws -subdir=bench/80k -pctput=100 -maxputs=80000 -minsize=1 -maxsize=1 -numworkers=16 -cleanup=false -skiplist -duration=10m
50+
```
51+
52+
### 3. Install dependencies
53+
54+
```bash
55+
pip install aistore boto3
56+
```
57+
58+
### 4. Run the benchmark
59+
60+
```bash
61+
python3 bench.py
62+
```
63+
64+
Flags:
65+
66+
| Flag | Default | Description |
67+
|---|---|---|
68+
| `--endpoint` | `$AIS_ENDPOINT` | AIS proxy URL |
69+
| `--bucket` | `my-bucket` | S3 bucket name |
70+
| `--provider` | `aws` | backend provider |
71+
| `--runs` | `20` | listing repetitions per scale point |
72+
73+
## Results
74+
75+
![NBI benchmark result](../../../../docs/assets/nbi/nbi_scale.png)
76+
77+
```
78+
==========================================================================================================================
79+
NBI latency-vs-scale (20 runs each)
80+
==========================================================================================================================
81+
Objects Creation --- Regular (ms) --- --- NBI (ms) --- --- S3 Direct (ms) --- Speedup
82+
p50 p95 p99 sd p50 p95 p99 sd p50 p95 p99 sd (R/N)
83+
--------------------------------------------------------------------------------------------------------------------------
84+
1K 472ms 179 429 429 90 25 110 110 20 259 302 302 21 7.2x
85+
2K 559ms 321 603 603 97 32 106 106 21 548 604 604 38 9.9x
86+
5K 1094ms 826 1190 1190 103 60 135 135 29 1343 1640 1640 88 13.8x
87+
10K 1934ms 1766 2201 2201 160 164 199 199 39 3100 4842 4842 608 10.7x
88+
20K 3455ms 3541 3978 3978 194 271 394 394 38 5475 6140 6140 277 13.1x
89+
40K 6578ms 7249 8075 8075 460 577 682 682 61 11246 14341 14341 1089 12.6x
90+
50K 7978ms 9110 10072 10072 592 674 841 841 69 13873 21246 21246 1770 13.5x
91+
80K 13375ms 14724 15973 15973 633 1244 1367 1367 61 22180 28541 28541 1731 11.8x
92+
==========================================================================================================================
93+
```

python/tests/perf/nbi/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)