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 직접 확인)
✅ 인프라: 완비
| 항목 | 경로 | 상태 |
|---|---|---|
| EventBus | lmcache/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 모델 |
|---|---|---|
fs | connector/fs_connector.py + local_disk_backend.py | asyncio + AsyncPQThreadPoolExecutor |
raw_block | storage_backend/raw_block/core.py | ThreadPool + io_uring (Rust via plugins/rust_raw_block_backend.py) |
dax | storage_backend/dax/core.py | memcpy (동기, 너무 빠름) |
s3 | connector/s3_connector.py | HTTP 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)이 안 되나
| 어댑터 | 문제 |
|---|---|
dax | memcpy라 단계 구분 무의미, dt≈0 |
fs | asyncio 루프 내부 — "disk_io" 와 "adapter_internal" 구분 어려움 |
s3 | disk_io 라벨이 틀림 (네트워크) |
raw_block | io_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주 |
| #2 | raw_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)
- pending dict:
- 테스트:
tests/v1/mp_observability/subscribers/metrics/test_l2_latency.pytest_l2_throughput.py패턴 복사- store/load 각각 정상 경로 + SUBMITTED 없는 COMPLETED(스킵) 테스트
-
l2_latency.py를EventBus에 등록하는 위치 확인 (subscriber registry)
참고 파일 경로 (현재 코드 기준)
| 역할 | 경로 |
|---|---|
| 템플릿 subscriber | lmcache/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 |