본문으로 건너뛰기

PR #3274 vs raw-block-perf-findings.md

상태: PR #3274 (dev 대비 4 commits, +2029/-289, 8 files) 분석. raw-block-perf-findings.md 의 개선 포인트가 PR 머지 후 어떻게 달라지는지 정리. PR 본문은 private/_archive/pr3274_diffs/ 에 저장된 raw diff 기반.


1. PR #3274 가 하는 일 (요약)

영역변경
새 모드use_uring_cmd=True (NVMe passthrough) 추가. 기존 use_iouring 와 직교 (block-level vs cmd-level).
Rust ring 추상화IoUring<SqueueEntry, Entry>IoUringWrapper { Standard | Big } 로 감쌈. Big = Entry128/Entry32 (kernel 5.19+ 필수). cmd 모드는 Big 만, 일반 io_uring 은 fallback 으로 Standard 사용.
NVMe 헬퍼nvme_identify_ns, nvme_get_nsid_from_fd, nvme_get_lba_shift, nvme_get_lba_size, nvme_uring_cmd_prep 추가. character device (/dev/ng<C>n<N>) 만 허용.
새 opcode 경로IoSubmissionnvme_cmd_data: Option<NvmeCmdData> 추가. build_and_submit_sqe 가 cmd_data 가 있으면 opcode::UringCmd80::new(fd, NVME_URING_CMD_IO).cmd(80B) 로, 없으면 기존 Read/Write/ReadFixed/WriteFixed 분기.
공통 helper 추출워커 루프의 4 곳 중복 코드를 copy_from_bounce_buffer / handle_completion_result / decrement_in_flight / build_and_submit_sqe 로 추출.
새 sync APIRust 에 write_uring(offset, data, payload, total) + read_uring (이미 있던 듯) 추가 — checkpoint 경로용 동기 io_uring 쓰기.
Python _write_buffers/_read_bufferscore.py모든 I/O 가 통과하는 단일 dispatcher 도입. posix → 루프, io_uring (block) → batched_*, io_uring_cmd → chunked read_uring/batched_write (MDTS 기반 split).
register_fixed_buffers_from_allocatorRawBlockCore 에 추가, allocator 의 get_paged_buffers() 결과를 등록. plugin 에서 자동 호출.
put_objs 배치 경로rust_raw_block_backend.py 가 io_uring 모드일 때 _submit_put_many 한 번으로 모음 (기존엔 키마다 _submit_put_one).
MDTS 자동 검출/sys/block/nvme<C>n<N>/queue/max_hw_sectors_kb 읽어 split size 계산 (max_data_transfer_size=0 일 때).

2. perf-findings 항목별 PR 영향

표 기호: ✅ 해결 / ⚠️ 부분 해결 / ⏸ 그대로 / 🆕 PR 이 새로 만든 이슈.

2.1 Rust io_uring 엔진 (1-x)

ID항목PR 영향근거
1-1notify_one 폭풍 (N회)그대로batched_write 루프 안 batch_ready.notify_one()rust/raw_block/src/lib.rs:1886 에 그대로. cmd 추가 외에는 수정 없음.
1-2CQ drain / SQ submit 같은 ring lock 직렬화⚠️ 악화 가능 + 일부 정리IoUringWrapperStandard/Big 분기를 추가하면서 lock 획득이 더 자주 발생한다 (submission_len, submission_sync 가 매번 lock().unwrap()). 단, 이전에 lock 보유 중이던 let mut ring = ring_clone.lock().unwrap(); 블록을 submission_sync() 헬퍼로 잘게 쪼개면서 scope 는 줄어듦. 결과적으로 lock 획득 빈도 ↑, 보유 시간 ↓. CQ drain Vec 복사는 여전히 그대로.
1-3워커 10μs busy-wait그대로wait_timeout_while(..., Duration::from_micros(10), ...) 동일.
1-4in_flight_count contention그대로decrement_in_flight 헬퍼로 추출만 됐고 동작은 동일.
1-5batch_in_flight HashMap lock그대로submission 마다 lock 잡는 패턴 동일. decrement_in_flight 도 매번 batch_in_flight.lock().
1-6register_fixed_buffers 가 호출 안 됨해결 (block 모드에 한해)rust_raw_block_backend.py:139-149_core.register_fixed_buffers_from_allocator(...) 호출. allocator 의 get_paged_buffers() 가 있을 때만. 단, MP 경로의 RawBlockL2Adapterraw_block_l2_adapter.py:343-348 에 "MP raw_block uses io_uring without fixed-buffer registration" 경고만 남기고 등록 안 함 — MP/L2 경로는 zero-copy 안 됨.
1-7wait_iouring 내부 busy-wait그대로위치만 옮겨지고 timeout 동일.

2.2 Python RawBlockCore (2-x)

ID항목PR 영향근거
2-1put_many lock 2N회그대로core.py:463-516 루프 구조 동일 — 키마다 with self._lock 두 번. PR 은 이 루프에 손대지 않음.
2-2put_many 키 단위 직렬 I/O⚠️ 부분 해결루프 내부의 _write_one 이 PR 후 _write_buffers([offset, offset+hdr], [header, buf], ...) 로 변경 (header + payload 2개를 하나의 batched_write 로 제출). 즉 키 1개당 2개 SQE 를 한 batch 로. 여러 키를 batch 로 묶지는 않음 — 진짜 batched_write 의 위력은 못 살림. 다만 plugin 레벨에서 _submit_put_many_core.put_many 통째로 단일 background task 로 보내 store-pool worker contention 은 완화.
2-3load_many_into 키 단위 직렬 읽기⚠️ 부분 해결core.py:610-627 루프 안 pread_intoself._read_buffers([off], [buf], ...) 로 바뀌었음 — 여전히 키 1개씩 호출. _read_buffersbatched_read 코드 경로를 알고 있지만 길이 1짜리 list 만 받아서 batched 의 의미가 없음. 진짜 한 번에 N개 키를 batched_read 로 보내는 호출자는 PR 에 없음.
2-4_snapshot_state lock 보유 중 dict 직렬화그대로snapshot 함수는 PR diff 에 안 보임.
2-5_free_slots Python list (O(N) 멤버 체크)그대로동일.
2-6_validate_loaded_entries 슬롯마다 pread⚠️ 간접 영향헤더 read 가 self._read_buffers([offset], [buf], ...) 로 옮겨졌지만 길이 1 호출이라 여전히 직렬. cmd 모드면 한 호출당 lba 정렬/MDTS split 분기까지 더 들어가서 약간 느려질 수 있음.
2-7체크포인트 JSON그대로변경 없음.

2.3 L2 어댑터 (3-x)

ID항목PR 영향근거
3-1store worker 가 put_many 직렬 호출개선 (plugin 경로)rust_raw_block_backend.pylen(pending) > 1 일 때 _submit_put_many 1개의 future 로 통합 — _store_pool worker 가 self._lock 에서 충돌 안 함. 단 MP 경로의 RawBlockL2Adapter 는 변경 없음 (config 만 추가).
3-2lookup 워커 1개그대로변경 없음.

2.4 PR 머지로 새로 생기는 이슈 (🆕)

항목설명근거
🆕 _write_buffers/_read_buffers 단일 호출의 batched_ 의미 소실*모든 I/O 가 dispatcher 를 통과하지만 호출자 대다수가 길이 1 list 로 호출. can_batch 판정 후 batched_write([1개]) 호출 → ring 1회 push + wait_iouring 1회 — Python ↔ Rust 한 번 왕복이 그대로. block 모드 fast-path 가 사라지고 한 번씩 더 우회 (pread_into 직접 호출 → dispatcher → batched_*).
🆕 uring_cmd 모드에서는 fixed buffer 자동 등록 Xrust_raw_block_backend.py:140if self._core.io_engine == "io_uring": 만 보고 use_uring_cmd 검사 안 함. register_fixed_buffers 자체는 cmd 모드에서도 동작 (uring_cmd.buf_index(Some(idx)))하지만 페이지 정렬이 LBA 정렬과 다를 때 SQE 빌드 단계에서 실패할 여지.
🆕 MDTS split 이 Python 루프 안에서 처리됨_write_uring_cmd_buffers 가 chunk 단위로 list 를 만든 뒤 batched_write 1회 — read 쪽 core.py:434 는 chunk 마다 read_uring 동기 호출 (batched_read 안 씀). 큰 페이로드일수록 io_uring 의 병렬성이 사라짐.
🆕 O_DIRECT 가 cmd 모드에서 강제 falsecore.py:226-232 — 기존 alignment 체크 로직(_is_buffer_aligned)도 우회. _validate_uring_cmd_chunk 가 LBA 정렬을 따로 검증하지만 buffer pointer 정렬은 검증 안 함 → 사용자 buffer 가 안 정렬돼 있어도 NVMe 가 받아들이면 통과, 거부하면 cmd 에러로 떨어짐 (Rust 의 IoUringWrapper::Standard 분기는 cmd 모드에선 절대 안 들어가므로 fallback 없음).
🆕 kernel 5.19+ 필수 + IoUring::<Entry128, Entry32>::builder() 우선 시도use_uring_cmd=False 여도 첫 시도가 Big entries — kernel 이 받아들이면 일반 io_uring 도 SQE/CQE 두 배 메모리 (128B/32B). 이 점은 본 perf-findings 에는 없던 새 트레이드오프. 5.19+ 시스템은 모두 자동으로 Big 경로를 타게 됨.

