본문으로 건너뛰기

Task 1 — L2 Staged Latency Histogram: 구현 계획

작성: 2026-05-22 / 코드 현황: dev 브랜치 HEAD 기준 직접 확인 이전 문서: v1_original_roadmap.md §Task 1


왜 이 Task인가 — 우리 목표와의 연결

Samsung 전략 4단계와의 매핑

우리 목표
① LMCache I/O 경로 분석 ──→ Task 1이 그 첫 번째 데이터 수집 레이어
② Storage Stack 최적화 ──→ 단계별 레이턴시가 없으면 어디서 병목인지 모름
③ FDP/HC-SSD 연결 ──→ 디바이스별 레이턴시 차이를 측정해야 FDP 효과 증명 가능
④ LMCache Upstream 기여 ──→ Task 1은 독립적이고 명확한 기여 단위 → 리뷰 수용성 높음

이게 왜 지금 필요한가

  • FDP/HC-SSD가 빠르다는 주장은 측정 데이터 없이 upstream PR로 못 들어감
    • "raw_block + FDP 쓰면 좋아요" → reviewer: "어디서 얼마나? 어떤 단계가 줄어드는 거야?"
    • Task 1이 만드는 queue_wait_ms / processing_ms / disk_io_ms 히스토그램이 그 근거
  • Task 4 (health check), Task 5 (device-aware policy) 는 Task 1 없이 설계 불가
    • p99 레이턴시 기반 health alert → 측정값이 있어야 임계값 설정 가능
    • "어떤 어댑터에 prefetch할지" 정책 → 어댑터별 레이턴시 분포가 있어야 결정 가능
  • 현재 mp_observability에 throughput만 있고 latency가 빠져 있음
    • l2_throughput.py 는 GB/s 만 측정 (submit→complete 전체를 하나로 묶음)
    • 병목이 큐 대기인지 실제 I/O인지 구별이 불가능

코드 현황 (2026-05-22 직접 확인)

✅ 인프라: 완비

항목경로상태
EventBuslmcache/v1/mp_observability/event_bus.py✅ 완비
L2 이벤트 타입lmcache/v1/mp_observability/event.py L41-56✅ 6개 정의됨
템플릿 subscriber…/subscribers/metrics/l2_throughput.py✅ 복붙 가능
Store publish 지점…/storage_controllers/store_controller.py L505-516✅ L505, L588, L608
Load publish 지점…/storage_controllers/prefetch_controller.py L739, L765✅ 확인

✅ 이벤트 타입 목록 (현재 정의된 것)

# store path
L2_STORE_SUBMITTED = "l2.store.submitted"
L2_STORE_COMPLETED = "l2.store.completed"

# load path (per-adapter)
L2_LOAD_TASK_SUBMITTED = "l2.load_task.submitted"
L2_LOAD_TASK_COMPLETED = "l2.load_task.completed"

# load path (aggregate)
L2_PREFETCH_LOAD_SUBMITTED = "l2.prefetch.load.submitted"
L2_PREFETCH_LOAD_COMPLETED = "l2.prefetch.load.completed"

*_DISPATCHED 이벤트는 아직 없음 — PR #1에서 추가 필요.

✅ 어댑터 구조 파악

어댑터실제 경로I/O 모델
fsconnector/fs_connector.py + local_disk_backend.pyasyncio + AsyncPQThreadPoolExecutor
raw_blockstorage_backend/raw_block/core.pyThreadPool + io_uring (Rust via plugins/rust_raw_block_backend.py)
daxstorage_backend/dax/core.pymemcpy (동기, 너무 빠름)
s3connector/s3_connector.pyHTTP async

⚠️ 메모리에 기록된 경로(connectors/raw_block_connector.py, connectors/fs_connector.py)는 구버전 경로. 현재는 connector/ (단수) 하위에 있음.


리스크 항목 — 현황 분석

Risk 1: EventBus.publish() 핫패스 비용 → ✅ 해결됨

확인 내용 (event_bus.py 직접 읽음):

def publish(self, event: Event) -> None:
if len(self._queue) >= self._config.max_queue_size:
# 큐 풀이면 drop (로그만)
...
self._queue.append(event) # ← deque.append() = O(1), non-blocking
  • 핫패스는 deque.append() 하나 + 드레인 스레드 wakeup 신호
  • 별도 백그라운드 drain thread가 subscriber 콜백 실행 → 레이턴시 측정 자체가 레이턴시에 영향 없음
  • max_queue_size 기본값 10,000; 가득 차면 drop (누수 없음)
  • 결론: 리스크 없음

Risk 2: fs 어댑터의 "dispatched" 타임스탬프 진입점 → ⚠️ 요확인

확인 내용 (local_disk_backend.py):

# store path
asyncio.run_coroutine_threadsafe(
self.async_save_bytes_to_disk, ...
)

# load path
return await self.disk_worker.submit_task(
...,
self.batched_async_load_bytes_from_disk,
)
  • AsyncPQThreadPoolExecutor에 job을 submit하는 시점 = SUBMITTED
  • 실제 코루틴이 executor thread에서 시작되는 시점 = DISPATCHED (우리가 찍어야 하는 곳)
  • 문제: executor 내부에서 콜백을 호출해야 하는데, executor가 이벤트 버스를 모름
  • 해결 방향: batched_async_load_bytes_from_disk 함수 진입부 첫 줄에 타임스탬프 찍거나, executor가 job 시작 시 콜백을 호출하는 훅 추가 필요
  • 어댑터 작성자 확인 필요 (또는 PR 리뷰에서 협의)

