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 @@ -24,6 +24,8 @@ services:

DB_POOL_MAX_SIZE: ${DB_POOL_MAX_SIZE:-10}

SHORT_CODE_STRATEGY: ${SHORT_CODE_STRATEGY:-sequence}

depends_on:
mysql:
condition: service_healthy
Expand Down
43 changes: 36 additions & 7 deletions docs/04-experiment.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,39 @@ DB Only보다 약 2.16배 높은 전체 평균 처리량과 낮은 p95를 기록

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

## 16. 실험 한계
## 16. 단축 코드 생성 전략 비교

Sequence ID + Base62 방식과 SHA-256 Hash + Base62 방식의
단축 URL 생성 성능을 비교했다.

두 방식 모두 Platform Thread, HikariCP 최대 커넥션 10개,
20 VU, 1분 조건에서 측정했으며,
실제 신규 생성 경로를 실행하기 위해 요청마다 서로 다른 URL을 사용했다.

| 지표 | Sequence + Base62 | Hash + Base62 |
|---|---:|---:|
| 요청 수 | 97,495 | 77,933 |
| RPS | 1,624.60 | 1,298.60 |
| 평균 응답 시간 | 12.12ms | 15.18ms |
| p95 | 22.23ms | 30.14ms |
| 최대 응답 시간 | 254.82ms | 467.43ms |
| 실패율 | 0% | 0% |

이번 측정에서 Sequence 방식은 Hash 방식보다 RPS가 약 25.1% 높았고,
평균 응답 시간과 p95도 더 낮았다.

현재 구현에서 Sequence 방식은 `INSERT → ID 발급 → short_code UPDATE`를 수행한다.
Hash 방식은 `중복 코드 조회 → SHA-256 계산 → INSERT`를 수행하며,
저장 충돌 재시도를 위해 저장 트랜잭션을 분리했다.

**로컬 단일 MySQL 환경에서는 충돌 검사와 재시도가 필요 없는
Sequence + Base62 방식이 더 단순하고 높은 처리량을 보였다.
따라서 현재 기본 생성 전략으로 Sequence 방식을 유지한다.**

다만 전략별 한 번만 측정했으며,
해시 연산과 DB 조회·트랜잭션 비용을 각각 분리해 측정하지는 않았다.

## 17. 실험 한계

- 로컬 Docker 환경에서 실행했다.
- k6, 애플리케이션, MySQL, Redis가 같은 장비의 자원을 사용했다.
Expand All @@ -460,7 +492,7 @@ DB Only보다 약 2.16배 높은 전체 평균 처리량과 낮은 p95를 기록
- Stress Test의 k6 최종 결과는 모든 VU 구간을 합산한 값이므로, 특정 VU 구간의 값은 Grafana 시계열을 통해 판단했다.
- Redis Stress Test의 처리량 한계가 애플리케이션, Redis 또는 로컬 환경 중 어디에서 발생했는지는 추가로 분리하지 않았다.

## 17. 후속 실험
## 18. 후속 실험

- [x] Redis Cache Aside 적용
- [x] Redis 적용 전후 부하 테스트
Expand All @@ -469,8 +501,5 @@ DB Only보다 약 2.16배 높은 전체 평균 처리량과 낮은 p95를 기록
- [x] Platform Thread와 Virtual Thread 비교
- [x] HikariCP Pool 크기 비교
- [x] 더 높은 VU로 Stress Test 수행
- [ ] 조건별 3회 측정 후 중앙값 비교
- [ ] 단축 코드 생성 전략 비교
- Sequence ID + Base62
- Hash + 충돌 처리
- 분산 ID + Base62
- [x] Sequence ID + Base62와 Hash + 충돌 처리 비교
- [ ] 분산 ID + Base62 비교
44 changes: 44 additions & 0 deletions k6/create-load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import http from 'k6/http';
import { check } from 'k6';

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

thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<1000'],
},
};

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

const RUN_ID = __ENV.RUN_ID || 'local';

