본문으로 건너뛰기

raw_block io_uring I/O 인프라

TL;DR — raw_block backend의 모든 device I/O가 단일 dispatcher (_write_buffers / _read_buffers)를 통과하도록 정리되었고, io_uring 엔진에서는 여러 버퍼를 한 번의 batched_write로 묶어 제출할 수 있는 경로가 생겼다. 단, can_batch 조건(모든 버퍼가 payload_len == total_len) 때문에 슬롯 헤더+페이로드 쌍을 쓰는 일반 write 경로는 아직 batch를 타지 못하고 개별 write_uring으로 떨어진다. 인프라는 깔렸으나 쓰기 루프는 여전히 키 1개씩 처리된다.

항목
종류I/O 인프라 (io_uring 엔진 통합)
영역lmcache/v1/storage_backend/raw_block/core.py, rust/raw_block/src/lib.rs
핵심 추가_write_buffers/_read_buffers dispatcher, batched_write/wait_iouring, fixed buffer 등록
커널 요건io_uring 사용 시 5.19+ (Big SQE/CQE ABI)
이 문서의 역할raw_block put_many 쓰기 경로 최적화의 전제

1. 배경 — 직접 호출에서 dispatcher로

이전에는 모든 I/O 호출자(슬롯 write, 다중 로드, 메타데이터 검증, 체크포인트 R/W)가 Rust 바인딩의 raw_dev.pwrite_from_buffer(...) / raw_dev.pread_into(...)직접 호출했다. POSIX든 io_uring이든 차이 없이 "동기 한 건"이었다.

io_uring 엔진을 제대로 태우려면 호출 지점마다 엔진 분기를 넣어야 하는데, 그러면 분기가 코드 전체에 흩어진다. 그래서 단일 진입점을 도입했다.


2. 단일 dispatcher: _write_buffers / _read_buffers

모든 I/O가 아래 두 함수를 통과한다.

호출자 (슬롯 write / 다중 load / 메타 검증 / 체크포인트 R/W)


_write_buffers(offsets, buffers, payload_lens, total_lens)
_read_buffers (offsets, buffers, payload_lens, total_lens)

├─ io_engine != "io_uring":
│ └─ 버퍼마다: pwrite_from_buffer / pread_into (POSIX, 기존 동작)

├─ io_engine == "io_uring" + uring_cmd 모드:
│ └─ NVMe passthrough 경로 (MDTS 단위 chunk split)

└─ io_engine == "io_uring" (일반 block 모드):
├─ can_batch: batched_write / batched_read + wait_iouring
└─ else: write_uring / read_uring (동기 한 건씩)

인자는 모두 병렬 리스트다. offsets[i], buffers[i], payload_lens[i], total_lens[i]가 i번째 I/O 한 건을 기술한다. 리스트에 N개를 담아 한 번 호출하면 dispatcher가 엔진에 맞게 N건을 처리한다.

  • payload_len — 실제 의미 있는 바이트 수
  • total_len — 디바이스에 쓰는 전체 길이 (O_DIRECT 정렬 round-up 포함)

3. can_batch — batch 제출의 핵심 조건

io_uring 일반 모드에서 dispatcher는 다음을 판정한다.

can_batch = all(payload_len == total_len for ... in zip(payload_lens, total_lens))

모든 버퍼에 대해 payload_len == total_len일 때만 batched_write를 쓴다. 이유:

  • batched_write는 single-shot SQE 묶음이다. 한 SQE = 한 (offset, buffer, len). 버퍼 내부의 padding이나 short-write 개념이 없다.
  • 정렬이 안 맞는 버퍼를 넘기면 Rust 측에서 즉시 ValueError (bounce buffer를 쓰지 않음).

그래서 can_batch=True인 실질적 조건은 **"패딩이 필요 없는, 이미 정렬된 버퍼만 있을 때"**이다.

3.1 왜 슬롯 헤더+페이로드 쌍은 batch를 못 타는가

raw_block 슬롯 하나를 쓰려면 헤더와 페이로드 두 버퍼를 쓴다.

