본문으로 건너뛰기

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_onecan_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):

latN=10N=100N=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개 동시 전송 가능
commitkey별 _put_lock N회_commit_many — lock 1회

2.3 header 정렬 문제 (can_batch=False 원인)

현재 _write_onecan_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슬롯 할당 → _Inflight N개 등록. write_plan 구성.
  • Phase B+C: write_plan의 각 키마다 header + payload 버퍼를 모아 2N 길이 리스트 구성 → _write_buffers(all_offsets, all_bufs, ...) 1회 호출. 내부에서 can_batch=Truebatched_write([2N]) → 1 io_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-trip
  • test_put_many_batch_io_skips_already_indexed — 중복 5 + 신규 5, stored_keys 신규만
  • test_put_many_batch_io_write_failure_rolls_back_write_buffers mock 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는 실측 검증 후).

변경파일내용상태
L2rust_raw_block_backend.pydedup 배치화 + F1/F2/F3 fix구현 완료 (커밋됨)
P0core.py_put_many_batch_io N-SQE batch구현 완료, 실측 미검증
테스트test_raw_block_core.pyP0 correctness 3개통과
벤치bench_dispatch_patterns.py4-way 시뮬레이션완료 (한계 있음)

단독 제출하지 않는 이유: L2만 머지하면 NVMe 구간 regression. P0 합산 시 dispatch 절약 + NCQ 병렬성 동시 작동 (시뮬레이션 기준). 단 실측 통과가 전제.


8. 변경 로그

일자작성자내용
2026-06-08ny초안 작성. L2 벤치마크 결과(NVMe 구간 regression) + io_uring 분석 후 방향 결정. L2와 묶어 단일 PR 계획 수립.
2026-06-08nyP0 구현 완료 (_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 실측 계획 수립.