운영 환경 벤치마크
이 end-to-end benchmark는 production 형태의 deployment를 사용합니다. uvicorn 위의 FastAPI, request path의 DLRM CPU inference, Aerospike CE, k6 HTTP load, serialize된 response body를 모두 포함합니다.
HTTP parsing, JSON serialization, model forward pass처럼 recommendation serving pod가 부담하는 비용이 모두 측정됩니다. 공통 비용 때문에 Isolated Benchmark보다 client 간 차이는 작게 보이지만, capacity planning에는 이 수치가 더 적합합니다.
Sources: 2026년 4월에 내부에서 측정했습니다. Raw 산출물은 저장소에 commit하지 않았으며,
benchmark/를 다시 작성하기 전 git history의benchmark/results/*.md에서 확인할 수 있습니다. 아래 주요 수치는 해당 산출물에서 가져왔습니다. 새 isolated harness에서 다시 측정하는 작업은 Bottleneck Analysis에서 추적합니다.
격차가 어디 있는가
Mean latency advantage of aerospike-py vs official (Python 3.11 + GIL)
A) Pure DB client ████████████████████ −80% (108 → 22 ms) 🔥
B) uvicorn ASGI █████ −21% (290 → 228 ms)
C) uvicorn + DLRM ███████████ −42% p95 (324→189 ms) 🔥
mean 차이가 줄어도 Environment C에서는 p95 우위가 유지됨: 공식 client의 result 변환이 tail에서 GIL 때문에 직렬화됨.
- A — Pure DB client
- B — uvicorn ASGI only
- C — uvicorn + DLRM (실 서빙)
Environment A — Pure DB Client
측정 범위. FastAPI, model inference, HTTP load generator를 제외합니다. Python loop가 batch_read를 직접 호출하므로 client 자체의 차이가 가장 크게 나타납니다.
Setup
| Item | Value |
|---|---|
| Source | 2026년 4월 내부 run (pre-rewrite artifact; git history 에서 복원 가능) |
| Clients | official (sync C), official-async (executor 로 sync 래핑), py-async (aerospike-py) |
| Sets / batch sizes | 9 / 50, 200 |
| Concurrency / iterations | 10 / 30 |
Aggregate 결과 (9 sets × 2 batch sizes 평균)
| Client | avg mean (ms) | avg p99 (ms) | avg TPS |
|---|---|---|---|
| official | 107.56 | 195.34 | 138.2 |
| official-async | 110.64 | 211.33 | 125.7 |
| py-async (aerospike-py) | 22.45 | 120.67 | 373.7 |
| py-async vs official | latency 4.8× 빠름 | 1.6× | TPS 2.7× 높음 |
| py-async vs official-async | 4.9× 빠름 | 1.8× | 3.0× |
Per-set speedup 분포 (aerospike-py vs official)
Batch 50: mean speedup across 9 sets 4.4× ──────── 7.8× (median ~5.8×)
Batch 200: mean speedup across 9 sets 3.1× ──────── 6.6× (median ~4.5×)
Outlier: set_8 (0% found rate) → fast not-found path, 실 read latency 아님.
set_8 은 outlierFound rate가 0%입니다. 공식 client는 success path보다 error를 빨리 반환하므로 이 row는 실제 read latency가 아니라 빠른 not-found path를 보여 줍니다. 나머지 모든 set에서는 mean latency가 4~8배 개선되었습니다.
**official-async**는 sync C client를 loop.run_in_executor로 실행합니다. Request마다 thread pool을 거치므로 모든 set에서 bare sync client보다 조금 느립니다. 따라서 aerospike-py와 official-async의 차이는 official과의 차이보다 일관되게 큽니다.
Environment B — uvicorn ASGI Only
측정 범위. batch_read에 FastAPI와 uvicorn만 추가하고 model inference는 제외합니다. Key-value 조회 앞에 REST API를 둔 형태입니다.
Setup
| Item | Value |
|---|---|
| Source | 2026년 4월 내부 run (pre-rewrite artifact) |
| Concurrency / iterations | 5 / 50 |
Pipeline breakdown (latency in ms)
| Client | total mean | p50 | p90 | p95 | p99 | aerospike step | inference | TPS |
|---|---|---|---|---|---|---|---|---|
| official | 289.56 | 289.36 | 429.74 | 461.15 | 473.60 | 280.00 | 1.55 | 16.6 |
| aerospike-py | 228.49 | 189.56 | 457.42 | 468.03 | 497.09 | 221.12 | 1.46 | 19.4 |
- Total mean: −21% (290 → 228 ms)
- Aerospike step: −21% (280 → 221 ms) — DB 호출이 wall time의 대부분을 차지하므로 client의 개선이 E2E 결과에 그대로 반영됨
- TPS: +17% (16.6 → 19.4 req/s)
- p99는 거의 같음(474 대 497 ms) — concurrency=5에서는 GIL 경합이 tail 차이를 만들 만큼 부하가 높지 않음
더 높은 concurrency 에서 backpressure 노출
Concurrency 10에서 200번 반복한 두 번째 run입니다. 같은 2026년 4월 artifact set을 사용했습니다.
| Client | total mean | p95 | TPS | errors |
|---|---|---|---|---|
| official | 465.81 | 885.64 | 20.9 | 0 |
| aerospike-py | 580.70 | 986.93 | 13.9 | 56 |
이 run에서는 test harness가 포화되어 request rejection 또는 timeout이 56건 발생했습니다. 공식 client는 같은 부하를 다른 방식으로 흡수했으므로 동등한 조건의 비교가 아닙니다. 이 결과는 backpressure를 적절히 조정하지 않으면 native async client가 server pool의 처리 한도를 넘는 in-flight 작업을 보낼 수 있음을 보여 줍니다. Bottleneck Analysis의 gather→single 방식으로 이 문제를 줄일 수 있습니다.
Environment C — uvicorn + DLRM (실 서빙)
측정 범위. 다음 production 형태의 전체 pipeline을 측정합니다.
HTTP request → key extraction → batch_read(9 sets × 200 keys)
→ feature build → DLRM inference (PyTorch CPU) → response
실제 recommendation serving pod와 가장 가까운 환경입니다.
Setup
| Item | Value |
|---|---|
| Source | 2026년 4월 내부 run (k6 runtime/client 비교; config "3.11 + GIL, stage OFF") |
| Image | aerospike-benchmark:latest (Python 3.11.14) |
| Loadgen | k6 10 VUs × 60s per scenario, 4 scenarios |
| Pods | replica 2, 4 CPU / 4 GiB request, 8 CPU / 8 GiB limit |
두 endpoint(/predict/official/sample, /predict/py-async/sample)는 같은 pod에서 실행됩니다. k6가 번갈아 호출하므로 server state와 네트워크 조건이 같습니다.
k6 client-side latency
single mode (10 VUs × 60s)
p95 aerospike-py ███████████████ 189 ms
official █████████████████████████ 324 ms −42% 🔥
p90 aerospike-py █████████████ 173 ms
official ██████████████████████ 293 ms −41% 🔥
avg aerospike-py █████████ 118 ms
official ████████████ 146 ms −19%
| Mode | aerospike-py p95 | official p95 | aerospike-py 우위 |
|---|---|---|---|
| single | 189 | 324 | −42% 🔥 |
| gather (9× fan-out) | 234 | 266 | −12% |
| merge_gather (aero-py only) | 202 | — | — |
| stress (0→20→50 VUs ramp) | 592 | — | — |
gather에서 차이가 줄어드는 이유는 두 client 모두 result 변환 단계에서 GIL 때문에 직렬화되기 때문입니다. 자세한 내용은 Bottleneck Analysis를 참고하세요.
Server-side Prometheus (동일 run, single mode)
| Metric | aerospike-py | official |
|---|---|---|
predict_duration_seconds p95 (FastAPI E2E) | 202 ms | 274 ms |
aerospike_batch_read_all_duration_seconds p95 | 137 ms | 252 ms |
Server-side와 client-side 수치는 같은 방향으로 변합니다. 둘 사이의 약 13 ms 차이는 network RTT, gateway, connection setup에서 발생합니다.
환경별 패턴
layer 가 추가될수록 → ratio 압축, tail 은 여전히 우위
───────────────── ────────── ──────────────────
A) Pure DB (HTTP/ML 없음) mean 4.8× p99 1.6×
B) uvicorn ASGI mean 1.27× ≈ noise at C=5
C) uvicorn + DLRM mean 1.24× p95 −42% 🔥
Database call 주변에 layer를 추가하면 mean ratio는 4.8×에서 1.24×로 줄어듭니다. 그러나 upper-percentile 우위는 유지됩니다. aerospike-py는 I/O 중에 GIL을 해제하지만 공식 client는 run_in_executor를 거치며 GIL 획득이 직렬화되어 tail latency가 커집니다.
Python 3.14t free-threaded — 같은 pipeline, GIL 제거
Python 3.14t는 GIL을 비활성화한 free-threaded CPython 빌드(Py_GIL_DISABLED=1)입니다. 아래 수치는 같은 Environment C pipeline(FastAPI + DLRM + k6 10 VUs × 60s)에서 runtime만 바꿔 측정했습니다. Rust와 C source는 변경하지 않았습니다.
TL;DR
p95 single-mode (k6 10 VUs × 60s, FastAPI + DLRM)
aerospike-py
3.11 + GIL ███████████████ 189 ms
3.14t ████████ 97 ms −49% 🔥
official (C extension)
3.11 + GIL █████████████████████████ 324 ms
3.14t ██████████ 128 ms −60% 🔥
Throughput (aerospike-py): 41.6 → 61.2 iter/s +47%
GIL은 두 client 모두의 병목이었습니다. aerospike-py는 Rust 코드를 바꾸지 않고도 성능이 개선되었습니다.
무엇이 빨라지는가 (그리고 왜)
- aerospike-py: stage breakdown
- official C client: 더 큰 ratio
부하를 준 상태에서 측정한 internal stage timing입니다. GIL의 영향을 받는 두 stage가 거의 0으로 줄었습니다.
Stage 3.11 + GIL 3.14t Change
────────────────────────────────────────────────────────────────────
spawn_blocking_delay 234 ms ████████ 0.12 ms −99.95% 🔥
event_loop_resume_delay 39.7 ms ██ ≈ 0 ≈ −100%
io (Aerospike network) 7.51 ms ▌ 1.27 ms −83%
merge_to_dict 4.48 ms ▎ 3.54 ms −21%
key_parse 967 μs · 1.06 ms +10% (noise)
tokio_schedule_delay 83.1 μs 49.5 μs −40%
limiter_wait 3.56 μs 0.96 μs −73%
개선 폭의 대부분은 다음 두 stage에서 나옵니다.
- **
spawn_blocking_delay**는 234 ms에서 0.12 ms로 줄었습니다. GIL 환경에서는 Rust async future가 끝난 뒤 결과를Py<...>객체로 바꾸는 작업을spawn_blockingworker에 전달합니다. Worker는 GIL을 얻어야 하므로 asyncio event loop와 다른 worker가 경합하면 queue 대기가 수백 ms까지 늘어납니다. GIL이 없으면 worker가 바로 실행됩니다. - **
event_loop_resume_delay**는 거의 0으로 줄었습니다. GIL 환경에서는 future가 끝나 event loop가 깨어난 뒤에도 coroutine이 GIL을 얻을 때까지 기다립니다. Free-threaded mode에서는 여러 coroutine을 병렬로 재개할 수 있습니다.
io가 8배 줄어든 것은 간접 효과입니다. Tokio worker가 Python 코드와 GIL을 두고 경쟁하지 않고 Aerospike protocol response를 parse할 수 있게 되었습니다.
공식 aerospike client는 synchronous C extension입니다. 애플리케이션에서는 이를 loop.run_in_executor(ThreadPoolExecutor, ...)로 감쌉니다. 각 request는 다음 단계를 거칩니다.
1. Thread-pool worker dispatch
2. C extension 이 인자 parse 하려 GIL 획득 ← GIL 하에서 직렬화
3. 네트워크 호출 (GIL release)
4. C extension 이 Python dict 빌드 위해 GIL 재획득 ← GIL 하에서 직렬화
GIL 경합이 있으면 2번과 4번이 모든 in-flight request에서 직렬로 실행됩니다.
GIL을 제거하면 이 단계들을 병렬로 실행할 수 있습니다. 공식 client의 p95는 60% 줄었습니다(324 → 128 ms). 기존 GIL 병목이 더 컸으므로 aerospike-py의 49%보다 감소 폭이 큽니다.
3.14t 의 same-workload 비교
Python 3.14t에서 두 client에 같은 server load를 적용했습니다. 같은 pod 안의 endpoint를 k6 10 VU가 번갈아 호출했습니다.
| Client | p95 (single mode) | Difference |
|---|---|---|
| aerospike-py | 126 ms | baseline |
| official aerospike | 128 ms | +2 ms (~1.5%, noise) |
Python 3.11 + GIL에서 42%였던 차이가 Python 3.14t에서는 약 2 ms로 줄었습니다.
이는 client 구조보다 GIL 경합이 기존 차이의 대부분을 만들었다는 점을 보여 줍니다.
Throughput (TPS)
3.11 + GIL, stage OFF ████████████ 41.6 baseline
3.11 + GIL, stage ON █████████████ 44.1 +6% (noise)
3.14t, aerospike-py only ███████████████████ 61.2 +47% 🔥
3.14t, both clients (split load) ██████████████ 47.3 +14%
| Config | iterations/s | http_reqs/s |
|---|---|---|
| 3.11 + GIL, stage OFF | 41.6 | 50.8 |
| 3.11 + GIL, stage ON | 44.1 | 52.9 |
| 3.14t, aerospike-py only | 61.2 | 80.0 |
| 3.14t, both clients (split load) | 47.3 | 59.8 |
그래도 solo load 에선 aerospike-py 가 여전히 우위 (3.14t)
126 ms와 128 ms가 나온 측정에서는 10 VU가 두 endpoint로 나뉘어 각 client가 약 5 effective VU만 처리했습니다. 부하를 한 client에 집중하면 차이가 다시 커집니다.
| Metric | aerospike-py solo | official solo | aerospike-py 우위 |
|---|---|---|---|
| k6 single p95 | 97 ms | 134 ms | −28% |
| k6 gather p95 (9× fan-out) | 107 ms | 253 ms | −58% 🔥 |
Server predict_duration_seconds p95 | 100 ms | 138 ms | −28% |
Server batch_read_all p95 | 64 ms | 67 ms | −4% |
| 이론 capacity (Little's Law: 10 VUs / p95) | ~103 req/s | ~75 req/s | +37% |
Python 3.14t의 solo load에서도 aerospike-py가 더 빠른 이유는 다음과 같습니다.
- Native async와 thread pool wrapper의 차이. GIL 경합이 없어도
run_in_executor는 request마다 thread pool 전환을 추가합니다. aerospike-py는 asyncio loop에서 직접 await합니다. - Lazy dict conversion.
batch_read()는LazyBatchRecordshandle을 반환합니다. Python dict는 caller가.to_dict()를 호출할 때 만들어집니다..to_numpy(dtype)는 GIL을 해제한 structured array 경로를 제공합니다. - FFI boundary를 한 번만 통과. aerospike-py의 Rust 코드는 PyO3 호출 한 번 안에서 전체
batch_read를 끝냅니다. C extension은 호출할 때마다 Python과 C의 경계를 여러 번 통과합니다.
Concurrency가 높아질수록 이 차이들이 누적됩니다. Gather 결과인 107 ms와 253 ms가 이를 가장 잘 보여 줍니다.
Migration notes
- CI matrix에 Python 3.14t를 추가합니다.
python:3.14.2t-slim에서 unit test와 integration test를 실행합니다. gil_used = false를 선언하기 전에 Rust의unsafe와 shared mutable state를 점검합니다.- 공식 client wheel을 기다리거나 직접 build합니다. PyPI에는 아직 공식
aerospike패키지의cp314twheel이 없지만 source build는 동작합니다.
부수 효과: inference도 빨라집니다. Python 3.14t에서 control variable인 DLRM inference(PyTorch CPU)가 43.5 ms에서 20.7 ms로 52% 줄었습니다. 이 개선은 aerospike-py와 무관합니다. Async I/O와 함께 실행되는 GIL-bound inference path라면 같은 이점을 얻을 수 있습니다.
다음 단계:
- Isolated Benchmark — FastAPI/JSON/inference 비용을 빼고 client 격차 자체 보기
- Bottleneck Analysis — per-stage profiling,
gather→singlerecipe (TODO: 새 harness 에서 재측정)