Skip to content

[PERF] Lambda + Caffeine 캐시 성능 측정 — 간헐적 트래픽 시나리오' #213

@softmoca

Description

@softmoca

📌 관련 Epic

Part of #204

📋 기능 설명

Lambda 환경에서 **SOPT 홈페이지의 실제 트래픽 패턴(간헐적 접속)**을 시뮬레이션하여,
Caffeine 로컬 캐시의 효과가 사실상 사라지는 것을 정량적으로 증명한다.

측정 환경

  • 인프라: AWS Lambda + API Gateway (https://org-api-dev.sopt.org)
  • 캐시: Caffeine (In-Memory) — EC2와 동일 코드

핵심 가설

Lambda 인스턴스는 일정 시간(약 5~15분) 비활성 상태가 지속되면 소멸된다.
SOPT 홈페이지는 대부분의 시간에 트래픽이 없고, 간헐적으로 접속이 발생하는 패턴이다.
따라서 사용자가 접속할 때마다 새 인스턴스가 생성되어 캐시가 비어있고,
매번 Playground API를 다시 호출해야 한다.

즉, 연속 트래픽에서는 캐시가 작동하지만, 실제 사용 패턴에서는 캐시 효과가 거의 없다.

EC2와의 근본적 차이

EC2 Lambda
인스턴스 수명 서버가 켜져 있는 동안 영구 비활성 5~15분 후 소멸
캐시 수명 프로세스 재시작 전까지 유지 (TTL 24시간) 인스턴스와 함께 소멸
간헐적 트래픽 캐시 그대로 유지 → 히트 인스턴스 소멸 → 새 인스턴스 → 미스

🎯 왜 필요한가요?

이슈 #4에서 연속 부하 테스트로 측정한 Lambda 결과(P50 75ms, 안정적)는
Lambda가 잘 작동하는 최상의 시나리오일 뿐이다.

실제 SOPT 홈페이지 트래픽 패턴에서 Lambda + 로컬 캐시가 효과를 잃는다는 걸 증명해야
Redis 외부 캐시 도입의 필요성이 설득력을 갖는다.

💡 제안하는 해결 방법

1. 간헐적 트래픽 시뮬레이션 스크립트

일정 간격으로 API를 호출하여, 각 호출이 Cold Start인지 Warm인지를 기록한다.

파일: k6/load-test-interval.js

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend, Counter } from 'k6/metrics';

// 커스텀 메트릭
const responseLatency = new Trend('interval_latency', true);
const coldCount = new Counter('cold_count');    // 응답 > 임계값 → 캐시 미스로 추정
const warmCount = new Counter('warm_count');    // 응답 < 임계값 → 캐시 히트로 추정

// Cold/Warm 판별 임계값 (ms)
// EC2 캐시 히트 시 P95가 8.46ms, 캐시 미스 시 P50이 2.65s
// Lambda에서 캐시 히트 시 ~80ms, 미스 시 훨씬 느릴 것으로 예상
const COLD_THRESHOLD_MS = 500;

