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
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ services:

CACHE_ENABLED: ${CACHE_ENABLED:-true}

DB_POOL_MAX_SIZE: ${DB_POOL_MAX_SIZE:-10}

depends_on:
mysql:
condition: service_healthy
Expand Down
162 changes: 130 additions & 32 deletions docs/04-experiment.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,34 @@ MySQL만 사용하는 초기 리다이렉트 구조에서 VU 증가에 따라

## 4. 통제 변수

### 공통 조건

- 동일한 단축 URL 1개 반복 조회
- 애플리케이션 인스턴스 1개
- MySQL 인스턴스 1개
- Java 25
- Platform Thread 사용
- HikariCP 기본 설정
- Redirect 추적 비활성화
- Think Time 없음
- 조건별 테스트 시간 1분

### Redis 비교

- Platform Thread 사용
- HikariCP 최대 커넥션 10개
- 캐시 활성화 여부만 변경

### Thread 비교

- DB Only
- HikariCP 최대 커넥션 10개
- Platform Thread와 Virtual Thread만 변경

### HikariCP 비교

- DB Only
- Platform Thread
- HikariCP 최대 커넥션만 5, 10, 20으로 변경

## 5. 실험 환경

| 항목 | 값 |
Expand Down Expand Up @@ -88,11 +106,14 @@ MySQL만 사용하는 초기 리다이렉트 구조에서 VU 증가에 따라

### 서버 관점

- CPU
- Process CPU
- JVM Heap
- 활성 스레드
- HikariCP 활성·대기 연결
- Platform Thread 수
- HikariCP Active·Pending
- MySQL 조회량
- Virtual Thread Mounted·Queued
- Carrier Pool Size
- Virtual Thread Pinning·제출 실패

CPU, JVM, HikariCP, DB Lookup 지표는 100 VU 비교 실험에서 기록했다.

Expand Down Expand Up @@ -225,57 +246,134 @@ Redis 적용 후 모든 VU 구간에서 실패율 0%를 유지하면서 처리
100 VU 서버 지표에서 DB Only는 모든 요청마다 MySQL을 조회했다.
HikariCP의 10개 커넥션이 모두 사용됐고 최대 약 85개의 요청이 커넥션을 기다렸다.

Redis 적용 후에는 최초 1회를 제외한 요청이 Cache Hit로 처리됐다.
DB 조회와 커넥션 대기가 제거되면서 응답 지연이 감소하고 처리량이 증가했다.
Redis 적용 후에는 최초 1회를 제외한 요청이 Cache Hit로 처리됐다.
반복적인 DB 조회가 사실상 제거됐으며,
Prometheus 수집 시점에서는 HikariCP 연결 사용과 대기가 관찰되지 않았다.
이에 따라 응답 지연이 감소하고 처리량이 증가했다.

따라서 반복 조회가 많은 리다이렉트 경로에서는 Redis Cache Aside가 DB 접근과 커넥션 풀 병목을 줄이는 데 효과적이었다.

## 13. Platform Thread와 Virtual Thread 비교

Redis 적용 실험 이후 동일한 조건으로 비교한다.
DB Only, HikariCP 최대 커넥션 10개, 100 VU, 1분 조건에서
스레드 방식만 변경해 비교했다.

### 실험 A
| 지표 | Platform Thread | Virtual Thread |
|---|---:|---:|
| 요청 수 | 337,498 | 238,090 |
| RPS | 5,576.15 | 3,942.58 |
| 평균 응답 시간 | 17.56ms | 25.04ms |
| p95 | 50.24ms | 56.39ms |
| p99 | 105.40ms | 152.11ms |
| 최대 응답 시간 | 1.07s | 1.28s |
| Process CPU 최대 | 약 90% | 약 62% |
| Platform Live Threads 최대 | 약 121 | 약 34 |
| HikariCP Active 최대 | 10 | 10 |
| HikariCP Pending 최대 | 약 85 | 약 87 |
| 오류율 | 0% | 0% |

Virtual Thread 적용 후 Platform Thread 수와 Process CPU 사용량은 감소했다.

하지만 RPS는 감소했고 평균, p95, p99 응답 시간은 증가했다.
두 방식 모두 HikariCP 최대 커넥션 10개를 사용했으며,
약 80개 이상의 요청이 DB 커넥션을 기다렸다.

따라서 이번 조건에서는 Virtual Thread만으로 처리량이 개선되지 않았고,
DB 커넥션 풀이 주요 제한 요소로 남았다.

다만 조건별 한 번만 측정했으므로
Virtual Thread가 항상 Platform Thread보다 느리다고 일반화할 수는 없다.

### Virtual Thread 실행 지표

| 지표 | 결과 |
|---|---:|
| Mounted 관찰 최대 | 약 8 |
| Queued 관찰 최대 | 약 9 |
| Carrier Pool Size 최대 | 8 |
| Target Parallelism | 8 |
| Pinned Events | 0 |
| Submit Failed | 0 |

Virtual Thread는 최대 8개의 Carrier Thread 위에서 실행됐다.
부하 테스트 중 Carrier Thread를 점유한 채 대기하는 Pinning과
Virtual Thread 제출 실패는 발생하지 않았다.