Risk 3: pending-dict 메모리 누수 가드 → ✅ 패턴 확인됨

l2_throughput.py에서 확인한 패턴:

def _record(event, correlation_key, pending, hist):
pending_entry = pending.pop(correlation_key, None)
if pending_entry is None:
return # SUBMITTED 없이 COMPLETED 온 경우 — 조용히 skip
  • .pop() 으로 COMPLETED 시 즉시 제거 → 누수 없음
  • COMPLETED 없이 SUBMITTED만 온 경우 (어댑터 크래시 등): 딕셔너리에 계속 쌓임
    • l2_throughput.py 도 동일한 취약점 — 현재 업스트림도 해결 안 된 문제
    • 우리 구현도 동일하게 처리하면 됨 (별도 만료 로직은 PR #1 scope 밖)

채택 설계 — adapter-aware 2+N 단계 모델

왜 균일 4단계 모델(queue_wait/adapter_internal/disk_io/completion)이 안 되나

어댑터문제
daxmemcpy라 단계 구분 무의미, dt≈0
fsasyncio 루프 내부 — "disk_io" 와 "adapter_internal" 구분 어려움
s3disk_io 라벨이 틀림 (네트워크)
raw_blockio_uring은 커널에 I/O 제출 후 완료 콜백 — 4단계 깔끔히 분리 가능

어댑터마다 다른 단계 수를 강제하면 OTel 메트릭에 undefined 값이 생기거나 의미 없는 0이 찍힘

채택 모델

공통 (모든 어댑터):
SUBMITTED ──[queue_wait_ms]──► DISPATCHED ──[processing_ms]──► COMPLETED

raw_block 전용 추가:
DISPATCHED ──[...] ──► IO_SUBMITTED ──[disk_io_ms]──► IO_COMPLETED ──► COMPLETED

새로 추가할 이벤트 타입:

L2_STORE_DISPATCHED = "l2.store.dispatched"
L2_LOAD_TASK_DISPATCHED = "l2.load_task.dispatched"
# (raw_block PR #2에서)
L2_STORE_IO_SUBMITTED = "l2.store.io_submitted"
L2_STORE_IO_COMPLETED = "l2.store.io_completed"
L2_LOAD_TASK_IO_SUBMITTED = "l2.load_task.io_submitted"
L2_LOAD_TASK_IO_COMPLETED = "l2.load_task.io_completed"

PR 분할 계획

PR범위주요 파일규모추정
#1*_DISPATCHED 이벤트 타입 추가 + 각 어댑터 worker 진입부 1줄 publish + l2_latency.py subscriber + 테스트event.py, local_disk_backend.py, raw_block/core.py, subscribers/metrics/l2_latency.py, tests/~400줄1~1.5주
#2raw_block 전용 IO_SUBMITTED/COMPLETED (subscriber에서 optional 처리)raw_block/core.py or rust_raw_block_backend.py, l2_latency.py 업데이트~200줄1주
#3(선택) docs/design/v1/mp_observability/ 이벤트 계약 업데이트 + 히스토그램 버킷 튜닝docs/design/~150줄0.5주

PR #1 구현 체크리스트

  • event.py: L2_STORE_DISPATCHED, L2_LOAD_TASK_DISPATCHED 추가
  • local_disk_backend.py: async_save_bytes_to_disk / batched_async_load_bytes_from_disk 진입부에 DISPATCHED publish (→ Risk 2 해결책 확정 후)
  • raw_block/core.py: worker thread 진입부에 DISPATCHED publish
  • subscribers/metrics/l2_latency.py: l2_throughput.py 패턴 기반으로 신규 작성
    • pending dict: (adapter_index, task_id)(queue_submit_ts,)
    • 히스토그램 2개: lmcache_mp.l2_queue_wait_ms, lmcache_mp.l2_processing_ms
    • attribute: l2_name, direction (store/load)
  • 테스트: tests/v1/mp_observability/subscribers/metrics/test_l2_latency.py
    • test_l2_throughput.py 패턴 복사
    • store/load 각각 정상 경로 + SUBMITTED 없는 COMPLETED(스킵) 테스트
  • l2_latency.pyEventBus에 등록하는 위치 확인 (subscriber registry)

참고 파일 경로 (현재 코드 기준)

역할경로
템플릿 subscriberlmcache/v1/mp_observability/subscribers/metrics/l2_throughput.py
이벤트 타입 정의lmcache/v1/mp_observability/event.py
EventBus 구현lmcache/v1/mp_observability/event_bus.py
Store publish 지점lmcache/v1/distributed/storage_controllers/store_controller.py L505
Load publish 지점lmcache/v1/distributed/storage_controllers/prefetch_controller.py L739, L765
fs I/O 진입부lmcache/v1/storage_backend/local_disk_backend.py L519, L568
raw_block 구현lmcache/v1/storage_backend/raw_block/core.py
테스트 템플릿tests/v1/mp_observability/subscribers/metrics/test_l2_throughput.py