put_many io_uring write coalescing 계획 (Tier 1)
계기 — 팀원 질문: "write면 어차피 append일 텐데, 커맨드를 하나씩 던지는 batching이 드라마틱하게 좋냐? 차라리 merge해서 최대 IO 사이즈(MDTS)까지 채운 큰 IO 한 방이 NVMe 특성상 낫지 않냐." → batch io(#3636)는 submit/wait을 1회로 묶었지만 디바이스가 받는 커맨드는 여전히 2N개라는 정당한 지적. 그 위에 coalescing을 얹는 계획.
1. 코드 근거 (왜 지금 2N 커맨드인가)
- Python
_write_buffers가 넘기는[offset, buffer, len]항목 1개 = Rustbatched_write→build_and_submit_sqe의opcode::WriteSQE 1개 = NVMe write 커맨드 1개. - Rust엔 벡터드 쓰기(writev/Writev)가 없음 — 버퍼 1개당 커맨드 1개뿐.
_put_many_batch_io_chunk(#3636 브랜치core.py:1462)는 키마다 헤더 entry + 페이로드 entry를 각각 push → 2N entry = 2N 커맨드. submit/wait만 1회로 묶였을 뿐 커맨드 수는 그대로.
결정적 사실 — 키 내부 헤더+페이로드는 항상 인접
_encode_header가bytearray(self.header_bytes)를 만들고hdr_total = round_up(header_bytes, block_align) == header_bytes(header_bytes가 block_align 배수라 그대로).- 헤더는
[offset, offset+header_bytes), 페이로드는 정확히offset+header_bytes에서 시작. - → 슬롯 연속성과 무관하게, 한 키의 헤더+페이로드는 디바이스에서 물리적으로 붙어 있다. 이게 Tier 1을 "append 가정 없이 항상 유효"하게 만드는 근거.
2. 두 단계 coalescing (Tier 1만 채택)
| Tier 1 (키 내부) — 채택 | Tier 2 (키 사이) — 보류 | |
|---|---|---|
| 합치는 대상 | 한 키의 헤더+페이로드 (2→1 커맨드) | 연속 슬롯의 인접 키들 |
| 전제 | 항상 성립 (§1 근거) | 슬롯 연속(cold-fill/append) + 슬롯 내 gap 처리 |
| 효과 | 커맨드 2N→N 무조건 | N → ceil(연속구간/MDTS), append일 때 극대 |
| 질문 대응 | "merge 했다" — append 가정 불필요 | "큰 IO 한 방"의 정답이나 조건부 |
| 리스크 | 낮음 | 높음(gap·MDTS·정렬) |
스코프 결정(사용자 확정): Tier 1까지만. Tier 2는 Step 3 실측이 "커맨드 수가 병목"임을 입증할 때만 별도 검토.
공통 의존성 — Rust 벡터드 쓰기
페이로드가 크므로(수십~수백 KB) 메모리 복사로 헤더+페이로드를 합치면 손해. 복사 없이
흩어진 버퍼를 한 디바이스 오프셋에 연속 기록하는 opcode::Writev(iovec 그룹) 필요.
→ Python 단독 불가, Rust+Python 변경.
조정 필요:
iouring-batch-follow-up-claude.mdPart B도batched_write시그니처를 확장(payload_lens추가, O_DIRECT 패딩 bounce용)한다. 본 작업은 iovec 그룹(writev) 추가로 목적이 다름. 두 확장이 같은 Rust 함수에 닿으므로 머지 순서/시그니처를 함께 설계할 것 (예:batched_writev(offsets, buffer_groups, payload_lens, total_lens)통합안 검토).
3. 계획 (TDD · PR 분리)
전제: #3636 미머지 → 머지 후 dev 기준 또는 그 브랜치 위 스택.
- Step 0 — 측정 스파이크 (go/no-go). 실 NVMe에서 (a) batch io의 2N 커맨드가 정말 병목인지 (QD·커맨드율 관찰), (b) 디바이스 MDTS 조회. §6 미검증 상태를 먼저 깬다. 검증: N=100에서 커맨드 감소가 유의미할 여지 수치 확인. 없으면 중단.
- Step 1 — Rust 벡터드 쓰기 primitive.
opcode::Writev(또는 fixed-buffer 버전) + iovec 그룹 받는 API. 검증: Rust 단위테스트 — 인접 오프셋 2버퍼가 단일 커맨드로 기록·라운드트립. - Step 2 — Python Tier 1 coalescing.
_put_many_batch_io_chunk버퍼 빌드에서 키마다 헤더+페이로드를 iovec 그룹 1개로 묶음. TDD: fake device 계약 테스트 — N키 put_many가batched_write1회·커맨드 N개(2N 아님), 라운드트립·all-or-nothing 롤백·부분슬롯 시맨틱 유지. (_MAX_PUT_MANY_IO_URING_BATCH_KEYS128엔트리 캡도 N엔트리 기준으로 완화 가능.) - Step 3 — 실 NVMe 실측. 커맨드 반감이 WAF/throughput/QD에 주는 영향. 검증: §6 판정 기준 (N=100에서 순차 대비 >10% & QD 상승).
4. Out of scope
- Tier 2 cross-key merge (Step 3 결과로 정당화 시 별도).
- O_DIRECT 패딩 batched 지원 (Part B follow-up 소관 — 단 §2 조정 항목 참조).
- 읽기 경로(
_read_buffers) coalescing.