```text
Java 25 + Platform Thread
VIRTUAL_THREADS_ENABLED=false
```
#### Grafana 측정 결과

### 실험 B
**Platform Thread**

```text
Java 25 + Virtual Thread
VIRTUAL_THREADS_ENABLED=true
```
![Platform Thread 100 VU 성능 지표](images/platform-thread-100vu-performance.png)

| 지표 | Platform Thread | Virtual Thread |
|---|---:|---:|
| RPS | 미측정 | 미측정 |
| p95 | 미측정 | 미측정 |
| p99 | 미측정 | 미측정 |
| CPU | 미측정 | 미측정 |
| 활성 스레드 | 미측정 | 미측정 |
| DB Pool 대기 | 미측정 | 미측정 |
| 오류율 | 미측정 | 미측정 |
![Platform Thread 100 VU DB 및 커넥션 풀 지표](images/platform-thread-100vu-db-pool.png)

**Virtual Thread**

![Virtual Thread 100 VU 성능 지표](images/virtual-thread-100vu-performance.png)

![Virtual Thread 100 VU DB 및 커넥션 풀 지표](images/virtual-thread-100vu-db-pool.png)

![Virtual Thread 실행 지표](images/virtual-thread-100vu-metrics.png)

## 14. HikariCP Pool 크기 비교

Thread 비교에서 HikariCP Active가 최대 커넥션 10개에 도달하고
Pending 요청이 발생했다.

커넥션 풀 크기가 실제 처리량과 응답 시간에 미치는 영향을 확인하기 위해
DB Only, Platform Thread, 100 VU, 1분 조건에서
최대 커넥션 수만 5, 10, 20으로 변경했다.

| 지표 | Pool 5 | Pool 10 | Pool 20 |
|---|---:|---:|---:|
| 요청 수 | 341,196 | 466,563 | 438,566 |
| RPS | 5,670.37 | 7,753.05 | 7,288.71 |
| 평균 응답 시간 | 17.47ms | 12.66ms | 13.49ms |
| p95 | 51.37ms | 36.01ms | 38.87ms |
| p99 | 99.99ms | 74.36ms | 70.45ms |
| 최대 응답 시간 | 830.87ms | 598.46ms | 335.53ms |
| 오류율 | 0% | 0% | 0% |

Pool 5에서 Pool 10으로 증가했을 때 처리량이 증가하고
평균, p95, p99 응답 시간이 모두 감소했다.
Pool 5는 100개의 동시 요청을 처리하기에 커넥션 수가 부족했던 것으로 보인다.

반면 Pool 20은 Pool 10보다 p99와 최대 응답 시간은 감소했지만,
RPS가 낮아지고 평균과 p95가 증가했다.

이번 단일 실행에서는 Pool 10이 처리량과 일반적인 응답 지연 측면에서
가장 균형 있는 결과를 보였다.
커넥션 수를 늘린다고 성능이 계속 향상되는 것은 아니며,
동시에 실행되는 쿼리 증가에 따른 DB 경합과 로컬 실행 환경의 변동도
함께 고려해야 한다.

Pool 20 실험에서는 k6 iterations와 DB Lookup이 모두 438,566건으로 일치했다.
Cache Hit과 Cache Miss는 모두 0건이므로
모든 리다이렉트 요청이 MySQL 조회로 처리됐음을 확인했다.

> 조건별 한 번만 실행한 결과이므로 Pool 10을 최적값으로 확정할 수는 없다.
> 정확한 최적값을 결정하려면 조건별 반복 측정과 MySQL 서버 지표가 필요하다.

## 14. 실험 한계
## 15. 실험 한계

- 로컬 Docker 환경에서 실행했다.
- k6, 애플리케이션, MySQL, Redis가 같은 장비의 자원을 사용했다.
- 조건별 테스트를 한 번씩만 수행했다.
- 조건별 한 번만 측정해 실행 환경의 변동이 포함될 수 있다.
- 테스트 시간이 조건별 1분으로 짧다.
- 하나의 단축 URL만 반복 조회했다.
- Prometheus 수집 간격 사이의 짧은 HikariCP 사용은 그래프에서 누락될 수 있다.
- Prometheus 수집 간격 사이의 짧은 지표 변화는 누락될 수 있다.
- 고정 VU 방식이므로 응답 시간이 짧을수록 같은 시간에 더 많은 요청을 전송한다.
- HikariCP Pool 비교에서 MySQL CPU, 실행 중인 쿼리 수, 락 대기까지는 측정하지 않았다.

## 15. 후속 실험
## 16. 후속 실험

- [x] Redis Cache Aside 적용
- [x] Redis 적용 전후 부하 테스트
- [x] Prometheus·Grafana 서버 지표 비교
- [x] 캐시 ON/OFF 환경변수 적용
- [x] Platform Thread와 Virtual Thread 비교
- [x] HikariCP Pool 크기 비교
- [ ] 조건별 3회 측정 후 중앙값 비교
- [ ] Platform Thread와 Virtual Thread 비교
- [ ] 더 높은 VU로 Stress Test 수행
- [ ] 단축 코드 생성 전략 비교
- Sequence ID + Base62
Expand Down
Loading