export const options = {
  scenarios: {
    interval_test: {
      executor: 'per-vu-iterations',
      vus: 1,
      iterations: 10,         // 10회 반복
    },
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://org-api-dev.sopt.org';
const CTX_PATH = __ENV.CTX_PATH !== undefined ? __ENV.CTX_PATH : '/v2';
const PROJECTS_URL = `${BASE_URL}${CTX_PATH}/projects`;

// 호출 간격 (초) — Lambda 인스턴스 소멸을 유도
// 5분 = 300초, 10분 = 600초, 15분 = 900초
const INTERVAL_SECONDS = parseInt(__ENV.INTERVAL || '600');

export function setup() {
  console.log('========================================');
  console.log('Interval Traffic Simulation');
  console.log(`Target URL: ${PROJECTS_URL}`);
  console.log(`Interval: ${INTERVAL_SECONDS}s (${INTERVAL_SECONDS / 60}min)`);
  console.log(`Iterations: ${options.scenarios.interval_test.iterations}`);
  console.log(`Cold threshold: ${COLD_THRESHOLD_MS}ms`);
  console.log('========================================');

  return { projectsUrl: PROJECTS_URL };
}

export default function (data) {
  const iteration = __ITER;

  // API 호출
  const res = http.get(data.projectsUrl, {
    timeout: '30s',
  });

  check(res, {
    'status is 200': (r) => r.status === 200,
  });

  const duration = res.timings.duration;
  responseLatency.add(duration);

  // Cold/Warm 판별
  const isCold = duration > COLD_THRESHOLD_MS;
  if (isCold) {
    coldCount.add(1);
  } else {
    warmCount.add(1);
  }

  const label = isCold ? '🥶 COLD (cache miss)' : '🔥 WARM (cache hit)';
  console.log(
    `[${iteration + 1}/10] ${label}${duration.toFixed(0)}ms (status: ${res.status})`
  );

  // 마지막 반복이 아니면 간격 대기
  if (iteration < options.scenarios.interval_test.iterations - 1) {
    console.log(`⏳ Waiting ${INTERVAL_SECONDS}s for Lambda instance to cool down...`);
    sleep(INTERVAL_SECONDS);
  }
}

export function teardown(data) {
  console.log('========================================');
  console.log('Interval test completed.');
  console.log(`Target: ${data.projectsUrl}`);
  console.log('========================================');
}

2. 실행 방법

# 10분 간격, 10회 반복 (총 약 90분 소요)
k6 run \
  --env BASE_URL=https://org-api-dev.sopt.org \
  --env CTX_PATH=/v2 \
  --env INTERVAL=600 \
  k6/load-test-interval.js

# 5분 간격으로 더 빠르게 테스트 (총 약 45분 소요)
k6 run \
  --env BASE_URL=https://org-api-dev.sopt.org \
  --env CTX_PATH=/v2 \
  --env INTERVAL=300 \
  k6/load-test-interval.js

3. 예상 결과

[1/10] 🥶 COLD (cache miss) — 3200ms (status: 200)    ← 새 인스턴스, 캐시 없음
⏳ Waiting 600s for Lambda instance to cool down...
[2/10] 🥶 COLD (cache miss) — 2800ms (status: 200)    ← 인스턴스 소멸 후 재생성
⏳ Waiting 600s for Lambda instance to cool down...
[3/10] 🥶 COLD (cache miss) — 3100ms (status: 200)    ← 매번 반복
...
[10/10] 🥶 COLD (cache miss) — 2900ms (status: 200)

10회 중 대부분이 COLD로 찍히면 → 간헐적 트래픽에서 캐시 히트율 ≈ 0% 증명

4. EC2 대조군 (같은 스크립트로 EC2 테스트)

# EC2에서 동일한 간격 테스트
k6 run \
  --env BASE_URL=http://localhost:8080 \
  --env CTX_PATH=/v2 \
  --env INTERVAL=600 \
  k6/load-test-interval.js

EC2에서는 10회 모두 WARM으로 찍힐 것 → 동일 조건에서 캐시 효과의 차이를 명확히 대비

5. 결과 기록

## 시나리오 ④ 측정 결과 (Lambda, Caffeine 로컬 캐시 — 간헐적 트래픽)

### 테스트 조건
- 호출 간격: 10분 (600초)
- 반복 횟수: 10회
- Cold 판별 임계값: 500ms

### 호출별 결과
| # | 응답시간 | 판별 | 비고 |
|---|---|---|---|
| 1 | ___ms | COLD / WARM | |
| 2 | ___ms | COLD / WARM | |
| ... | | | |
| 10 | ___ms | COLD / WARM | |

### 요약
- Cold 횟수: ___/10 (___%)
- Warm 횟수: ___/10 (___%)
- 평균 응답시간: ___ms

### ② EC2 Caffeine 대비 비교 (핵심!)
| 지표 | ② EC2 + Caffeine (간헐적) | ④ Lambda + Caffeine (간헐적) |
|---|---|---|
| Cold 비율 | 0/10 (0%) | ___/10 (___%) |
| 평균 응답시간 | ~4ms (캐시 히트) | ___ms |
| 캐시 실효성 | ✅ 트래픽 무관 유지 | ❌ 간헐적 트래픽에서 무효화 |

6. 보충: 연속 트래픽 측정 결과 (이미 완료)

이슈 코멘트에 아래도 함께 기록하여, "연속 vs 간헐적" 두 패턴의 차이를 보여준다:

### 참고: 연속 트래픽에서의 Lambda 결과 (초당 30 요청, 2분)
| 지표 | 값 |
|---|---|
| P50 | 75.65ms |
| P95 | 83.18ms |
| RPS | 28.3 |

→ 연속 트래픽에서는 Lambda도 캐시가 잘 작동함
→ 하지만 SOPT 홈페이지의 실제 패턴은 간헐적 트래픽이므로, 이 결과는 실사용과 괴리가 있음

✅ 완료 조건 (Definition of Done)

  • k6/load-test-interval.js 스크립트 작성 완료
  • Lambda 환경에서 간헐적 트래픽 시뮬레이션 실행 완료 (10분 간격 × 10회)
  • EC2 환경에서 동일 조건 대조군 테스트 완료
  • Cold/Warm 비율 비교 결과 기록
  • CloudWatch Logs에서 Cache HIT/MISS 로그 확인 (보충)
  • 연속 트래픽 결과와 간헐적 트래픽 결과 함께 정리
  • 측정 결과를 이슈 코멘트에 기록

🔗 참고 자료

💬 추가 논의사항

  • Lambda 인스턴스 소멸 시간은 AWS가 내부적으로 관리하므로 정확한 시간을 보장할 수 없음. 5분/10분/15분 간격으로 각각 테스트하여 소멸 임계점을 파악하면 더 좋음
  • CloudWatch의 ConcurrentExecutions 메트릭으로 인스턴스 생성/소멸 패턴도 확인 가능
  • Cold 판별 임계값(500ms)은 실제 테스트 후 조정 가능 — EC2 캐시 미스(2.65s)와 Lambda 캐시 히트(~80ms) 사이 적절한 값으로 설정
  • SOPT 홈페이지의 실제 트래픽 패턴(Google Analytics 등)을 함께 제시하면 포트폴리오 설득력이 더 높아짐

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions