Raw Block Storage Stack 분석 메모
분석 범위:
core.py+raw_block_l2_adapter.py(MP path) +rust_raw_block_backend.py(legacy path)
변경 검증 가이드 (다음 fetch 후):
git log 29bbd553..HEAD -- lmcache/v1/storage_backend/raw_block/core.py \lmcache/v1/storage_backend/raw_block/key_codec.py
_write_checkpoint/_snapshot_state/_free_slots구현이 변경되면 B1·P1·B2 진단이 stale._checkpoint_onceidle 로직이 바뀌면 D2 정책 이슈가 해소됐을 수 있음.
역할 요약
RawBlockCore는 legacy non-MP와 MP L2 path 양쪽에서 공유하는 raw-block I/O 엔진.
슬롯 할당/해제, 체크포인트/복구, lock refcount, Rust lmcache_rust_raw_block_io 바인딩을 소유.
발견 이슈
B — 버그 / 논리 오류
B1. _meta_seq 레이스 컨디션 (medium / latent)
위치: core.py:1173
_write_checkpoint는 self._lock 밖에서 next_seq = self._meta_seq + 1을 읽는다.
현재는 안전하다. 작성자가 이를 인지하고 설계한 것으로 보임:
close()는_meta_thread.join()완료 후 checkpoint를 쓰므로 background thread와close()사이에 레이스 없음- 프로덕션 코드에서
checkpoint_now()의 호출자는 현재 테스트 1곳뿐 (tests/v1/storage_backend/test_raw_block_core.py:114)
암묵적 가정: _write_checkpoint는 "한 번에 한 스레드만 호출한다"는 전제 위에 설계됐다.
이 가정이 유지되는 한 next_seq를 락 밖에서 읽어도 문제없다.
레이스가 실재하는 조건 (현재는 해당 없음):
- adapter/plugin이
checkpoint_now()를 외부에서 주기적으로 호출하는 기능 추가 시 meta_enable_periodic=True상태에서checkpoint_now()동시 호출 시
결론: 현재 아키텍처에서는 버그가 아니다. 단, checkpoint_now()가 public API인 이상
호출 제약을 docstring에 명시하거나 _checkpoint_lock으로 직렬화를 enforce하지 않으면
향후 확장 시 조용히 터질 수 있는 잠재 취약점.
B2. _append_free_slot_locked O(n) 중복 검사 (low / cosmetic)
위치: core.py:1019
_free_slots가 list이므로 slot in self._free_slots 중복 검사는 O(n).
단, caller 3곳 모두 slot lifecycle 구조상 duplicate가 실제로 발생하지 않는다:
| caller | slot 출처 | 중복 가능성 |
|---|---|---|
delete_many L685 | self._index.pop() — 동일 key는 index에 1번만 | 없음 |
put_many 실패 L502 | self._inflight.pop() — 역시 1번만 | 없음 |
_validate_loaded_entries L1441 | to_drop list, index에서 각 key 1번 | 없음 |
즉 guard는 순수 방어 코드이며 정상 동작에서는 항상 통과된다.
O(n) 비용이 실질적으로 문제가 되는 조건:
_free_slots가 F개 쌓인 상태에서 D개 bulk delete 시 비교 횟수 = D×F + D(D-1)/2- 하지만 캐시가 꽉 찬 직후 eviction이 시작될 때
_free_slots≈ 0이므로 F ≈ 0 → O(D)에 수렴 _free_slots가 크게 쌓이는 시나리오("많이 지우고 잠깐 put 안 하는" 경우)에서만 O(D×F) 발생
단독 수정 가치는 낮다. 의미 있는 이유는 P2(FDP FIFO)를 구현할 때 자연스럽게 함께 처리할 수 있다는 점이다.
deque + set 교체 하나로 O(1) dedup + FIFO 동시 해결:
from collections import deque
# __init__
self._free_slots: deque[int] = deque()
self._free_slots_set: set[int] = set()
def _append_free_slot_locked(self, slot: int) -> None:
if slot < 0 or slot >= self._max_slots:
return
if slot in self._free_slots_set: # O(1)
return
self._free_slots.append(slot)
self._free_slots_set.add(slot)
def _allocate_slot_locked(self) -> int:
if self._free_slots:
slot = self._free_slots.popleft() # FIFO (P2 동시 해결)
self._free_slots_set.discard(slot)
return self._slot_to_offset(slot)
...
주의: _apply_loaded_state core.py:1354에서
self._free_slots = [...]로 직접 덮어쓰는 부분도 함께 수정 필요.
P — 성능 이슈
P1. _snapshot_state lock 내 직렬화 작업 (medium / conditional)
with self._lock: 안에서 전체 index dict comprehension이 실행된다.
entry마다 list(shape), hasattr() 2회, .tolist() 조건부 실행 포함.
실질 영향은 조건부다:
_checkpoint_once는idle_ok(inflight_io_count == 0) 조건에서만 실행 → 정상 운용 중에는 I/O와 checkpoint가 겹치지 않음- 따라서 lock 경합이 실제로 발생하려면 checkpoint가 도는 순간 별도 스레드가
put_many/delete_many를 호출해야 함 - entry < ~1,000이면 lock 보유 시간 수 ms 미만 → 무시 가능
- entry > ~10,000이고 checkpoint 빈도가 높은 경우에만 체감 영향
개선 가치: 대용량 NVMe(수만 entry 목표 설계)에서는 선제적으로 수정할 이유가 있다. shallow copy 분리로 lock 보유 구간을 크게 줄일 수 있다:
# 락 안에서는 shallow copy만
with self._lock:
dirty_total = self._meta_dirty_total
index_snapshot = dict(self._index) # O(n) but no per-entry work
free_slots_copy = list(self._free_slots)
next_slot = self._next_slot
# 락 밖에서 직렬화 (per-entry Python work 여기서)
snapshot["entries"] = {k: _entry_to_dict(v) for k, v in index_snapshot.items()}
P2. _free_slots LIFO → FDP/HC-SSD wear에 불리 (FDP 전용)
위치: core.py:1007
_free_slots.pop() (LIFO)는 최근 해제된 슬롯을 즉시 재사용한다.
SSD 타입별 영향:
| SSD 타입 | LIFO 영향 |
|---|---|
| 일반 NVMe (TLC/QLC) | 중립~약간 유리 — FTL이 자체 wear leveling, LIFO로 SLC cache 재활용 가능 |
| DRAM/ramdisk | 무관 |
| FDP-capable NVMe | 불리 — host가 placement 직접 제어, LIFO로 특정 RU에 집중 기록 |
| HC-SSD | 불리 — 동일 이유 |
측정 방법:
- 단기 latency/throughput 측정으로는 차이 없음 (I/O 속도 자체는 동일)
_allocate_slot_locked에 slot histogram 계측 → LIFO/FIFO 분포 즉시 확인 가능- WAF 측정:
nvme smart-log의data_units_written비교 → 수일 단위 sustained load 필요 - FDP RU 분포:
nvme fdp stats→ FDP 장치에서 수 시간 부하 후 확인
일반 SSD 사용자에게 FIFO 강제는 regression 가능 → free_slot_order: "lifo"|"fifo" config로 선택하거나 FDP 활성화 여부로 분기하는 설계가 바람직.
B2와 통합 수정 시 자연스럽게 해결 가능 (deque.popleft FIFO).
P3. put_many 순차 write, io_uring batch 미활용 (medium)
위치: core.py:434-521
put_many 루프가 각 key에 대해 _write_one을 동기적으로 호출하므로
io_uring SQ에 여러 SQE를 한 번에 제출하는 batch I/O를 활용하지 못한다.
Rust 바인딩에 pwrite_batch 인터페이스가 추가되면 여기서 활용할 수 있다.
Q — 코드 품질 (coding_standards.md 기준)
| # | 위치 | 문제 |
|---|---|---|
| Q1 | core.py:9 | Optional import + 사용 (standards: prefer X | None) |
| Q2 | core.py:58-62 | Any 타입 힌트 — io_engine: str | None, use_iouring: bool | None으로 명시 가능 |
| Q3 | core.py:463 | zip(strict=False) — 이미 길이 검증 완료 후이므로 strict=True가 방어적으로 더 적절 |
| Q4 | core.py:602 | except Exception: pass — silently drop cast failure, 최소 logger.debug 권장 |
D — 설계 주의사항
D1. 체크포인트 JSON 포맷 확장성
대형 NVMe에 KV 텐서 수만 개가 올라오면 JSON 페이로드가 수 MB가 된다.
_meta_payload_capacity 초과 시 checkpoint를 통째로 skip하는데, 이 경우 crash 후
데이터는 device에 있지만 index가 날아간다. 용량 초과 warning 외 별도 알람이 없다.
D2. 지속 I/O 하에서 checkpoint 미발생 가능성
지속적인 I/O load 하에서 idle_ok = False이므로 periodic checkpoint가 전혀 발생하지 않는다.
close() 시 force=True checkpoint가 최후 보루인데, SIGKILL 등 비정상 종료 시 최신 index를 잃는다.
max_dirty_threshold 추가로 일정 수 이상 변경 시 강제 checkpoint하는 방어 로직 필요.
D3. 단일 글로벌 락
index 조회, 슬롯 할당, lock refcount, inflight 추적 모두 self._lock 하나를 사용.
실제 I/O는 락 밖에서 하므로 당장은 큰 문제 없으나, 다수 vLLM worker가 동시에
exists_many/load_many_into를 호출할 때 read 경합 발생 가능.
우선순위 요약 (core.py 범위)
| 우선순위 | 항목 | 실질 영향 | 수정 난이도 |
|---|---|---|---|
| 향후 확장 전 정리 | B1 (checkpoint_now 호출 제약 docstring 명시) | 현재는 안전, latent only | 낮음 |
| FDP 구현 시 통합 수정 | B2 + P2 세트 (deque + set 교체, FIFO) | B2 단독은 낮음; P2는 FDP 전용 효과 | 낮음 |
| 대용량 NVMe 설계 시 선제 수정 | P1 (lock 내 shallow copy 분리) | entry < 1,000 이면 거의 무관; 수만 entry 목표라면 의미 있음 | 낮음 |
| 코드 정리 | Q1~Q4 (타입 힌트, zip strict) | 기능 영향 없음 | 낮음 |
| 중기 검토 | D1 (JSON 포맷 한계 대응) | 수만 entry 시 checkpoint skip 위험 | 중간 |
| 중기 검토 | D2 (dirty threshold checkpoint) | 지속 부하 하 비정상 종료 시 index 손실 | 낮음 |
| 장기 / Rust 연동 | P3 (io_uring batch write) | io_uring 효과 미활용 | 높음 — Rust 바인딩 확장 필요 |
Adapter layer 분석 (T1/T2/L1~L4) 은 별도 문서 참조: [[raw_block_stack_analysis]] —
raw_block_l2_adapter.py+rust_raw_block_backend.py+core.pystack 전체 I/O lifecycle 분석