Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 103 additions & 8 deletions docs/04-experiment.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,20 @@ MySQL만 사용하는 초기 리다이렉트 구조에서 VU 증가에 따라

### Stress Test

- 수행 여부: 미실행
- 목적: 오류 또는 급격한 지연이 발생하는 한계 구간 확인
- 수행 여부: 완료
- 목적:
- VU 증가에 따른 처리량 한계 확인
- p95와 p99가 급격히 증가하는 구간 확인
- DB Only와 Redis 구조의 한계 비교
- 비교 대상: DB Only / Redis Cache Aside
- 공통 조건:
- Platform Thread
- HikariCP 최대 커넥션 10개
- 동일한 단축 URL 반복 조회
- Redirect 추적 비활성화
- Think Time 없음
- 부하 단계: 100 → 200 → 300 → 500 VU
- 전체 실행 시간: 4분 20초

## 7. 측정 지표

Expand Down Expand Up @@ -354,27 +366,110 @@ Cache Hit과 Cache Miss는 모두 0건이므로
> 조건별 한 번만 실행한 결과이므로 Pool 10을 최적값으로 확정할 수는 없다.
> 정확한 최적값을 결정하려면 조건별 반복 측정과 MySQL 서버 지표가 필요하다.

## 15. 실험 한계
## 15. Stress Test

DB Only와 Redis Cache Aside 구조에서 VU를 100, 200, 300, 500까지 단계적으로 증가시켰다.

두 구조 모두 Platform Thread와 HikariCP 최대 커넥션 10개를 사용했으며,
캐시 활성화 여부만 변경했다.

### k6 전체 결과

아래 결과는 4분 20초 동안 변화한 모든 VU 구간을 합산한 값이다.

| 지표 | DB Only | Redis |
|---|---:|---:|
| Redirect 요청 수 | 2,123,358 | 4,586,523 |
| 전체 평균 RPS | 8,160.01 | 17,598.13 |
| 평균 응답 시간 | 30.87ms | 13.61ms |
| p90 | 73.64ms | 22.63ms |
| p95 | 90.98ms | 26.75ms |
| 최대 응답 시간 | 389.70ms | 475.44ms |
| 요청 실패율 | 0% | 0% |

Redis 적용 후 DB Only 대비 전체 평균 RPS는 약 115.66% 증가했다.
평균 응답 시간은 약 55.91%, p95는 약 70.60% 감소했다.

최대 응답 시간은 Redis가 더 높았지만,
단일 이상치에 가까운 최대값보다 평균과 p95를 중심으로 결과를 해석했다.

### DB Only 관찰

DB Only는 VU 증가에 따라 초기에는 처리량이 증가했지만,
약 200 VU 이후 RPS가 약 8.5K~9K 수준에서 더 이상 증가하지 않았다.

반면 p95와 p99는 VU가 증가할수록 계속 증가했다.
HikariCP Active는 최대 커넥션 10개에 도달했고,
Pending 요청은 약 180~190까지 증가했다.

따라서 200 VU 이후에는 더 많은 요청을 받아도 처리량이 증가하지 않고,
DB 커넥션을 기다리는 요청만 증가한 것으로 나타났다.

### Redis 관찰

Redis 구조는 VU 증가에 따라 처리량이 약 22K RPS까지 증가했다.
500 VU까지 5xx 오류와 요청 실패는 발생하지 않았다.

전체 Stress Test에서 측정된 캐시 및 DB 지표는 다음과 같다.

| 지표 | 결과 |
|---|---:|
| Cache Hit | 4,586,523 |
| Cache Miss | 1 |
| DB Lookup | 1 |
| Cache Hit Ratio | 약 100% |

최초 조회에서만 Cache Miss와 DB Lookup이 발생했고,
이후 모든 반복 요청은 Redis Cache Hit로 처리됐다.

Prometheus 수집 시점에서는 HikariCP Active와 Pending이 관찰되지 않았다.
Redis 구조도 약 22K RPS 부근에서 처리량 증가가 둔화됐지만,
500 VU까지 오류 없이 DB Only보다 낮은 지연을 유지했다.

### 결과 해석

DB Only는 약 200 VU 이후 HikariCP 커넥션 풀이 포화되면서
처리량이 정체되고 대기 요청이 증가했다.

Redis Cache Aside는 반복적인 DB 조회와 커넥션 대기를 제거해
DB Only보다 약 2.16배 높은 전체 평균 처리량과 낮은 p95를 기록했다.

다만 Redis도 약 22K RPS 부근에서 처리량 증가가 둔화됐으므로,
더 높은 부하에서는 애플리케이션, Redis, 네트워크 또는
로컬 Docker 환경의 다른 제한 요소를 추가로 확인해야 한다.

#### Grafana 측정 결과

