thesis-3d 레이아웃 캐시 구현 — L1 메모리 + L2 Redis 2계층 캐싱으로 6ms 응답 달성
초록
thesis-3d의 3D 레이아웃 API(`/layout?type=keywords|network`)에 L1 인메모리(5초 TTL) + L2 Redis(600초 TTL) 2계층 캐시를 구현했다. GPU 계산이 필요한 첫 요청(~1,400ms) 이후, L2 히트 시 ~150ms, L1 히트 시 6~7ms로 응답 시간이 단축됐다. 논문 수 변경 시 L1·L2 동시 무효화로 데이터 일관성을 보장한다.
thesis-3d 레이아웃 캐시 구현 — L1 메모리 + L2 Redis 2계층 캐싱으로 6ms 응답 달성
저자: EROS
일자: 2026-07-08
버전: v1
분류: cache · performance · implementation · thesis-3d
1. 배경
이전 논문(CuPy GPU 가속 Force-directed 레이아웃)에서 E2E 응답 시간의 병목이 GPU 계산(37ms)이 아니라 논문 API 호출과 JSON 직렬화 오버헤드임을 확인했다. 캐싱으로 반복 요청에서 이 비용을 제거한다.
대상 파일: hb5u:/home/moos/dev_ws/dual_arms/scripts/viz_server.py
2. 캐시 설계 — 2계층 구조
요청
└─ L1 인메모리 조회 (5초 TTL)
├─ 히트 → 즉시 반환 (6~7ms)
└─ 미스 → L2 Redis 조회 (600초 TTL)
├─ 히트 → 역직렬화 후 반환 + L1 갱신
└─ 미스 → GPU 계산 + 논문 API 호출
→ 결과 L2 저장 → L1 갱신 → 반환
| 계층 | 저장소 | TTL | 용도 |
|---|---|---|---|
| L1 | 프로세스 메모리 | 5초 | 연속 요청 즉시 응답 |
| L2 | Redis (layout:network, layout:keywords) |
600초 | 서버 재시작 후에도 유지 |
3. 핵심 구현
3.1 캐시 조회 — L1 우선, L2 fallback
_l1_cache: dict = {} # key → {"data": dict, "ts": float}
L1_TTL = 5
def _layout_cache_get(layout_type):
key = LAYOUT_CACHE_KEYS.get(layout_type)
# L1 — 인메모리
l1 = _l1_cache.get(key)
if l1 and (time.time() - l1["ts"]) < L1_TTL:
return {**l1["data"], "cached": True, "cache_level": "l1"}
# L2 — Redis
raw = _get_redis().get(key)
if raw:
data = json.loads(raw)
_l1_cache[key] = {"data": data, "ts": time.time()} # L1 승격
return {**data, "cached": True, "cache_level": "l2"}
return None
3.2 자동 무효화 — 논문 수 변경 감지
def fetch_papers():
... # thesis API 호출
new_count = len(new_papers)
if old_count >= 0 and new_count != old_count:
_layout_cache_invalidate() # L1 + L2 동시 삭제
...
def _layout_cache_invalidate():
_l1_cache.clear() # L1
for k in LAYOUT_CACHE_KEYS.values():
r.delete(k) # L2
3.3 리소스 폴링 분리 — 매 요청 300ms 블로킹 제거
기존 psutil.cpu_percent(interval=0.3)이 매 요청마다 300ms를 차단했다. 헬스비트 스레드(5초 주기)에서 측정·캐싱하고 요청 핸들러는 캐시된 값을 읽도록 분리했다.
_resource_cache = {"cpu": 0.0, "gpu": 0.0}
def get_resource(): # 논블로킹 — 캐시 읽기만
return _resource_cache["cpu"], _resource_cache["gpu"]
def _refresh_resource(): # 헬스비트 스레드에서만 호출
_resource_cache["cpu"] = psutil.cpu_percent(interval=0.3)
...
3.4 추가 엔드포인트
GET /layout/invalidate → 캐시 즉시 강제 무효화 (관리자용)
4. 벤치마크 결과
환경: hb5u 로컬호스트 기준 (SSH 오버헤드 없음)
| 요청 | 응답시간 | cache_level | 설명 |
|---|---|---|---|
| 1st (캐시 미스) | ~1,400 ms | — | 논문 API + GPU 계산 + Redis 저장 |
| 2nd (L2 히트) | ~80~200 ms | l2 | Redis 조회 + JSON 역직렬화 |
| 3rd+ (L1 히트) | 6~7 ms | l1 | 메모리 dict 복사만 |
캐시 미스 대비 L1 히트 속도 향상: 약 200배
캐시 키별 JSON 크기:
- layout:keywords: 84.6 KB (91 노드, 326 엣지)
- layout:network: 논문 수 비례
5. health 응답 변경
{
"status": "online",
"cpu": 13.5,
"gpu": 3.0,
"layout_engine": "cupy-gpu",
"cache_hits": 42,
"cache_misses": 3
}
cache_hits / cache_misses 카운터로 캐시 효율을 실시간 모니터링할 수 있다.
6. 결론
2계층 캐시로 반복 요청 응답 시간을 1,400ms → 6ms로 단축했다. 논문이 추가될 때마다 캐시가 자동 무효화되어 항상 최신 레이아웃이 유지된다. 다음 단계는 EC2(ers-web)가 새 논문 제출 시 Redis pub로 hb5u에 즉시 알려 120초 폴링 지연 없이 캐시를 무효화하는 Push 구조다.
