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
60 changes: 36 additions & 24 deletions docs/04-experiment.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ MySQL만 사용하는 초기 리다이렉트 구조에서 VU 증가에 따라
| Java | 25 |
| Spring Boot | 4.1.0 |
| Database | MySQL 8.4 |
| Redis | 미적용 |
| Redis | Baseline 미적용 / Experiment 적용 |
| k6 | 2.1.0 |
| Prometheus | 3.13.0 |
| Grafana | 13.1.1 |
Expand Down Expand Up @@ -106,9 +106,11 @@ MySQL만 사용하는 초기 리다이렉트 구조에서 VU 증가에 따라

## 8. Warm-up

- 별도 Warm-up: 미실행
- 측정 제외 구간: 없음
- 보완 사항: Redis 비교 실험에서는 동일한 Warm-up 조건으로 Baseline도 다시 측정한다.
- Baseline: 별도 Warm-up 미실행
- Redis: `setup()`에서 리다이렉트를 한 번 호출해 캐시 저장
- 측정 제외 구간: URL 생성 요청과 캐시 Warm-up 요청
- 실제 측정 요청: Redis Cache Hit 상태
- 한계: Baseline과 Redis의 Warm-up 조건이 완전히 동일하지 않아 최종 비교 시 재측정이 필요하다.

## 9. Baseline 결과

Expand Down Expand Up @@ -136,12 +138,14 @@ MySQL만 사용하는 초기 리다이렉트 구조에서 VU 증가에 따라

## 10. 개선 내용

리다이렉트 조회에 Redis Cache Aside 전략을 적용할 예정이다.
리다이렉트 조회에 Redis Cache Aside 전략을 적용했다.

```text
Redis 조회
→ Cache Hit: 원본 URL 반환
→ Cache Miss: MySQL 조회 후 Redis 저장
→ Cache Miss: MySQL 조회
→ Redis 저장
→ 원본 URL 반환
```

기대 효과:
Expand All @@ -153,30 +157,38 @@ Redis 조회

## 11. 개선 후 결과

### Redis Redirect

| VU | 실행 시간 | 요청 수 | RPS | 평균 | p95 | p99 | 최대 | 실패율 |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 20 | 1분 | 789,780 | 13,155.12 | 1.39ms | 2.81ms | 5.67ms | 198.76ms | 0.00% |
| 50 | 1분 | 975,688 | 16,238.53 | 2.90ms | 6.10ms | 11.20ms | 192.33ms | 0.00% |
| 100 | 1분 | 1,043,207 | 17,371.33 | 5.49ms | 11.93ms | 21.52ms | 154.10ms | 0.00% |

### Baseline 비교

| VU | 지표 | Baseline | Redis 적용 | 변화 |
|---:|---|---:|---:|---:|
| 20 | RPS | 7,950.61 | 미측정 | 미측정 |
| 20 | p95 | 4.35ms | 미측정 | 미측정 |
| 20 | p99 | 6.28ms | 미측정 | 미측정 |
| 50 | RPS | 7,379.92 | 미측정 | 미측정 |
| 50 | p95 | 16.17ms | 미측정 | 미측정 |
| 50 | p99 | 27.73ms | 미측정 | 미측정 |
| 100 | RPS | 8,531.95 | 미측정 | 미측정 |
| 100 | p95 | 32.96ms | 미측정 | 미측정 |
| 100 | p99 | 49.88ms | 미측정 | 미측정 |
| 20 | RPS | 7,950.61 | 13,155.12 | 65.46% 증가 |
| 20 | p95 | 4.35ms | 2.81ms | 35.40% 감소 |
| 20 | p99 | 6.28ms | 5.67ms | 9.71% 감소 |
| 50 | RPS | 7,379.92 | 16,238.53 | 120.04% 증가 |
| 50 | p95 | 16.17ms | 6.10ms | 62.28% 감소 |
| 50 | p99 | 27.73ms | 11.20ms | 59.61% 감소 |
| 100 | RPS | 8,531.95 | 17,371.33 | 103.60% 증가 |
| 100 | p95 | 32.96ms | 11.93ms | 63.80% 감소 |
| 100 | p99 | 49.88ms | 21.52ms | 56.86% 감소 |


## 12. 결과 분석

현재 Baseline에서는 VU 증가에 따라 응답 지연이 커지는 현상을 확인했다.
Redis 적용 후 모든 VU 구간에서 실패율 0%와 Check 성공률 100%를 유지했다.

20 VU에서는 RPS가 약 65% 증가했고 p95는 약 35% 감소했다. 50 VU와 100 VU에서는 RPS가 두 배 이상으로 증가했으며 p95와 p99도 약 56~64% 감소했다.

Redis 적용 후 다음 내용을 비교한다.
따라서 반복 조회가 많은 환경에서는 Redis Cache Hit를 통해 MySQL 조회를 생략하는 방식이 처리량과 응답 지연 개선에 효과적이었다.

- p95와 p99가 감소했는가?
- RPS가 증가했는가?
- MySQL 조회량이 감소했는가?
- HikariCP 사용량이 감소했는가?
- Redis가 새로운 병목으로 나타나는가?
다만 이번 테스트는 고정 VU 방식이므로 응답이 빨라지면 같은 시간 동안 더 많은 요청을 보내게 된다. 또한 CPU, HikariCP, MySQL 조회량을 기록하지 않았으므로 DB 부하가 실제로 얼마나 감소했는지는 추가 측정이 필요하다.

## 13. Platform Thread와 Virtual Thread 비교

Expand Down Expand Up @@ -218,8 +230,8 @@ VIRTUAL_THREADS_ENABLED=true

## 15. 후속 실험

- [ ] Redis Cache Aside 적용
- [ ] 동일한 조건으로 Redis 적용 전후 비교
- [x] Redis Cache Aside 적용
- [x] 동일한 조건으로 Redis 적용 전후 비교
- [ ] Prometheus와 Grafana 서버 지표 기록
- [ ] 조건별 3회 측정 후 중앙값 비교
- [ ] Platform Thread와 Virtual Thread 비교
Expand Down
99 changes: 99 additions & 0 deletions k6/redis-redirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import http from 'k6/http';
import { check } from 'k6';

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

export const options = {
vus: Number(__ENV.VUS || 20),
duration: __ENV.DURATION || '1m',
maxRedirects: 0,

summaryTrendStats: [
'avg',
'min',
'med',
'p(90)',
'p(95)',
'p(99)',
'max',
],

thresholds: {
checks: ['rate>0.99'],
http_req_failed: ['rate<0.01'],
'http_req_duration{endpoint:redirect}': ['p(95)<500']
}
};

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) => {
const body = res.json();
return body.shortCode !== undefined && body.shortCode !== null;
}
});

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

const shortCode = response.json().shortCode;

const warmUpResponse = http.get(
`${BASE_URL}/api/v1/${shortCode}`,
{
redirects: 0,
tags: {
endpoint: 'warm-up'
}
}
)

if (warmUpResponse.status !== 302) {
throw new Error(
`캐시 Warm-up 실패: status=${warmUpResponse.status}`
);
}

return {
shortCode
};
}

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

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