**DB Only**

![DB Only Stress Test](images/stress-db-only.png)

**Redis**

![Redis Stress Test](images/stress-redis.png)

## 16. 실험 한계

- 로컬 Docker 환경에서 실행했다.
- k6, 애플리케이션, MySQL, Redis가 같은 장비의 자원을 사용했다.
- 조건별 한 번만 측정해 실행 환경의 변동이 포함될 수 있다.
- 테스트 시간이 조건별 1분으로 짧다.
- Load Test는 조건별 1분, Stress Test는 구조별 4분 20초 동안 실행했다.
- 하나의 단축 URL만 반복 조회했다.
- Prometheus 수집 간격 사이의 짧은 지표 변화는 누락될 수 있다.
- 고정 VU 방식이므로 응답 시간이 짧을수록 같은 시간에 더 많은 요청을 전송한다.
- HikariCP Pool 비교에서 MySQL CPU, 실행 중인 쿼리 수, 락 대기까지는 측정하지 않았다.
- 고정 VU 및 단계적 VU 증가 방식에서는 응답 시간이 짧을수록 더 많은 요청이 발생한다.
- Stress Test의 k6 최종 결과는 모든 VU 구간을 합산한 값이므로, 특정 VU 구간의 값은 Grafana 시계열을 통해 판단했다.
- Redis Stress Test의 처리량 한계가 애플리케이션, Redis 또는 로컬 환경 중 어디에서 발생했는지는 추가로 분리하지 않았다.

## 16. 후속 실험
## 17. 후속 실험

- [x] Redis Cache Aside 적용
- [x] Redis 적용 전후 부하 테스트
- [x] Prometheus·Grafana 서버 지표 비교
- [x] 캐시 ON/OFF 환경변수 적용
- [x] Platform Thread와 Virtual Thread 비교
- [x] HikariCP Pool 크기 비교
- [x] 더 높은 VU로 Stress Test 수행
- [ ] 조건별 3회 측정 후 중앙값 비교
- [ ] 더 높은 VU로 Stress Test 수행
- [ ] 단축 코드 생성 전략 비교
- Sequence ID + Base62
- Hash + 충돌 처리
Expand Down
Binary file added docs/images/stress-db-only.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/stress-redis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
119 changes: 119 additions & 0 deletions k6/redirect-stress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import http from 'k6/http';
import { check } from 'k6';

const BASE_URL = __ENV.BASE_URL || 'http://app:8080';

export const options = {
stages: [
{ duration: '20s', target: 100 },
{ duration: '40s', target: 100 },

{ duration: '20s', target: 200 },
{ duration: '40s', target: 200 },

{ duration: '20s', target: 300 },
{ duration: '40s', target: 300 },

{ duration: '20s', target: 500 },
{ duration: '40s', target: 500 },

{ duration: '20s', target: 0 },
],

thresholds: {
'http_req_failed{endpoint:redirect}': ['rate<0.10'],
'checks{endpoint:redirect}': ['rate>0.90'],
},
};

export function setup() {
const response = http.post(
`${BASE_URL}/api/v1/data/shorten`,
JSON.stringify({
longUrl: 'https://www.google.com',
}),
{
headers: {
'Content-Type': 'application/json',
},
tags: {
endpoint: 'setup',
},
}
);

const success = check(
response,
{
'단축 URL 생성 상태는 201이다': (res) => res.status === 201,
'shortCode가 반환된다': (res) => {
try {
const body = res.json();
return body.shortCode !== undefined
&& body.shortCode !== null;
} catch {
return false;
}
},
},
{
endpoint: 'setup',
}
);

if (!success) {
throw new Error(
`단축 URL 생성 실패: status=${response.status}, body=${response.body}`
);
}

const shortCode = response.json().shortCode;

// Redis에서는 캐시 Warm-up, DB Only에서는 초기 동작 확인 역할
const initialRedirectResponse = http.get(
`${BASE_URL}/api/v1/${shortCode}`,
{
redirects: 0,
tags: {
endpoint: 'warm-up',
},
}
);

if (initialRedirectResponse.status !== 302) {
throw new Error(
`초기 리다이렉트 실패: status=${initialRedirectResponse.status}`
);
}

return {
shortCode,
};
}

export default function (data) {
const response = http.get(
`${BASE_URL}/api/v1/${data.shortCode}`,
{
redirects: 0,
tags: {
endpoint: 'redirect',
test_type: 'stress',
},
}
);

check(
response,
{
'리다이렉트 상태는 302이다': (res) => res.status === 302,
'Location 헤더가 존재한다': (res) =>
res.headers.Location !== undefined,
'원본 URL이 올바르다': (res) =>
res.headers.Location === 'https://www.google.com',
},
{
endpoint: 'redirect',
}
);
}
Loading