P0 — put_many 내부 병렬 I/O (batched_write N-SQE)
한 줄 요약: PR #3274(io_uring) 머지 후,
put_many내부 N-key 순차 루프를 단일batched_write(N offsets, N buffers)호출로 교체하여 NVMe NCQ 병렬성 활성화. L2(batched_submit_put_task단일 dispatch)와 함께 한 PR로 제출 예정.
1. 배경
1.1 현재 put_many 구조 (PR #3274 이후에도)
PR #3274가 io_uring 인프라(_write_buffers/_read_buffers dispatcher)를 도입하지만,
put_many 내부 루프는 여전히 키 1개씩 순차 처리:
# core.py put_many 내부 (단순화)
for spec, obj in zip(specs, objs):
slot = self._allocate(spec) # _put_lock 획득/해제
self._write_one(slot, spec, obj) # io_uring: write_uring × 2 (header + payload)
self._commit(slot, spec) # _put_lock 획득/해제
_write_one은 can_batch=False(header=32B, O_DIRECT 요구=4096B 불일치) → 순차.
체크포인트 write만 batched_write로 진짜 배치 처리됨 (io_uring_post_pr3274.md §2-2).
1.2 L2와의 관계 (벤치마크 근거)
L2(batched_submit_put_task 단일 dispatch, branch perf/legacy_batch_put)는
plugin → core 호출을 N→1로 줄인다. 올바른 구조이나, core 내부가 순차이면
fanout의 thread pool 병렬성이 dispatch 절약을 압도 → 실 스토리지에서 regression.
벤치마크 결과 (2026-06-08, bench_dispatch_patterns.py):
| lat | N=10 | N=100 | N=1000 |
|---|---|---|---|
| 0µs (순수 CPU) | batch 85% 빠름 ✓ | batch 85% 빠름 ✓ | batch 93% 빠름 ✓ |
| 50µs (NVMe 현실) | fanout 110% 빠름 ! | fanout 60% 빠름 ! | ~equal |
| 200µs (느린 NVMe) | fanout 395% 빠름 ! | fanout 44% 빠름 ! | fanout 126% 빠름 ! |
근본 원인: batch = 1 to_thread에서 N key 순차 write. fanout = N to_thread가 thread pool 동시 실행.
P0 구현 후 예상:
- L2: dispatch 1회 (overhead N→1)
- P0: N SQE 한 번에 submit → NCQ가 N개 동시 처리 → lat=50µs 구간에서도 batch가 fanout 대비 이득
2. 설계 방향
2.1 목표 구조
# 목표 core.py put_many (단순화)
# Phase 1: 전체 N 슬롯 할당 (단일 lock 구간)
slots = self._allocate_many(specs) # _put_lock 한 번, N 슬롯 예약
# Phase 2: N-SQE 배치 제출 (NVMe NCQ 활용)
offsets = [self._slot_offset(s) for s in slots]
buffers = [self._prepare_combined_buffer(spec, obj) for spec, obj in zip(specs, objs)]
lens = [len(b) for b in buffers]
batch_id = self._raw_dev.batched_write(offsets, buffers, lens)
self._raw_dev.wait_iouring(batch_id) # 모든 CQE 수거
# Phase 3: 일괄 commit
self._commit_many(slots, specs) # _put_lock 한 번
2.2 현재 대비 변경 요약
| 항목 | 현재 (PR #3274 이후) | 목표 |
|---|---|---|
| 슬롯 할당 | key별 _put_lock 획득 N회 | _allocate_many — lock 1회 |
| I/O 제출 | _write_one N회 (2 write_uring per key) | batched_write([N]) — SQE 2N개를 한 번에 submit |
| NVMe NCQ | 활용 안 됨 (순차) | N개 동시 전송 가능 |
| commit | key별 _put_lock N회 | _commit_many — lock 1회 |
2.3 header 정렬 문제 (can_batch=False 원인)
현재 _write_one이 can_batch=False인 이유:
- header = 32B, payload는 블록 정렬됨
- O_DIRECT는 모든 버퍼가 block_align(≥512B, 실제 4096B) 배수여야 함
- header 단독 SQE는 정렬 불만족 →
batched_write거부 (Rust에서 ValueError)
해결 방향 비교:
| 방안 | 설명 | 하위호환 | 비고 |
|---|---|---|---|
| (b) 합산 버퍼 | header + payload를 정렬된 단일 버퍼에 합산 → SQE 1개 | ✅ 슬롯 레이아웃 불변 | zero-copy 뷰 재설계 필요 |
| (a) header 패딩 | header 슬롯을 4096B로 확장 | ❌ 포맷 변경 | 마이그레이션 필요 |
| (c) buffered 모드 | header만 O_DIRECT 없이 write | 중간 | io_uring buffered write 지원 여부 확인 필요 |
권장: (b) 합산 버퍼 — 슬롯 레이아웃 변경 없음, Rust batched_write 재사용 가능.
_build_direct_odirect_view의 zero-copy 경로와 정합성 별도 검토 필요.
2.4 부분 실패 처리
batched_write 중 일부 SQE 실패 시 CQE에 per-SQE error code 반환.
→ 결과 비트맵(list[bool])으로 변환, 현재 put_many.results 인터페이스 유지.
3. 구현 내용 (2026-06-08 완료)
브랜치: perf/l2-p0-batch-put (ankit-sam/raw_iouring_cmd = PR #3274 기반)
3.1 put_many 분기 (core.py)
def put_many(self, keys, objs):
# 검증 ...
if self.io_engine == "io_uring" and len(keys) > 1:
return self._put_many_batch_io(keys, objs) # ← 신설 batch 경로
# 기존 순차 루프 (posix 또는 단일 key) — 한 줄도 변경 안 함
for i, (key, obj) in enumerate(...):
...
분기 조건 = io_engine=="io_uring" AND len(keys)>1. posix는 thread pool
병렬성이 이미 잘 작동하므로 batch로 빼지 않음 (벤치마크가 posix batch는
regression임을 보여줌).
3.2 _put_many_batch_io 4단계 (신설)
- Phase A (단일
_lock): N개 키에 대해 index/inflight dedup →_allocate_slot_locked로 N슬롯 할당 →_InflightN개 등록. write_plan 구성. - Phase B+C: write_plan의 각 키마다 header + payload 버퍼를 모아 2N 길이
리스트 구성 →
_write_buffers(all_offsets, all_bufs, ...)1회 호출. 내부에서can_batch=True면batched_write([2N])→ 1io_uring_submit→ NCQ. - Phase D (단일
_lock): write_plan 전체를_index에 일괄 commit,_inflight정리. - 예외 처리:
_write_buffers실패 시 except 블록에서 write_plan 전체를_inflight.pop+_append_free_slot_locked롤백 (all-or-nothing).
3.3 테스트 (tests/v1/storage_backend/test_raw_block_core.py)
posix core에서 _put_many_batch_io를 직접 호출하여 Phase A/D 정합성 검증
(io_uring 실장 비의존). 3개 모두 통과:
test_put_many_batch_io_round_trip— N=10 write→load round-triptest_put_many_batch_io_skips_already_indexed— 중복 5 + 신규 5, stored_keys 신규만test_put_many_batch_io_write_failure_rolls_back—_write_buffersmock raise → 슬롯 전량 반환,_inflight비었는지 확인
기존 회귀: test_raw_block_core.py + test_rust_raw_block_backend.py 34개 통과.
(GDS·uring_cmd_get_nvme_info 2건 실패는 실 NVMe 권한 문제로 무관)
4. 성능 검증 — 시뮬레이션 (2026-06-08)
스크립트: benchmarks/storage_backend_io/bench_dispatch_patterns.py (4-way 확장)
환경: fake in-memory device, Python 3.12, warmup=3, iters=10
4.1 결과
4-way dispatch benchmark obj=64B
N lat µs fanout batch/posix batch+P0 P0 vs fanout
10 0 0.750 ms 0.276 ms 0.330 ms -55.9% ✓
10 50 1.791 ms 3.566 ms 2.303 ms +28.6%
10 200 6.112 ms 6.476 ms 4.027 ms -34.1% ✓
100 0 6.957 ms 0.919 ms 0.915 ms -86.8% ✓
100 50 22.672 ms 27.038 ms 16.240 ms -28.4% ✓
100 200 43.198 ms 57.953 ms 31.163 ms -27.9% ✓
1000 0 115.275 ms 7.859 ms 7.993 ms -93.1% ✓
1000 50 321.555 ms 265.765 ms 164.061 ms -49.0% ✓
1000 200 232.541 ms 579.574 ms 309.628 ms +33.2%
4.2 해석
- N=100, lat=50µs (NVMe 현실): batch/posix는 fanout 대비 +19% regression (L2 단독 한계 재확인), batch+P0는 fanout 대비 −28% → P0가 L2 regression을 뒤집고 fanout보다 빠름.
- N=100, lat=200µs: batch+P0 −28%, 일관되게 우위.
- N=1000, lat=200µs: batch+P0가 fanout보다 +33% 느림 — Phase A 단일 lock 구간에서 1000개 슬롯 순차 할당이 직렬화 병목. 큰 배치에서 Phase A 최적화 여지 있음 (후속).
4.3 ⚠️ 시뮬레이션의 결정적 한계 (반드시 인지)
이 벤치마크는 NCQ 병렬성을 모델 가정으로 박아넣었다:
def batched_write(self, offsets, buffers, total_lens):
if self._latency_s > 0:
time.sleep(self._latency_s) # ← N개를 latency 1회만 대기
# = "NVMe가 N개를 완벽 병렬 처리한다"를 전제
즉 "P0가 NCQ로 빨라진다"는 결론을 전제로 깔고 측정한 것이라 논리적으로 순환이다. 이 벤치마크가 증명하는 것은:
- ✅ P0 구조가 N-SQE batch를 한 번에 submit할 수 있는 형태임
- ✅ dispatch/lock overhead 측면에서 batch가 fanout 대비 유리할 여지
- ❌ 실제 NVMe에서 NCQ가 정말 병렬화하는지 — 증명 못 함
검증 안 된 실제 변수: io_uring submit 오버헤드, Rust 2N 버퍼 준비 비용,
NVMe 큐 깊이·칩 수에 따른 실 병렬도, wait_iouring 완료 수거 비용.
5. 실측 검증 계획 (미실행)
5.1 필요 환경
io_engine="io_uring"을 실제로 태우려면 실 NVMe block device + io_uring
지원 커널(5.19+). fake device로는 검증 불가 (ci_test_coverage.md §4-4:
io_uring은 CI best-effort, 실장 필수).
5.2 측정 스크립트 (작성 필요)
benchmarks/storage_backend_io/bench_put_many_real_nvme.py (신규):
- 실 device path 인자 (
/dev/nvmeXnY또는 테스트 파일 on NVMe FS) io_engine="io_uring",use_odirect=True로 RawBlockCore 생성- 비교: (a) 기존 순차
put_many(분기 우회 플래그) vs (b)_put_many_batch_io - 그리드: N=[10, 100, 1000], payload=[4KB, 64KB, 256KB] (실 KV 청크 크기)
- 측정: wall-clock, p50/p95/p99,
iostat로 device QD(큐 깊이) 동시 관찰
5.3 판정 기준
- N=100, 실 NVMe에서 batch+P0가 순차 대비 >10% 빠르면 의미 있음 → PR 진행
- iostat에서 batch 시 평균 QD가 순차 대비 상승 확인 (NCQ 활용 증거)
- 차이 <5%면: 구조는 맞으나 실효 없음 → L2 correctness fix만 살리고 P0 보류
5.4 실행 체크리스트
- 실 NVMe device 확보 (Samsung SSD 평가 장비?)
-
bench_put_many_real_nvme.py작성 - 순차/batch 분기 우회 플래그 또는 두 코어 인스턴스
- 5.3 기준으로 판정 → 노트 §6 갱신
6. 실측 결과 (대기 중)
실 NVMe 측정 후 기록.
7. PR 구성 계획
L2(correctness) + P0를 단일 PR로 제출 (단, P0는 실측 검증 후).
| 변경 | 파일 | 내용 | 상태 |
|---|---|---|---|
| L2 | rust_raw_block_backend.py | dedup 배치화 + F1/F2/F3 fix | 구현 완료 (커밋됨) |
| P0 | core.py | _put_many_batch_io N-SQE batch | 구현 완료, 실측 미검증 |
| 테스트 | test_raw_block_core.py | P0 correctness 3개 | 통과 |
| 벤치 | bench_dispatch_patterns.py | 4-way 시뮬레이션 | 완료 (한계 있음) |
단독 제출하지 않는 이유: L2만 머지하면 NVMe 구간 regression. P0 합산 시 dispatch 절약 + NCQ 병렬성 동시 작동 (시뮬레이션 기준). 단 실측 통과가 전제.
8. 변경 로그
| 일자 | 작성자 | 내용 |
|---|---|---|
| 2026-06-08 | ny | 초안 작성. L2 벤치마크 결과(NVMe 구간 regression) + io_uring 분석 후 방향 결정. L2와 묶어 단일 PR 계획 수립. |
| 2026-06-08 | ny | P0 구현 완료 (_put_many_batch_io, branch perf/l2-p0-batch-put). correctness 테스트 3개 통과. 4-way 시뮬레이션: N=100/lat=50µs에서 batch+P0가 fanout −28%. 단 NCQ 병렬성을 모델 가정으로 깐 순환 측정 — 실 NVMe 검증 필요. §5 실측 계획 수립. |