export default function () {
/*
* Hash 방식에서 같은 URL을 반복 요청하면
* 기존 코드를 반환하는 조회 경로가 실행된다.
*
* 실제 생성 성능을 비교하기 위해 요청마다
* 서로 다른 longUrl을 사용한다.
*/
const longUrl = `https://example.com/${RUN_ID}/${__VU}/${__ITER}`;

const response = http.post(
`${BASE_URL}/api/v1/data/shorten`,
JSON.stringify({
longUrl: longUrl,
}),
{
headers: {
'Content-Type': 'application/json',
},
}
);

check(response, {
'shorten status is 2xx': (res) =>
res.status >= 200 && res.status < 300,
});
}
133 changes: 133 additions & 0 deletions results/short-code-generation/create-hash-20vu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"metrics": {
"http_reqs": {
"count": 77933,
"rate": 1298.5955861946923
},
"http_req_sending": {
"avg": 0.02495120167322926,
"min": 0.006,
"med": 0.02,
"max": 41.789,
"p(90)": 0.032,
"p(95)": 0.038
},
"http_req_duration": {
"max": 467.427,
"p(90)": 23.289800000000003,
"p(95)": 30.137199999999954,
"avg": 15.17851957450626,
"min": 3.908,
"med": 12.146,
"thresholds": {
"p(95)<1000": false
}
},
"http_req_failed": {
"passes": 0,
"fails": 77933,
"thresholds": {
"rate<0.01": false
},
"value": 0
},
"iteration_duration": {
"avg": 15.378397084457212,
"min": 4.021083,
"med": 12.337708,
"max": 467.778625,
"p(90)": 23.507333600000003,
"p(95)": 30.383824999999963
},
"http_req_connecting": {
"max": 6.642,
"p(90)": 0,
"p(95)": 0,
"avg": 0.011304902929439387,
"min": 0,
"med": 0
},
"iterations": {
"count": 77933,
"rate": 1298.5955861946923
},
"data_received": {
"count": 14899795,
"rate": 248274.90308605786
},
"vus_max": {
"min": 20,
"max": 20,
"value": 20
},
"http_req_waiting": {
"p(95)": 29.860799999999987,
"avg": 15.011057921547803,
"min": 3.855,
"med": 12.011,
"max": 467.278,
"p(90)": 23.088
},
"vus": {
"min": 20,
"max": 20,
"value": 20
},
"http_req_receiving": {
"avg": 0.14251045128507617,
"min": 0.013,
"med": 0.066,
"max": 110.449,
"p(90)": 0.261,
"p(95)": 0.454
},
"checks": {
"passes": 77933,
"fails": 0,
"value": 1
},
"data_sent": {
"count": 15451387,
"rate": 257466.06647743637
},
"http_req_tls_handshaking": {
"max": 0,
"p(90)": 0,
"p(95)": 0,
"avg": 0,
"min": 0,
"med": 0
},
"http_req_duration{expected_response:true}": {
"avg": 15.17851957450626,
"min": 3.908,
"med": 12.146,
"max": 467.427,
"p(90)": 23.289800000000003,
"p(95)": 30.137199999999954
},
"http_req_blocked": {
"p(95)": 0.008,
"avg": 0.018988143661864423,
"min": 0.001,
"med": 0.005,
"max": 9.821,
"p(90)": 0.007
}
},
"root_group": {
"name": "",
"path": "",
"id": "d41d8cd98f00b204e9800998ecf8427e",
"groups": {},
"checks": {
"shorten status is 2xx": {
"name": "shorten status is 2xx",
"path": "::shorten status is 2xx",
"id": "9d3cc5a6164524f56dde377dfce89cb7",
"passes": 77933,
"fails": 0
}
}
}
}
133 changes: 133 additions & 0 deletions results/short-code-generation/create-sequence-20vu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"root_group": {
"id": "d41d8cd98f00b204e9800998ecf8427e",
"groups": {},
"checks": {
"shorten status is 2xx": {
"name": "shorten status is 2xx",
"path": "::shorten status is 2xx",
"id": "9d3cc5a6164524f56dde377dfce89cb7",
"passes": 97495,
"fails": 0
}
},
"name": "",
"path": ""
},
"metrics": {
"vus": {
"value": 20,
"min": 20,
"max": 20
},
"http_req_duration{expected_response:true}": {
"p(90)": 17.139,
"p(95)": 22.233500000000014,
"avg": 12.123366131596507,
"min": 3.792,
"med": 10.596,
"max": 254.824
},
"http_req_connecting": {
"avg": 0.010780368224011473,
"min": 0,
"med": 0,
"max": 6.77,
"p(90)": 0,
"p(95)": 0
},
"iteration_duration": {
"p(95)": 22.4492,
"avg": 12.292302568716453,
"min": 3.893917,
"med": 10.750541,
"max": 255.035792,
"p(90)": 17.335675200000008
},
"http_req_duration": {
"med": 10.596,
"max": 254.824,
"p(90)": 17.139,
"p(95)": 22.233500000000014,
"avg": 12.123366131596507,
"min": 3.792,
"thresholds": {
"p(95)<1000": false
}
},
"http_req_failed": {
"passes": 0,
"fails": 97495,
"thresholds": {
"rate<0.01": false
},
"value": 0
},
"http_reqs": {
"count": 97495,
"rate": 1624.5978934833338
},
"http_req_blocked": {
"med": 0.004,
"max": 9.566,
"p(90)": 0.007,
"p(95)": 0.008,
"avg": 0.017171649828190052,
"min": 0.001
},
"vus_max": {
"value": 20,
"min": 20,
"max": 20
},
"checks": {
"passes": 97495,
"fails": 0,
"value": 1
},
"http_req_receiving": {
"min": 0.013,
"med": 0.052,
"max": 24.353,
"p(90)": 0.196,
"p(95)": 0.354,
"avg": 0.10637411149289834
},
"http_req_tls_handshaking": {
"min": 0,
"med": 0,
"max": 0,
"p(90)": 0,
"p(95)": 0,
"avg": 0
},
"http_req_waiting": {
"p(95)": 22.03110000000002,
"avg": 11.996838001948772,
"min": 3.455,
"med": 10.489,
"max": 250.847,
"p(90)": 16.962600000000005
},
"http_req_sending": {
"p(95)": 0.033,
"avg": 0.020154018154776295,
"min": 0.005,
"med": 0.017,
"max": 5.25,
"p(90)": 0.027
},
"data_received": {
"count": 17852017,
"rate": 297475.24706527166
},
"iterations": {
"count": 97495,
"rate": 1624.5978934833338
},
"data_sent": {
"count": 19725426,
"rate": 328692.60503268254
}
}
}
Loading