3. 우선순위 표 재배치

PR 머지 후 perf-findings 의 P0~P3 가 어떻게 재배열되는지:

원래 우선순위항목머지 후 상태새 우선순위비고
P0put_many/load_many_into → batched_*⚠️ Rust API 는 호출되지만 호출 단위가 키 1개여전히 P0"여러 키를 한 batch 로 묶기" 가 다음 단계
P0notify_one N→1 회⏸ 그대로P0PR 와 무관, 그대로 가치 있음
P1wait_iouring busy-wait⏸ 그대로P1변경 없음
P1_snapshot_state lock 축소⏸ 그대로P1
P1register_fixed_buffers 호출✅ plugin 경로만 해결P1 → P2MP/L2 경로는 여전히 미등록, 그건 다른 PR 영역
P2CQ drain Vec 복사 제거⏸ 그대로 + lock 빈도 ↑P1 로 승격wrapper 가 lock 획득을 더 자주 함 → CQ drain 시 ring lock 잡기 → split 가치 ↑
P2batch_in_flight HashMap lock⏸ 그대로P2
P2_free_slots set 화⏸ 그대로P2
P3_validate_loaded_entries batched read⏸ 그대로P3
P3체크포인트 JSON → binary⏸ 그대로P3
(신규)_write_buffers/_read_buffers 호출자가 길이 1 list 만 보냄🆕 PR 이 만든 새 이슈P0dispatcher 를 살리려면 호출자를 다중 키 단위로 바꿔야 함
(신규)uring_cmd read 가 chunk 단위 동기 호출🆕P1_read_uring_cmd_buffersbatched_read 로 바꿔야
(신규)MP/L2 경로에 fixed buffer 등록 hook 없음🆕 (경고만)P1MP allocator 와의 contract 정의 필요

4. FDP/HC-SSD 관점 시사점 (사용자 작업 맥락)

(사용자 목표: plugin-type L2 adapter 로 FDP/HC-SSD 지원, project_lmcache_analysis)

  1. uring_cmd 경로가 들어오면 FDP directive 는 두 곳에 끼워야 함

    • nvme_uring_cmd_prepcdw12 |= dtype<<20, cdw13 |= dspec<<16 가 이미 FDP 필드 자리. 단 현재 호출자 (_build_nvme_cmd_data(0, 0)) 는 항상 dtype=0, dspec=0 — FDP 활성화하려면 NvmeCmdData 빌드 시점에 stream/placement 결정 필요.
    • 사용자 adapter 가 L2 router 단 에서 placement ID 를 결정하고 NvmeCmdData 에 주입하는 hook 이 필요. PR 자체가 그 hook 을 노출하진 않음.
  2. HC-SSD zone append 등 다른 cmd opcode 확장

    • NVME_URING_CMD_IO (0xC048_4E80) 단일 opcode 만 지원. zone append 는 NVME_IO_ZONE_APPEND (0x7D)nvme_uring_cmd_prepcmd.opcode 분기에 추가하면 됨. PR 의 구조는 확장 가능하나 현재는 read/write 만.
  3. char device 강제 (/dev/ngXnY) 가 사용자 환경에 맞는가

    • block device (/dev/nvmeXnY) 사용 중이면 cmd 모드로 못 감. HC-SSD vendor SDK 가 char device 를 노출하는지 확인 필요.
  4. 사용자 adapter 위치 결정에 영향

    • PR 머지 후 LMCache 의 권장 cmd 모드 진입점은 RawBlockCore 안. 사용자 plugin 이 RawBlockCore 를 그대로 쓰고 NvmeCmdData 의 dtype/dspec 만 결정하는 경량 hook 만 넣으면 core 수정 없이 FDP 를 얹을 수 있음 — sub-class 또는 monkey-patch 후보.

5. 다음 액션 후보

  • PR 의 review 코멘트/discussion 본문 확인 — 위 🆕 항목들이 이미 지적됐는지.
  • RawBlockCore.put_many_write_buffers 를 진짜로 다중 키 단위로 호출하도록 follow-up 가능한지 검토.
  • _read_uring_cmd_buffersbatched_read 를 쓰지 않는 이유 확인 — read 경로의 fixed buffer 등록과 관련된 제약일 수 있음.
  • FDP directive 를 NvmeCmdData 에 주입하는 plugin hook 설계 (placement_id 결정 → adapter → core).
  • kernel < 5.19 환경에서 Big entries fallback to Standard 동작이 실제로 sane 한지 (Big 우선 시도 자체의 비용).