본문으로 건너뛰기

io_uring batched padded write 후속 PR 리뷰

요약

perf/iouring-batched-padded-write는 PR #3636에서 남아 있던 regular io_uring padded O_DIRECT write의 per-entry write_uring() fallback을 없애고, batched_write()payload_lens를 받아 padded write도 batch submit으로 처리하게 하는 후속 작업이다.

초기 리뷰에서 확인한 핵심은 payload_lens API 방향은 맞지만, regular io_uring write 계열의 padding semantics가 path별로 파편화될 수 있다는 점이었다. 최종 구현 기록은 별도 문서인 io_uring_padded_write_semantics_implementation.md에 정리했다.

배경: PR #3636에서 무엇이 batch였나

PR #3636에서 NVMe passthrough만 batch였던 것은 아니다. 기준 동작은 다음과 같다.

경로PR #3636 동작설명
POSIXbatch 아님pwrite_from_buffer()를 entry별로 호출
regular io_uring, non-paddedbatchpayload_len == total_len이면 batched_write() 사용
regular io_uring, padded O_DIRECTbatch 아님payload_len < total_len이면 per-entry write_uring() fallback
NVMe passthrough (io_uring_cmd)batch_write_uring_cmd_buffers()가 chunk를 만들고 batched_write() 호출

PR #3636의 빈틈은 regular io_uring 전체가 batch가 아니었다는 점이 아니라, regular io_uring에서 padded O_DIRECT write만 batch path를 못 탔다는 점이다.

당시 batched_write() API는 다음 형태였다.

batched_write(offsets, buffers, total_lens)

이 API는 source buffer에서 total_len만큼 읽는다고 가정했다. 하지만 padded write는 payload_lentotal_len이 다르다.

payload_len = 실제 payload byte 수
total_len = O_DIRECT alignment 때문에 올림된 물리 I/O byte 수
padding = [payload_len, total_len)

O_DIRECT padding은 I/O 길이를 block size에 맞춘다는 뜻이지, 커널이 padding bytes를 자동으로 zero-fill한다는 뜻이 아니다. total_len으로 submit하면 커널은 source pointer에서 total_len bytes를 읽는다. 따라서 userspace가 padding 영역을 명확히 준비해야 한다.

리뷰 관점의 핵심 판단

  1. payload_lens 추가는 필요하다.

    • 기존 batched_write(offsets, buffers, total_lens)는 padded write에서 source 유효 길이와 물리 I/O 길이를 구분하지 못했다.
    • payload_lens가 있어야 payload_len < total_len인 write를 out-of-bounds read 없이 batch로 표현할 수 있다.
  2. regular io_uring serial/batched path의 padding 의미를 통일해야 한다.

    • serial write_uring()payload_len/total_len을 받을 수 있었지만, cap >= total_len이면 caller tail을 그대로 쓰는 구조였다.
    • batched path만 별도 zero-fill 규칙을 갖게 만들면 io_uring write semantics가 더 난잡해진다.
  3. zero-copy/fixed-buffer 이점은 최대한 유지해야 한다.

    • payload_len < total_len이라고 항상 bounce하면 안전하지만 성능상 둔하다.
    • tail이 이미 zero라면 direct/fixed-buffer path를 유지하고, tail이 non-zero이거나 source가 짧거나 O_DIRECT alignment가 맞지 않을 때만 bounce하는 절충안이 낫다.

변경 전후 흐름

PR #3636 regular io_uring padded write

RawBlockCore.put_many()
-> _prepare_write_payload()
payload_len < total_len
-> _write_buffers()
can_batch = False
-> for each entry:
raw_dev.write_uring(offset, buf, payload_len, total_len)

여러 entry가 있어도 per-entry submit path로 빠졌다.

후속 PR 최종 흐름

RawBlockCore.put_many()
-> _prepare_write_payload()
payload_len, total_len 준비
-> _write_buffers()
-> raw_dev.batched_write(offsets, buffers, total_lens, payload_lens)
-> Rust common helper:
tail zero면 direct/fixed-buffer
tail non-zero/source short/unaligned이면 bounce + zero-fill
-> batch submit

리뷰에서 요구한 보완 방향

  • batched_write() stale-tail regression을 추가한다.
    • source buffer가 total_len만큼 크고 tail이 non-zero인 경우를 검증한다.
    • readback tail은 zero여야 한다.
  • serial write_uring()에도 같은 stale-tail regression을 추가한다.
    • serial/batched regular io_uring write가 같은 semantics를 가져야 한다.
  • payload_lens validation regression을 추가한다.
    • length mismatch
    • payload_len > total_len
    • cap < payload_len
  • io_uring_cmd path는 변경하지 않는다.
    • NVMe max transfer size chunking과 command alignment 제약이 있으므로 이번 변경의 대상이 아니다.

후속 구현 문서

실제 구현 내용, regression 테스트, 검증 결과는 io_uring_padded_write_semantics_implementation.md에 정리했다.

요약하면 최종 구현은 다음 정책을 따른다.

regular io_uring write는 padding tail을 zero로 기록한다.
tail이 이미 zero면 direct/fixed-buffer path를 유지한다.
tail이 non-zero/source short/unaligned이면 bounce + zero-fill한다.
serial write_uring()과 batched_write()는 같은 helper를 공유한다.