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_len | total_len | 정렬 |
|---|---|---|---|
| 헤더 | 32B (실데이터) | 4096B (O_DIRECT round-up) | 불일치 |
| 페이로드 | 정렬됨 | 정렬됨 | 일치 |
헤더는 실제 32바이트인데 O_DIRECT는 4096B 정렬을 요구한다 →
round_up(32, 4096) = 4096 → payload_len(32) ≠ total_len(4096).
헤더+페이로드를 한 리스트로 묶으면 헤더 때문에 can_batch=False →
개별 write_uring 두 번으로 떨어진다. ring을 한 단계 더 우회할 뿐 제출 횟수는
그대로다.
3.2 호출자별 현황
| 호출자 | 리스트 길이 | can_batch | 실제 경로 |
|---|---|---|---|
| 슬롯 write (쓰기 루프 안) | 2 (헤더+페이로드) | False (헤더 정렬 불일치) | write_uring × 2 |
| 다중 load | 1 (페이로드) | True | batched_read([1]) |
| 메타데이터 검증 | 1 (헤더, header_bytes == block_align) | True | batched_read([1]) |
| 체크포인트 read | 1 | True | batched_read([1]) |
| 체크포인트 write | 2 (둘 다 block_align 배수) | True | batched_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 크기 | 용도 |
|---|---|---|
| Standard | 64B / 16B | 일반 read/write |
| Big | 128B / 32B | NVMe 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_cmdpassthrough도 동일 - O_DIRECT 정렬: 모든 버퍼가
block_align(통상 4096B) 배수여야 batch 제출 가능 - 후속 문서: raw_block put_many 쓰기 경로 최적화