버퍼payload_lentotal_len정렬
헤더32B (실데이터)4096B (O_DIRECT round-up)불일치
페이로드정렬됨정렬됨일치

헤더는 실제 32바이트인데 O_DIRECT는 4096B 정렬을 요구한다 → round_up(32, 4096) = 4096payload_len(32) ≠ total_len(4096).

헤더+페이로드를 한 리스트로 묶으면 헤더 때문에 can_batch=False개별 write_uring 두 번으로 떨어진다. ring을 한 단계 더 우회할 뿐 제출 횟수는 그대로다.

3.2 호출자별 현황

호출자리스트 길이can_batch실제 경로
슬롯 write (쓰기 루프 안)2 (헤더+페이로드)False (헤더 정렬 불일치)write_uring × 2
다중 load1 (페이로드)Truebatched_read([1])
메타데이터 검증1 (헤더, header_bytes == block_align)Truebatched_read([1])
체크포인트 read1Truebatched_read([1])
체크포인트 write2 (둘 다 block_align 배수)Truebatched_write([2])

→ 현재 진짜 N-건 batch 제출의 의미가 있는 곳은 체크포인트 write 정도이고, 나머지는 길이 1~2짜리라 사실상 single이다. 특히 다중 키 쓰기 루프는 키마다 write_uring 2회라 device 큐를 채우지 못한다. 이 한계가 put_many 쓰기 경로 최적화의 출발점이다.


4. Rust 측 구성

4.1 IoUringWrapper — 커널 호환 dual ring

io_uring ring을 두 종류로 들고 있다.

모드SQE / CQE 크기용도
Standard64B / 16B일반 read/write
Big128B / 32BNVMe uring_cmd(passthrough). 128B SQE 필요

커널 5.19+에서만 Big ring(Big SQE/CQE ABI)이 가능하다. passthrough를 쓰려면 ring을 Big으로 만들어야 cmd 경로도 제출할 수 있다.

4.2 batch 제출 API

fn batched_write(offsets: Vec<u64>, buffers: Vec<PyAny>, total_lens: Vec<usize>) -> u64
fn wait_iouring(batch_id: u64)
  • batched_write는 N개의 (offset, buffer, len)을 받아 한 번의 io_uring_submit으로 N개 SQE를 제출하고 batch_id를 반환한다.
  • wait_iouring(batch_id)는 그 배치의 모든 완료(CQE)를 수거할 때까지 대기한다.
  • batch_id별로 in-flight 카운트와 완료 핸들러를 따로 추적한다.

이렇게 N개를 한 번에 제출하면 디바이스(NVMe NCQ)가 여러 명령을 병렬로 처리할 수 있다. 반대로 한 건씩 write_uring → 완료 대기를 N번 반복하면 직렬화된다.

4.3 fixed buffer 등록

register_fixed_buffers_from_allocator(allocator)는 메모리 할당자의 페이지 버퍼를 io_uring에 미리 등록(register_buffers)해서 zero-copy I/O를 가능하게 한다. 할당자가 해당 메서드를 노출하지 않으면 경고 후 non-fixed 모드로 fallback한다.


5. 정리

무엇이 생겼나상태
단일 I/O dispatcher (_write_buffers/_read_buffers)✅ 모든 경로가 통과
io_uring N-건 batch 제출 (batched_write + wait_iouring)✅ API 존재
zero-copy fixed buffer 등록
NVMe passthrough(uring_cmd) 경로✅ (Big ring)
다중 키 쓰기에서 N-건 batch 활용❌ 슬롯 헤더 정렬로 can_batch=False, 키마다 개별 제출

인프라는 준비됐지만 가장 빈번한 경로 중 하나인 다중 키 쓰기는 아직 batch를 활용하지 못한다. 이를 활성화하는 작업이 raw_block put_many 쓰기 경로 최적화다.


6. 참고

  • 커널 요건: io_uring 5.19+ (Big SQE/CQE), uring_cmd passthrough도 동일
  • O_DIRECT 정렬: 모든 버퍼가 block_align(통상 4096B) 배수여야 batch 제출 가능
  • 후속 문서: raw_block put_many 쓰기 경로 최적화