본문으로 건너뛰기

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개 = Rust batched_writebuild_and_submit_sqeopcode::Write SQE 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_headerbytearray(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.md Part 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_write 1회·커맨드 N개(2N 아님), 라운드트립·all-or-nothing 롤백·부분슬롯 시맨틱 유지. (_MAX_PUT_MANY_IO_URING_BATCH_KEYS 128엔트리 캡도 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.